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.
Sebuah contoh pencarian sederhana¶
Mari kita mulai dengan sebuah penyesuaian pencarian sederhana. Kami akan menulis penyesuaian pencarian ne
yang bekerja berlawawan ke exact
. Author.objects.filter(name__ne='Jack')
akan dirubah ke SQL:
"author"."name" <> 'Jack'
Backend SQL berdisi sendiri, sehingga kita tidak perlu khawatir tentang basisdata berbeda.
Terdapat dua langkah untuk membuat ini bekerja. Pertama kami butuh menerapkan pencarian, kemudian kami butuh mengatakan Django tentang ini. Penerapannya sangat mudah:
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 mendaftar pencarian NotEqual
kami akan butuh memanggil register_lookup
pada kelas bidang kami ingin pencarian menjadi tersedia. Dalam kasus ini, pencarian dapat dimengerti pada semua subkelas Field
, jadi kami mendaftarkannya dengan Field
secara langsung:
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.
Contoh perubahan sederhana¶
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 pengubah 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 sekarang menjalankan permintaan kami punyai sebelumnya. Experiment.objects.filter(change__abs=27)
akan membangkitkan SQL berikut:
SELECT ... WHERE ABS("experiments"."change") = 27
Dengan menggunakan Transform
dari pada Lookup
itu berarti kami dapat merangkai pencarian lebih lanjut setelahnya. 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
.
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 akan Experiment.objects.filter(change__abs__lt=27)
untuk 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.
Mari kita ujikan contoh sederhana dari perubahan kasus tidak peka disini. Perubahan ini tidak sangat berguna dalam praktiknya ketika Django sudah datang dengan seikat pencarian siap pakai tidak peka, tetapi dia akan menjadi pertunjukan bagus dari perubahan timbal balik dalam sebuah cara basisdata-agnostik.
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, kumpulan permintaan Author.objects.filter(name__upper="doe")
akan membangkitkan sebuah permintaan kasus 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 kebiasaan pada backend spesifik dengan membuat subkelas dari NotEqual
dengan sebuah cara as_mysql
:
class MySQLNotEqual(NotEqual):
def as_mysql(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
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(CoordinatesField, self).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')
.