Django には (たとえば exact
や icontains
などの) フィルタリングを行う 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つの値に対して動作するルックアップ、 lhs
と rhs
は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=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
を使うことができます)。
そのため、次のような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)
注目すべきことがいくつかあります。 まず、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関数に左右されない左側と右側の同等性に基づいてクエリセットをフィルタリングする場合です。
ここで、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
にフォールバックします。組み込みのバックエンドのベンダ名は sqlite
、postgresql
、oracle
および mysql
です。
場合によっては、修正するのではなく、渡された名前に基づいて、どの 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')
をフォールバックとして呼び出します。2022年6月01日