couch_httpd_db:db_req/2
前回までドキュメントの取得の流れを見てきたので、次に更新まわりを見ていこうと思います。
couch_httpd_db.erl:
1db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
2 couch_httpd:validate_ctype(Req, "application/json"),
3 Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
4 validate_attachment_names(Doc),
5 Doc2 = case Doc#doc.id of
6 <<"">> ->
7 Doc#doc{id=couch_uuids:new(), revs={0, []}};
8 _ ->
9 Doc
10 end,OA
11 DocId = Doc2#doc.id,
12 update_doc(Req, Db, DocId, Doc2);
実際に確かめたわけではないのですが、ドキュメントの更新APIの入り口はこのあたりかな、と。couch_doc:from_json_obj/1
とcouch_httpd:json_body/1
でJSONNからドキュメントに変換しているようなので、このあたりのコードを見ています。まずはcouch_httpd:json_body/1
から。
couch_httpd_db.erl:
1json_body(Httpd) ->
2 ?JSON_DECODE(maybe_decompress(Httpd, body(Httpd))).
?JSON_DECODE
というマクロが指定されています。マクロの定義を見てみます。
couch_db.hrl:
1-define(JSON_DECODE(V), ejson:decode(V)).
?JSON_DECODE
マクロの実体はejson:decode/1
でした。JSONのエンコード、デコードを変更しやすいようにマクロにしているようです。ejsonのREADMEを見ると、ejson:decode/1
は
{[{<<"foo">>,true}]}
といったように、JSONをErlangのタプルとリストに変換する関数です。次にcouch_httpd:body/1
を見てみます。
couch_httpd.erl:
1body(#httpd{mochi_req=MochiReq, req_body=undefined}) ->
2 MaxSize = list_to_integer(
3 couch_config:get("couchdb", "max_document_size", "4294967296")),
4 MochiReq:recv_body(MaxSize);
5body(#httpd{req_body=ReqBody}) ->
6 ReqBody.
Bodyのサイズの上限は4GBとなってました。変数の名前から、HTTPの部分はmochiwebが使われているようです。
ejsonにより、JSONがタプルとリストの組み合わせに変換されるので、couch_doc:from_json_obj/1
はそれをドキュメントに変換する関数になります。コードを見てみます。
couch_doc.erl:
1from_json_obj({Props}) ->
2 transfer_fields(Props, #doc{body=[]});
3
4from_json_obj(_Other) ->
5 throw({bad_request, "Document must be a JSON object"}).
指定されたJSONオブジェクトがタプルでなければエラーとしています。そのままtransfer_fields/2
を見てみます。
couch_doc.erl:
1transfer_fields([], #doc{body=Fields}=Doc) ->
2 % convert fields back to json object
3 Doc#doc{body={lists:reverse(Fields)}};
4
5transfer_fields([{<<"_id">>, Id} | Rest], Doc) ->
6 validate_docid(Id),
7 transfer_fields(Rest, Doc#doc{id=Id});
8
9transfer_fields([{<<"_rev">>, Rev} | Rest], #doc{revs={0, []}}=Doc) ->
10 {Pos, RevId} = parse_rev(Rev),
11 transfer_fields(Rest,
12 Doc#doc{revs={Pos, [RevId]}});
13...
couch_doc:transer_fields
は18個ほど定義されており、各々のパターンによって#doc
の構築の仕方が定義されています。サスガにひとつひとつ見るのはツラいので今回は追いませんが、この関数を実行するとJSONオブジェクトが#doc
に変換されて返る、ということは分かりました。指定したJSONが思った形でDBに保存されないようであれば、このあたりを見てみると良いかもしれません。
couch_httpd_db:db_req/2
に戻って、validate_attachment_names/1
。これはattachementのnameがUTF-8かどうかチェックしています。Attachementsが何かについてはこちらを参照。
#doc.id
が設定されていなければUUIDを生成して#doc.idに設定してrevを初期化し、couch_httpd_db:update_doc/4
を呼び出します。
couch_httpd_db.erl:
1update_doc(Req, Db, DocId, #doc{deleted=false}=Doc) ->
2 Loc = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)),
3 update_doc(Req, Db, DocId, Doc, [{"Location", Loc}]);
4update_doc(Req, Db, DocId, Doc) ->
5 update_doc(Req, Db, DocId, Doc, []).
#doc.deleted
がfalse
の場合はドキュメントのURIを生成して"Location"
とし、update_doc/5
を呼び出します。
couch_httpd_db.erl:
1update_doc(Req, Db, DocId, Doc, Headers) ->
2 #doc_query_args{
3 update_type = UpdateType
4 } = parse_doc_query(Req),
5 update_doc(Req, Db, DocId, Doc, Headers, UpdateType).
Req
を指定してparse_doc_query/1
を呼び出し、UpdateType
を取得しています。この値を取得する過程を追ってみます。
couch_httpd_db.erl:
1parse_doc_query(Req) ->
2 lists:foldl(fun({Key,Value}, Args) ->
3 case {Key, Value} of
4 {"attachments", "true"} ->
5 Options = [attachments | Args#doc_query_args.options],
6 Args#doc_query_args{options=Options};
7 {"meta", "true"} ->
8 Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
9 Args#doc_query_args{options=Options};
10 {"revs", "true"} ->
11 Options = [revs | Args#doc_query_args.options],
12 Args#doc_query_args{options=Options};
13 {"local_seq", "true"} ->
14 Options = [local_seq | Args#doc_query_args.options],
15 Args#doc_query_args{options=Options};
16 {"revs_info", "true"} ->
17 Options = [revs_info | Args#doc_query_args.options],
18 Args#doc_query_args{options=Options};
19 {"conflicts", "true"} ->
20 Options = [conflicts | Args#doc_query_args.options],
21 Args#doc_query_args{options=Options};
22 {"deleted_conflicts", "true"} ->
23 Options = [deleted_conflicts | Args#doc_query_args.options],
24 Args#doc_query_args{options=Options};
25 {"rev", Rev} ->
26 Args#doc_query_args{rev=couch_doc:parse_rev(Rev)};
27 {"open_revs", "all"} ->
28 Args#doc_query_args{open_revs=all};
29 {"open_revs", RevsJsonStr} ->
30 JsonArray = ?JSON_DECODE(RevsJsonStr),
31 Args#doc_query_args{open_revs=couch_doc:parse_revs(JsonArray)};
32 {"latest", "true"} ->
33 Options = [latest | Args#doc_query_args.options],
34 Args#doc_query_args{options=Options};
35 {"atts_since", RevsJsonStr} ->
36 JsonArray = ?JSON_DECODE(RevsJsonStr),
37 Args#doc_query_args{atts_since = couch_doc:parse_revs(JsonArray)};
38 {"new_edits", "false"} ->
39 Args#doc_query_args{update_type=replicated_changes};
40 {"new_edits", "true"} ->
41 Args#doc_query_args{update_type=interactive_edit};
42 {"att_encoding_info", "true"} ->
43 Options = [att_encoding_info | Args#doc_query_args.options],
44 Args#doc_query_args{options=Options};
45 _Else -> % unknown key value pair, ignore.
46 Args
47 end
48 end, #doc_query_args{}, couch_httpd:qs(Req)).
クエリパラメータからオプションに変換しているようですが、この関数内ではupdate_type
が出てこないので_Else
に落ち、このパラメータは#doc_query_args.options
には設定されないようです。ちょっと変な感じもするけど、あってるのかな…。update_type
が取得できなかったと想定して、update_doc/6
を見ていきます。
couch_httpd_db.erl:
1update_doc(Req, Db, DocId, #doc{deleted=Deleted}=Doc, Headers, UpdateType) ->
2 case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
3 "true" ->
4 Options = [full_commit];
5 "false" ->
6 Options = [delay_commit];
7 _ ->
8 Options = []
9 end,
10 case couch_httpd:qs_value(Req, "batch") of
11 "ok" ->
12 % async batching
13 spawn(fun() ->
14 case catch(couch_db:update_doc(Db, Doc, Options, UpdateType)) of
15 {ok, _} -> ok;
16 Error ->
17 ?LOG_INFO("Batch doc error (~s): ~p",[DocId, Error])
18 end
19 end),
20 send_json(Req, 202, Headers, {[
21 {ok, true},
22 {id, DocId}
23 ]});
24 _Normal ->
25 % normal
26 {ok, NewRev} = couch_db:update_doc(Db, Doc, Options, UpdateType),
27 NewRevStr = couch_doc:rev_to_str(NewRev),
28 ResponseHeaders = [{"ETag", <<"\"", NewRevStr/binary, "\"">>}] ++ Headers,
29 send_json(Req,
30 if Deleted orelse Req#httpd.method == 'DELETE' -> 200;
31 true -> 201 end,
32 ResponseHeaders, {[
33 {ok, true},
34 {id, DocId},
35 {rev, NewRevStr}]})
36 end.
まず、ヘッダにX-Couch-Full-Commit
が指定されている場合はfull_commit
を、指定されていない場合はdelay_commit
をオプションとしています。クエリパラメータにbatch=ok
が付いていると非同期で、付いていない場合は同期でcouch_db:update_doc/4
を呼び出します。
Conclusion
HTTP経由でドキュメントを更新する流れを見てきました。肝心のcouch_db:update_doc/4
は次回読んでみる予定です。