カスタムのルックアップを書く

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

ルックアップの例

簡単なカスタムルックアップから始めましょう。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 (右辺) を表します。左辺は通常フィールド参照ですが、クエリ式 API を実装するものであれば何でもかまいません。右辺はユーザーから与えられた値です。Author.objects.filter(name__ne='Jack') の例では、左辺は Author モデルの name フィールドへの参照で、右辺は 'Jack' です。

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

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

トランスフォーム(Transform: 変換)の例

上記のカスタムルックアップは素晴らしいですが、場合によってはルックアップを一緒に連結したいこともあります。たとえば、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

LookUp の代わりに Transform を使うことで、それ以降のルックアップを連結できます。つまり、Experiment.objects.filter(change__abs__lt=27) は以下のような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=-27 かつ change__lt=27 と同じです (lte の場合はSQLの BETWEEN を使うことができます)。

そのため、Experiment.objects.filter(change__abs__lt=27) に次のような SQL を生成させたいです。

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関数に左右されない左側と右側の同等性に基づいてクエリセットをフィルタリングする場合です。

ここで、大文字小文字を区別しないトランスフォームを試してみましょう。Djangoには既に大文字小文字を区別しないルックアップが多数組み込まれているので、プラクティスとしては良い例ではありません。しかし、データベースにとらわれない双方向のトランスフォームとしては良い例でしょう。

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

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") は次のような大文字小文字を区別しないクエリを生成するようになります。

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.removeprefix("x"))
            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 に対して exact ルックアップを行います。すべての呼び出しシーケンスは常に 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') をフォールバックとして呼び出すこと。
Back to Top