読者です 読者をやめる 読者になる 読者になる

django-returnfieldsというパッケージを作っていました

django-returnfields というパッケージを作っていました。

これは何?

はじめはapi responseのfilteringをするライブラリとして作っていましたが、いろいろな変更の結果あるAPIのresponseに対してそのsubsetを返すためのoptimizerのようなものになりました。

具体的には以下の機能を持っています。

  • skip_fields, return_fields optionによるresponseのfiltering
  • aggressive 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_fieldsskip_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_fieldsreturn_fields によりeager loadingの必要性がなくなった場合にはeager loadingが行われません。

あとで詳しく

optimization関係や実際のdjango restframeworkとの組み合わせ(特にpaginationとの組み合わせ)などではもう少し説明する必要があるところがありますがとりあえず今回はここまで。