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 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 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 perilaku pada backend khusus dengan membuat subkelas dari NotEqual dengan sebuah metode 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().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 memanggil myfield.get_lookup('mylookup').
  • .filter(myfield__mytransform__mylookup) akan memanggil myfield.get_transform('mytransform'), dan lalu mytransform.get_lookup('mylookup').
  • .filter(myfield__mytransform) akan memanggil pertama myfield.get_lookup('mytransform'), yang akan gagal, sehingga dia akan gagal kembali memanggil myfield.get_transform('mytransform') dan kemudian mytransform.get_lookup('exact').
Back to Top