How to write custom lookups

Django には (たとえば exacticontains などの) フィルタリングを行う built-in lookups が数多くあります。このドキュメントでは、カスタムルックアップをどのように作るかや、既存のルックアップの動作をどのようにして変更するかについて説明します。ルックアップの API リファレンスについては Lookup API reference を参照してください。

ルックアップの例

簡単なカスタムルックアップから始めましょう。``exact``と逆の動作をするカスタムルックアップ``ne``を実装していきます。``Author.objects.filter(name__ne='Jack')``は次のSQLに変換されます。

"author"."name" <> 'Jack'

この SQL はバックエンドに依存しない書き方になっているため、別のデータベースについて心配する必要はありません。

カスタムルックアップを動作させるためには2つのステップが必要です。まず最初にルックアップを実装し、続いてDjangoに実装したルックアップを認識させる必要があります。

from django.db.models import Lookup

class NotEqual(Lookup):
    lookup_name = 'ne'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s <> %s' % (lhs, rhs), params

NotEqual ルックアップを登録するために、ルックアップを有効にしたいフィールドクラスの register_lookup を呼び出す必要があります。今回実装したルックアップは Field クラスの全てのサブクラスに適用できるため、 Field クラスに直接登録しましょう。

from django.db.models import Field
Field.register_lookup(NotEqual)

ルックアップは、デコレータパタンを使っても登録できます。

from django.db.models import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...

これ以降、どのようなフィールドであっても foo に対しては foo__ne が利用できます。なお、これを用いるクエリーセットを生成するならば、それ以前にルックアップの登録を行っておく必要があります。実装する場所は models.py ファイル内で行ってもよいし、AppConfig 内の ready() メソッドにてルックアップの登録を行うのでも構いません。

実装の詳細について見ると、最初に必要な属性は lookup_name です。この属性があると、ORM が name_ne を解釈できるようになり、NotEqual を使って SQL を生成できます。慣習として、こうした名前は常に小文字のアルファベットのみからなる文字列にしますが、必ず守らなければならない制約は、文字列 __ を決して含んではならないということです。

次に、"as_sql" メソッドを定義する必要があります。これには "compiler" という"SQL Compiler" オブジェクトと、アクティブなデータベース接続を必要とします。"SQL Compiler" オブジェクトについてのドキュメントはありませんが、それがSQL文字列を含むタプルと文字列に挿入されるパラメータを返す``compile()`` メソッドを持つ、ということだけ知っていれば十分です。ほとんどの場合、これを直接用いる必要はなく、process_lhs() および process_rhs() に渡すことができます。

2つの値に対して動作するルックアップ、 lhsrhs はleft-hand side(左辺)とright-hand side(右辺) を表します。左辺は通常フィールド参照ですが、 query expression API を実装するものであれば何でもかまいません。右辺はユーザーから与えられた値です。 Author.objects.filter(name__ne='Jack') の例では、左辺は Author モデルの name フィールドへの参照で、右辺は 'Jack' です。

process_lhs と``process_rhs`` を呼び出して、前述の compiler オブジェクトを使用してSQLに必要な値に変換します。 これらのメソッドは、 as_sql メソッドから戻る必要があるのと同じように、SQLとそのSQLに補間されるパラメーターを含むタプルを返します。 上記の例では、 process_lhs('"author"."name"', []) を返し、process_rhs('"%s"', ['Jack']) を返します 。 この例では、左側にパラメーターはありませんが、これは所有するオブジェクトに依存するため、返すパラメーターにそれらを含める必要があります。

最後に、これらの部分を `` <> `` を使ってSQL式にまとめ、クエリのすべてのパラメータを指定します。その後、生成されたSQL文字列とパラメータを含むタプルを返します。

変換の例

上記のカスタムルックアップは素晴らしいですが、場合によってはルックアップを一緒にチェーンできるようにしたいことがあります。 たとえば、abs() 演算子を利用したいアプリケーションを構築しているとしましょう。 開始値、終了値モデルおよび変更 (start - end) を記録する Experiment モデルがあります。 変化が一定の量に等しい (Experiment.objects.filter(change__abs=27))、またはそれが一定の量を超えなかった (Experiment.objects.filter(change__abs__lt=27)) すべての Experiment を見つけたいと思います。

注釈

この例は多少工夫がされていますが、データベースバックエンドに依存しない方法で、そしてすでにDjangoにある機能を複製することなく可能な機能の範囲をうまく示しています。

私たちは AbsoluteValue 変換器を書くことから始めましょう。これはSQL関数 ABS() を使って比較の前に値を変換します:

from django.db.models import Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

次に、IntegerField に登録しましょう:

from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)

これまでの処理で、クエリを使うことができるようになりました。 Experiment.objects.filter(change__abs=27) は次のSQLを生成します。

SELECT ... WHERE ABS("experiments"."change") = 27

By using Transform instead of Lookup it means we are able to chain further lookups afterward. So Experiment.objects.filter(change__abs__lt=27) will generate the following SQL:

SELECT ... WHERE ABS("experiments"."change") < 27

他にルックアップが指定されていない場合、Djangoは change__abs=27change__abs__exact=27 として解釈します。

これにより``ORDER BY``句と`DISTINCT ON``句の使用も可能です。例えば``Experiment.objects.order_by('change__abs')``は以下のSQLを生成します。

SELECT ... ORDER BY ABS("experiments"."change") ASC

そして、PosgreSQLのようにフィールドをDISTINCTすることもサポートしています。 ``Experiment.objects.distinct('change__abs')``は以下のSQLを生成します。

SELECT ... DISTINCT ON ABS("experiments"."change")

Transform``が適用された後、どのルックアップが許可されるかを探すとき、Djangoは ``output_field 属性を使用します。 ここでは変更しないため、これを指定する必要はありませんでしたが、より複雑な型(たとえば、原点に関連する点や複素数)を表すフィールドに AbsoluteValue を適用すると仮定します。 次に、変換がさらなる検索のために FloatField 型を返すように指定したいかもしれません。 これは、トランスフォームに output_field 属性を追加することで実行できます:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

    @property
    def output_field(self):
        return FloatField()

これは``abs__lte``のようなそれ以降の検索が``FloatField``の場合と同じように振る舞うことを保証します。

効率的な abs__lt 検索を書く

上記の abs ルックアップを使用するとき、生成されたSQLはインデックスを効率的に使用しないことがあります。特に、change__abs__lt=27 を使うとき、これは change__gt=-27change__lt=27 と同じです。( `` lte`` の場合はSQLの BETWEEN を使うことができます)。

そのため、次のようなSQLを生成するためには、Experiment.objects.filter(change__abs__lt=27) を使います。

SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

実装は次のようになります。

from django.db.models import Lookup

class AbsoluteValueLessThan(Lookup):
    lookup_name = 'lt'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = compiler.compile(self.lhs.lhs)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params + lhs_params + rhs_params
        return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params

AbsoluteValue.register_lookup(AbsoluteValueLessThan)

注目すべきことがいくつかあります。 まず、AbsoluteValueLessThanprocess_lhs() を呼び出していません。 代わりに、 AbsoluteValue によって行われた lhs の変換をスキップし、元の lhs を使用します。 つまり、ABS("experiments"."change") ではなく、 "experiments"."change" を取得する必要があります。AbsoluteValueLessThanAbsoluteValue ルックアップからのみアクセスできるため、 self.lhs.lhs を直接参照することは安全です。つまり、 lhs は常に AbsoluteValue のインスタンスです 。

また、クエリで両側が複数回使用されているので、paramsは lhs_paramsrhs_params を複数回含む必要があることにも注意してください。

最後の問い合わせはデータベース内で直接反転(27 から -27)を行います。これを行う理由は、 self.rhs が普通の整数値(例えば F() 参照)以外のものである場合、Pythonでは変換ができないためです。

注釈

実際のところ、 __abs を使った検索は、このように範囲クエリとして実装することができます。そして、ほとんどのデータベースバックエンドでは、インデックスを利用できるようにするほうが賢明です。しかしPostgreSQLでは、 abs(change) にインデックスを追加することをお勧めします。これにより、これらのクエリは非常に効率的になります。

双方向変換の例

前に説明した AbsoluteValue の例は、ルックアップの左側に適用される変換です。 変換を左側と右側の両方に適用したい場合があります。 たとえば、一部のSQL関数に左右されない左側と右側の同等性に基づいてクエリセットをフィルタリングする場合です。

ここで、case-insensitiveな変換を試してみましょう。Djangoには既にcase-insensitiveな検索が多数組み込まれているので、プラクティスとしては良い例ではありません。しかし、データベースにとらわれない双方向変換としては良い例でしょう。

SQL関数 UPPER() を使用して比較前に値を変換する UpperCase トランスフォーマーを定義します。bilateral = True を定義して、この変換が lhs``rhs``の両方に適用されることを示します:

from django.db.models import Transform

class UpperCase(Transform):
    lookup_name = 'upper'
    function = 'UPPER'
    bilateral = True

次に、登録しましょう:

from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)

そして、``Author.objects.filter(name__upper="doe")`は次のようなcase-insensitiveなクエリを生成します。

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

既存の検索向けに代替実装を書く

異なるデータベースベンダーは、同じ操作に対して異なるSQLを必要とする場合があります。 この例では、MySQLのNotEqual演算子のカスタム実装を書き換えます。<> の代わりに != 演算子を使用します。 (実際には、Djangoがサポートするすべての公式データベースを含め、ほとんどすべてのデータベースが両方をサポートしていることに注意してください)。

as_mysql メソッドで NotEqual のサブクラスを作成することで、特定のバックエンドの振る舞いを変えることができます。

class MySQLNotEqual(NotEqual):
    def as_mysql(self, compiler, connection, **extra_context):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s != %s' % (lhs, rhs), params

Field.register_lookup(MySQLNotEqual)

そしてそれを Field に登録することができます。元の NotEqual クラスが同じ lookup_name を持つので代わりに使われます。

クエリをコンパイルするとき、Djangoは最初に as_%s % connection.vendor メソッドを探し、それから as_sql にフォールバックします。組み込みのバックエンドのベンダ名は sqlitepostgresqloracle および mysql です。

Django がルックアップと変換のいずれを使うかを決定する仕組み

場合によっては、修正するのではなく、渡された名前に基づいて、どの Transform または Lookup を返すかを動的に変更したい場合があります。 例として、座標または任意の次元を格納するフィールドがあり、 .filter(coords__x7=4) のような構文で、7番目の座標の値が4であるオブジェクトを返すことができます。 これを行うには、次のようなもので``get_lookup`` をオーバーライドします:

class CoordinatesField(Field):
    def get_lookup(self, lookup_name):
        if lookup_name.startswith('x'):
            try:
                dimension = int(lookup_name[1:])
            except ValueError:
                pass
            else:
                return get_coordinate_lookup(dimension)
        return super().get_lookup(lookup_name)

それから、適切な dimension の値を扱う Lookup サブクラスを返すように get_coordinate_lookup を適切に定義します。

get_transform() と呼ばれる同様の名前のメソッドがあります。get_lookup() は常に Lookup サブクラスを返し、 get_transform() は``Transform`` サブクラスを返す必要があります。 Transform オブジェクトはさらにフィルター処理でき、Lookup オブジェクトはフィルター処理できないことに注意してください。

フィルタリングするときに、解決するルックアップ名が1つだけ残っている場合、Lookup を探します。 複数の名前がある場合、Transform を探します。 名前が1つしかなく、Lookup が見つからない状況では、Transform を探してから、その Transform正確な ルックアップを探します。 すべての呼び出しシーケンスは常に``Lookup`` で終わります。 明確にするためです:

  • .filter(myfield__mylookup)myfield.get_lookup('mylookup') を呼び出します。
  • .filter(myfield__mytransform__mylookup)``myfield.get_transform('mytransform')``を呼び出し、次に ``mytransform.get_lookup('mylookup')``を呼び出します。
  • .filter(myfield__mytransform) は最初に myfield.get_lookup('mytransform') を呼び出しますが、これは失敗するので、myfield.get_transform('mytransform')mytransform.get_lookup('exact') をフォールバックとして呼び出します。