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/1couch_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.deletedfalseの場合はドキュメントの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は次回読んでみる予定です。