Penyesuaian Pencarian¶
Django menawarkan beragam luas dari built-in lookups untuk penyaringan (sebagai contoh, exact
dan icontains
). Dokumentasi ini menjelaskan bagaimana menulis penyesuaian pencarian dan bagaimana mengubah pekerjaan dari pencarian yang ada. Untuk acuan API dari pencarian, lihat Lookup API reference.
Contoh pencarian¶
Mari kita mulai dengan pencarian penyesuaian kecil. Kami akan menulis pencarian penyesuaian ne
yang bekerj aberlawanan terhadap exact
. Author.objects.filter(name__ne='Jack')
akan menterjemahkan ke SQL:
"author"."name" <> 'Jack'
Backend SQL berdisi sendiri, sehingga kita tidak perlu khawatir tentang basisdata berbeda.
Ada dua langkah untuk membuatnya bekerja. Pertama kami butuh menerapkan pencarian, kemudian kami butuh memberitahu Django mengenai itu:
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
Untuk mendaftarkan pencarian NotEqual
kami butuh memanggil register_lookup
pada bidang kelas kami ingin cari agar tersedia. Dalam kasus ini, pencarian masuk akal pada semua subkelas Field
, sehingga kami mendaftarkan itu langsung dengan Field
:
from django.db.models.fields import Field
Field.register_lookup(NotEqual)
Pendaftaran pencarian dapat juga dikerjakan menggunakan pola decorator:
from django.db.models.fields import Field
@Field.register_lookup
class NotEqualLookup(Lookup):
# ...
Kami sekarang dapat menggunakan foo__ne
untuk setiap bidang foo
. Anda akan butuh memastikan bahwa pendaftaran ini terjadi sebelum anda mencoba membuat kumpulan permintaan menggunakannya. Anda dapat menempatkan penerapan dalam sebuah berkas models.py
, atau mendaftarkan pencarian dalam cara ready()
dari sebuah AppConfig
.
Melihat lebih dekat pada penerapan, atribut dibutuhkan pertama adalah lookup_name
. Ini mengizinkan ORM untuk memahami bagaimana mengartikan name__ne
dan menggunakan NotEqual
untuk membangkitkan SQL. Berdasarkan pemufakatan, nama-nama ini selalu deretan karakter huruf kecil mengandung hanya huruf, tetapi persyaratan mutlak adalah bahwa itu harus mengandung deretan karakter __
.
Kami lalu butuh menentukan cara as_sql
. Ini mengambil obyek SQLCompiler
, disebut compiler
, dan hubungan aktif basisdata. Obyek SQLCompiler
tidak didokumentasikan, tetapi satu-satunya kami butuh diketahui tentang mereka adalah bahwa mereka mempunyai cara compile()
yang mengemblikan sebuah tuple mengandung deretan karakter SQL, dan parameter untuk disisipkan kedalam deretan karakter itu. Di kebanyakan kasus, anda tidak butuh menggunakannya secara langsung dan dapat melewatinya ke process_lhs()
dan process_rhs()
.
Sebuah Lookup
bekerja terhadap dua nilai, lhs
dan rhs
, kepanjangan dari left-hand side dan right-hand side. Left-hand side biasanya acuan bidang, tetapi dia dapat menjadi apapun menerapkan query expression API. Right-hand adalah nilai diberikan oleh pengguna. Dalam contoh Author.objects.filter(name__ne='Jack')
, sisi tangan-kanan adalah sebuah acuan pada bidang name
dari model Author
, dan 'Jack'
adalah the right-hand side.
Kami memanggil process_lhs
dan process_rhs
untuk merubah mereka kedalam nilai-nilai kami butuh untuk SQL menggunakan obyek compiler
digambarkan sebelumnya. Cara ini mengembalikan tuple mengandung beberapa SQL dan parameter untuk ditambahkan kedaam SQL itu, seperti yang kita perlu untuk mengembalikan cara as_sql
kami. Dalam contoh diatas, process_lhs
mengembalikan ('"author"."name"', [])
dan process_rhs
mengembalikan ('"%s"', ['Jack'])
. Dalam contoh ini tidak ada parameter untuk left hand side, tetapi ini akan tergantung pada obyek kita punya, jadi jami masih butuh menyertakan merekan dalam parameter kami kembalikan.
Akhirnya kami menggabungkan bagian-bagian kedalam sebuah pernyataan SQL dengan <>
, dan memasok semua parameter untuk permintaan. Kami lalu mengambalikan sebuah tuple mengandung deretan karakter SQL dan parameter yang dibangkitkan.
A transformer example¶
Penyesuaian pencarian diatas adalah hebat, tetapi dalam beberapa kasus anda mungkin ingin dapat merangkai pencarian bersama-sama. Sebagai contoh, mari kita misalnya kami sedang membangun sebuah aplikasi dimana kami ingin membuat penggunaan dari operator abs()
. Kami mempunyai sebuah model Experiment
yang merekam sebuah nilai awal, nilai akhir, dan perubahan (awal - akhir). Kami akan suka menemukan semua percobaan dimana perubahan setara pada bilangan tertentu (Experiment.objects.filter(change__abs=27)
), atau dimana itu tidak melebihi bilangan tertentu (Experiment.objects.filter(change__abs__lt=27)
).
Catatan
Contoh ini agak dibikin, tetapi dia menunjukkan jangkauan fungsionalitas yang memungkinkan dalam cara backend basisdata berdisi sendiri, tanpa menggandakan fungsionalitas yang sudah ada di Django.
Kami akan mulai dengan menulis sebuah perubahan AbsoluteValue
. Ini akan menggunakan fungsi SQL ABS()
untuk merubah nilai sebelum dibandingkan:
from django.db.models import Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
Selanjutnya, mari kita daftarkan sebagai IntegerField
:
from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)
Kami dapat menjalankan permintaan kami miliki sebelumnya. Experiment.objects.filter(change__abs=27)
akan membangkitkan SQL berikut:
SELECT ... WHERE ABS("experiments"."change") = 27
Dengan menggunakan Transform
daripada Lookup
itu berarti kami dapat mengunci pencarian selanjutnya setelah itu. Jadi Experiment.objects.filter(change__abs__lt=27)
akan membangkitkan SQL berikut:
SELECT ... WHERE ABS("experiments"."change") < 27
Catat bahwa dalam kasus terdapat tidak ada pencarian lain yang ditentukan, Django menterjemahkan change__abs=27
sebagai change__abs__exact=27
.
Ini juga mengizinkan hasil digunakan dalam klausa ORDER BY
dan DISTINCT ON
. Sebagai contoh Experiment.objects.order_by('change__abs')
membangkitkan:
SELECT ... ORDER BY ABS("experiments"."change") ASC
Dan pada basisdata yang mendukung distinct pada bidang (seperti PostgreSQL), Experiment.objects.distinct('change__abs')
membangkitkan:
SELECT ... DISTINCT ON ABS("experiments"."change")
Ketika mencari pencarian mana yang diizinkan setelah Transform
diberlakukan, Django menggunakan atribut output_field
. Kami tidak butuh menentukan ini disini jika itu tidak berubah, tetapi seharusnya kami memberlakukan AbsoluteValue
pada beberapa bidang yang mewakili jenis lebih rumit (sebagai contoh sebuah titik relatif ke yang asli, atau angka rumit) kemudian kami mungkin ingin menentukan bahwa perubahan mengembalikan jenis FloatField
untuk pencarian lebih lanjut. Ini dapat dikerjakan dengan menambahkan sebuah atribut output_field
untuk perubahan:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
@property
def output_field(self):
return FloatField()
Ini memastikan bahwa pencarian lebih lanjut seperti abs__lte
berperilaku seperti mereka lakukan untuk FloatField
.
Menulis sebuah pencarian abs__lt
efisien¶
Ketika menggunakan penulisan diatas pencarian abs
, keluaran SQL tidak menggunakan indeks secara efisien dalam beberapa kasus. Khususnya, ketika kami menggunakan change__abs__lt=27
, ini setara pada change__gt=-27
AND change__lt=27
. (Untuk kasus lte
kami akan menggunakan SQL BETWEEN
).
Jadi kami ingin Experiment.objects.filter(change__abs__lt=27)
membangkitkan SQL berikut:
SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27
Peneprapannya adalah:
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)
Terdapat sepasang hal penting sedang terjadi. Pertama, AbsoluteValueLessThan
tidak memanggil process_lhs()
. Malahan dia melewati perubahan dari lhs
dikerjakan oleh AbsoluteValue
dan menggunakan lhs
asli. Yaitu, kami ingin mendapatkan "experiments"."change"
bukan ABS("experiments"."change")
. Mengacu secara langsung pada self.lhs.lhs
adalah aman AbsoluteValueLessThan
dapat diakses hanya dari pencarian AbsoluteValue
, yaitu lhs
selalu sebuah instance dari AbsoluteValue
.
Perhaikan juga bahwa kedua sisi menggunakan banyak waktu dalam permintaan parameter butuh untuk dikandung lhs_params
dan rhs_params
banyak waktu.
Permintaan akhir melakukan pembalikan (27
ke -27
) secara langsung di basisdata. Alasan untuk melakukan ini adalah bahwa jika self.rhs
sesuatu lain daripada nilai integer polos (sebagai contoh sebuah acuan F()
) kami tidak dapat melakukan perubahan dalam Python.
Catatan
Faktanya, kebanyakan pencarian dengan __abs
dapat diterapkan sebagai jangkauan permintaan seperti ini, dan pada kebanyakan backend basisdata sepertinya lebih bijaksana untuk dilakukan sehingga anda dapat membuat penggunaan indeks. Bagaimanapun dengan PostgreSQL anda mungkin ingin menambahkan indeks pada abs(change)
yang akan mengizinkan permintaan ini menjadi lebih efisien.
Sebuah contoh perubahan timbal balik¶
Contoh AbsoluteValue
kami obrolkan sebelumnya adalah sebuah perubahan yang berlaku pada left-hand side dari pencarian. Mungkin disana beberapa kasus dimana anda ingin perubahan diberlakukan pada kedua left-hand side and the right-hand side. Sebagai contoh, jika anda ingin menyaring kumpulan permintaan berdasarkan pada persamaan left and right-hand side kebal pada beberapa fungsi SQL.
Let's examine case-insensitive transformations here. This transformation isn't very useful in practice as Django already comes with a bunch of built-in case-insensitive lookups, but it will be a nice demonstration of bilateral transformations in a database-agnostic way.
Kami menentukan sebuab perubahan UpperCase
yang menggunakan fungsi SQL UPPER()
untuk merubah nilai sebelum dibandingkan. Kami menentukan bilateral = True
untuk mengindikasikan bahwa perubahan ini harus berlaku pada kedua lhs
and rhs
:
from django.db.models import Transform
class UpperCase(Transform):
lookup_name = 'upper'
function = 'UPPER'
bilateral = True
Selanjutnya, mari kita mendaftarkannya:
from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)
Sekarang, queryset Author.objects.filter(name__upper="doe")
akan membangkitkan permintaan tidak peka seperti ini:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
Menulis penerapan cara lain untuk pencarian yang ada¶
Terkadang penjaja basisdata berbeda membutuhkan SQL berbeda untuk pekerjaan yang sama. Untuk contoh ini kami akan menulis kembali sebuah penyesuaian penerapan untuk MySQL untuk penghubung NotEqual. Dari pada <>
kami akan menggunakan penghubung !=
. (Catat bahwa dalam kenyataan hampir semua basisdata mendukung keduanya, termasuk semua basisdata resmi didukung oleh Django).
Kami dapat merubah perilaku pada backend khusus dengan membuat subkelas dari NotEqual
dengan sebuah metode as_mysql
:
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)
Kami dapat mendaftarkannya dengan Field
. Dia memakan tempat dari kelas NotEqual
asli seperti dia mempunyai lookup_name
sama.
Ketika menyusun sebuah permintaan, Django pertama mencari cara as_%s % connection.vendor
, dan kemudian kembali ke as_sql
. Nama penjaja untuk membangun backend adalah sqlite
, postgresql
, oracle
dan mysql
.
Bagaimana Django menentuka pencarian dan merubah yang sedang digunakan¶
Dalam beberapa kasus anda mungkin berharap untuk secara dinamis merubah Transform
atau Lookup
dikembalikan berdasarkan pada nama dilewatkan, daripada memperbaikinya. Sebagai sebuah contoh, anda dapat mempunyai sebuah bidang yang menyimpan kordinat atau dimensi berubah-ubah dan berharap untuk mengizinkan sebuah sintaksis seperti .filter(coords__x7=4)
untuk mengembalikan obyek dimana kordinat 7 mempunyai nilai 4. Untuk melakukan ini, anda akan menimpa get_lookup
dengan sesuatu seperti:
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)
Anda kemudian akan menentukan get_coordinate_lookup
dengan benar untuk mengembalikan sebuah subkelas Lookup
yang menangani nilai yang terkait dari dimension
.
Ada cara yang dinamai mirip dipanggil get_transform()
. get_lookup()
harus selalu mengembalikan sebuah subkelas Lookup
, dan Lookup
sebuah subkelas Transform
. Itu sangat penting diingat bahwa obyek Transform
dapat lebih jauh disaring, dan obyek Lookup
tidak dapat.
Ketika menyaring, jika hanya ada satu nama pencarian tersisa untuk diselesaikan, kami akan mencari sebuah Lookup
. Jika ada banyak nama, dia akan mencari sebuah Transform
. Dalam keadaan dimana hanya ada satu nama dan sebuah Lookup
tidak ditemukan, kami mencari sebuah Transform
dan kemudian pencarian exact
pada Transform
tersebut. Semua panggilan selalu berurutan diakhiri dengan sebuah Lookup
. Untuk menjelaskan:
.filter(myfield__mylookup)
akan memanggilmyfield.get_lookup('mylookup')
..filter(myfield__mytransform__mylookup)
akan memanggilmyfield.get_transform('mytransform')
, dan lalumytransform.get_lookup('mylookup')
..filter(myfield__mytransform)
akan memanggil pertamamyfield.get_lookup('mytransform')
, yang akan gagal, sehingga dia akan gagal kembali memanggilmyfield.get_transform('mytransform')
dan kemudianmytransform.get_lookup('exact')
.