カスタムルックアップ
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__ne
を任意のフィールドfoo
に使用できます。 これを使用してクエリセットを作成する前に、この登録が行われることを確認する必要があります。 実装をmodels.py
ファイルに配置するか、AppConfig
のready()
メソッドにルックアップを登録できます。
実装を詳しく見てみると、最初に必要な属性はlookup_name
です。 これにより、ORMはname__ne
の解釈方法を理解し、NotEqual
を使用してSQLを生成できます。 慣例により、これらの名前は常に文字のみを含む小文字の文字列ですが、唯一の難しい要件は、文字列__
を含めてはならないことです。
次に、as_sql
メソッドを定義する必要があります。 これには、compiler
と呼ばれるSQLCompiler
オブジェクトと、アクティブなデータベース接続が必要です。 SQLCompiler
オブジェクトは文書化されていませんが、それらについて知っておく必要があるのは、SQL文字列を含むタプルを返すcompile()
メソッドと、それに補間されるパラメーターがあることだけです。ストリング。 ほとんどの場合、直接使用する必要はなく、process_lhs()
およびprocess_rhs()
に渡すことができます。
Lookup
は、lhs
とrhs
の2つの値に対して機能し、左側と右側を表します。 左側は通常フィールド参照ですが、クエリ式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()
演算子を使用するアプリケーションを構築しているとします。 開始値、終了値、および変更(開始-終了)を記録するExperiment
モデルがあります。 変化が特定の量に等しい(Experiment.objects.filter(change__abs=27)
)、または特定の量を超えなかった(Experiment.objects.filter(change__abs__lt=27)
)すべての実験を見つけたいと思います。
ノート
この例はやや工夫されていますが、データベースのバックエンドに依存しない方法で、すでに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')
は以下を生成します。
SELECT ... ORDER BY ABS("experiments"."change") ASC
また、個別のフィールド(PostgreSQLなど)をサポートするデータベースでは、Experiment.objects.distinct('change__abs')
は以下を生成します。
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
AND 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"
を取得する必要があります。 self.lhs.lhs
を直接参照しても安全です。つまり、lhs
は常に [のインスタンスです。 X155X]。
クエリでは両側が複数回使用されるため、パラメータには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を必要とする場合があります。 この例では、NotEqual演算子のMySQLのカスタム実装を書き直します。 <>
の代わりに、!=
演算子を使用します。 (実際には、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
に登録できます。 同じlookup_name
を持っているので、元のNotEqual
クラスの代わりになります。
クエリをコンパイルするとき、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[1:])
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super().get_lookup(lookup_name)
次に、get_coordinate_lookup
を適切に定義して、dimension
の関連する値を処理する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')
の呼び出しにフォールバックします。