django-returnfieldsというパッケージを作っていました
django-returnfields というパッケージを作っていました。
これは何?
はじめはapi responseのfilteringをするライブラリとして作っていましたが、いろいろな変更の結果あるAPIのresponseに対してそのsubsetを返すためのoptimizerのようなものになりました。
具体的には以下の機能を持っています。
skip_fields
,return_fields
optionによるresponseのfilteringaggressive
optionによるDB queryのoptimize
すごく高速に動作するというよりは、遅くなっている状態を避けようという感じのものなので最適な結果を保証するものではなかったりします。またrichなresponseを返すREST API以外ではあまり意味が無いかもしれません。
responseのfiltering
以下のようなresponseを返すAPIがあるとします。
// /api/users { "id": 1, "username": "foo", "skills": [ { "id": 1, "user": 1, "name": "magic" }, { "id": 2, "user": 1, "name": "magik" } ] }
return_fields
これに return_fields
optionを使って nameだけを取り出す事ができます。
// /api/users/?return_fields="username, skills__name" { "username": "foo", "skills": [ { "name": "magic" }, { "name": "magik" } ] }
skip_fields
かわりに skip_fields
optionを使っても同様のことができます。ただしこちらはresponseに含めたくないフィールドを指定します。
// /api/users/?skip_fields=skills__id,skills__user { "username": "foo", "skills": [ { "name": "magic" }, { "name": "magik" } ] }
why return_fields and skip_fields?
通常のdjangoの作法によるとこのような何らかのコレクションに対する絞り込みのoption名には include
, exclude
のペアを使うことが多いです。ですが、このパッケージでは return_fields
と skip_fields
という別の名前を使っています。
理由は、include
, exclude
の場合には相互排他的な意味を持っているためです。例えば、include=a,b,c
のときexclude=a
とした時には条件の指定がconflictしてエラーになります。
一方、 return_fields=a,b,c
かつ skip_fields=a
の意味は全体集合として {a, b, c}
を取り、そこから {c}
との差集合を取るという意味になるので {b,c}
として検索される事になります。また、return_fields
の指定がなかった場合には暗黙に可能なかぎり全部のfieldsを出力対象にするということになります。
DB queryのoptimize
aggressive=1
というoptionを付けるとoptimizerとしても機能します。具体的には以下の事をします。
- modelの定義に基づく prefetch, joinを付加する
- 選択されたfieldだけをonly,deferで取り出す
例えば上のAPIで /users/?format=json&return_fields=username,skills&skip_fields=skills__id,skills__user
によるアクセスは以下のようなqueryが実行されますが。
(0.000) SELECT "user"."id", "user"."password", "user"."last_login", "user"."is_superuser", "user"."username", "user"."first_name", "user"."last_name", "user"."email", "user"."is_staff", "user"."is_active", "user"."date_joined" FROM "user"; args=() (0.000) SELECT "skill"."id", "skill"."name", "skill"."user_id" FROM "skill" WHERE "skill"."user_id" = 1; args=(1,) (0.000) SELECT "skill"."id", "skill"."name", "skill"."user_id" FROM "skill" WHERE "skill"."user_id" = 2; args=(2,)
以下のようなアクセスの場合には /users/?format=json&aggressive=1&return_fields=username,skills&skip_fields=skills__id,skills__user
eager loadingが効きます。そして不要なフィールドがselect句に含まれません。
(0.000) SELECT "user"."id", "user"."username" FROM "user"; args=() (0.000) SELECT "skill"."id", "skill"."name", "skill"."user_id" FROM "skill" WHERE "skill"."user_id" IN (2, 1); args=(2, 1)
またこの例ではネストが1段だけですがN段のネストにも対応しています。そして、skip_fields
や return_fields
によりeager loadingの必要性がなくなった場合にはeager loadingが行われません。
あとで詳しく
optimization関係や実際のdjango restframeworkとの組み合わせ(特にpaginationとの組み合わせ)などではもう少し説明する必要があるところがありますがとりあえず今回はここまで。