Bagaimana menulis pencarian disesuaikan

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 Acuan Pencarian API.

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 import Field

Field.register_lookup(NotEqual)

Pendaftaran pencarian dapat juga dikerjakan menggunakan pola decorator:

from django.db.models 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 transformator

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 instead of Lookup itu berarti kami dapat rantai pencarian lebih lanjut sesudahnya. Sehingga``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.

Mari kita uji perubahan peka huruf besar-kecil disini. Perubahan ini tidak berguna dalam praktiknya ketika Django datang dengan sekelompok pencarian peka huruf besar-kecil siap pakai, tetapi itu akan menjadi pertunjukkan bagus dari perubahan dua belah pihak dalam 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, 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.removeprefix("x"))
            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