カスタムのルックアップを書く¶
Django には (たとえば exact
や icontains
などの) フィルタリングを行う 組み込みのルックアップ がたくさんあります。このドキュメントでは、カスタムルックアップを作る方法や、既存のルックアップの動作を変更する方法について説明します。ルックアップの 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つの値に対して動作するルックアップ lhs
と rhs
は、それぞれ left-hand side (左辺) と right-hand side (右辺) を表します。左辺は通常フィールド参照ですが、クエリ式 API を実装するものであれば何でもかまいません。右辺はユーザーから与えられた値です。Author.objects.filter(name__ne='Jack')
の例では、左辺は Author
モデルの name
フィールドへの参照で、右辺は 'Jack'
です。
process_lhs
と process_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=27
を change__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)
注目すべきことがいくつかあります。 まず、AbsoluteValueLessThan
は process_lhs()
を呼び出していません。 代わりに、 AbsoluteValue
によって行われた lhs
の変換をスキップし、元の lhs
を使用します。 つまり、ABS("experiments"."change")
ではなく、 "experiments"."change"
を取得する必要があります。AbsoluteValueLessThan
は AbsoluteValue
ルックアップからのみアクセスできるため、 self.lhs.lhs
を直接参照することは安全です。つまり、 lhs
は常に AbsoluteValue
のインスタンスです 。
また、クエリで両側が複数回使用されているので、paramsは lhs_params
と rhs_params
を複数回含む必要があることにも注意してください。
最後の問い合わせはデータベース内で直接反転(27
から -27
)を行います。これを行う理由は、 self.rhs
が普通の整数値(例えば F()
参照)以外のものである場合、Pythonでは変換ができないためです。
注釈
実際のところ、 __abs
を使ったルックアップは、このように範囲クエリとして実装することができます。そして、ほとんどのデータベースバックエンドでは、インデックスを利用できるようにするほうが賢明です。しかしPostgreSQLでは、 abs(change)
にインデックスを追加することをお勧めします。これにより、これらのクエリは非常に効率的になります。
双方向のトランスフォームの例¶
前に説明した AbsoluteValue
の例は、ルックアップの左側に適用される変換です。 変換を左側と右側の両方に適用したい場合があります。 たとえば、一部のSQL関数に左右されない左側と右側の同等性に基づいてクエリセットをフィルタリングする場合です。
ここで、大文字小文字を区別しないトランスフォームを試してみましょう。Djangoには既に大文字小文字を区別しないルックアップが多数組み込まれているので、プラクティスとしては良い例ではありません。しかし、データベースにとらわれない双方向のトランスフォームとしては良い例でしょう。
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")
は次のような大文字小文字を区別しないクエリを生成するようになります。
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
にフォールバックします。組み込みのバックエンドのベンダ名は sqlite
、postgresql
、oracle
および 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')
をフォールバックとして呼び出すこと。