Perutean di Django dari versi kedua kerangka menerima alat yang luar biasa - konverter. Dengan tambahan alat ini, menjadi mungkin tidak hanya untuk secara fleksibel mengkonfigurasi parameter dalam rute, tetapi juga untuk memisahkan area tanggung jawab komponen.
Nama saya Alexander Ivanov, saya adalah mentor di Yandex.Practicum di fakultas pengembangan back -end dan pengembang utama di Laboratorium Model Komputer. Dalam artikel ini, saya akan memandu Anda melalui pengonversi rute Django dan menunjukkan kepada Anda keuntungan menggunakan mereka. Hal pertama yang harus dimulai adalah batasan penerapan:
- Django versi 2.0+;
- pendaftaran rute harus dilakukan dengan
django.urls.path
.
Jadi, ketika sebuah permintaan tiba di server Django, pertama kali melewati rantai middleware, dan kemudian URLResolver ( algoritma ) dihidupkan . Tugas yang terakhir adalah menemukan rute yang cocok dalam daftar rute terdaftar.
Untuk analisis substantif, saya mengusulkan untuk mempertimbangkan situasi berikut: ada beberapa titik akhir yang harus menghasilkan laporan berbeda untuk tanggal tertentu. Mari kita asumsikan titik akhir terlihat seperti ini:
users/21/reports/2021-01-31/
teams/4/reports/2021-01-31/
Rute apa yang akan digunakan
urls.py
? Misalnya seperti ini:
path('users/<id>/reports/<date>/', user_report, name='user_report'),
path('teams/<id>/reports/<date>/', team_report, name='team_report'),
Setiap item di
< >
adalah parameter permintaan dan akan diteruskan ke handler.
Penting: nama parameter saat mendaftarkan rute dan nama parameter di penangan harus cocok.
Kemudian setiap penangan akan memiliki sesuatu seperti ini (perhatikan penjelasan jenis):
def user_report(request, id: str, date: str):
try:
id = int(id)
date = datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise Http404()
# ...
Tetapi ini bukan bisnis kerajaan - untuk menyalin-menempelkan blok kode seperti itu untuk setiap penangan. Masuk akal untuk memindahkan kode ini ke fungsi tambahan:
def validate_params(id: str, date: str) -> (int, datetime):
try:
id = int(id)
date = datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise Http404('Not found')
return id, date
Dan di setiap handler maka akan ada panggilan sederhana ke fungsi helper ini:
def user_report(request, id: str, date: str):
id, date = validate_params(id, date)
# ...
Secara umum, ini sudah bisa dicerna. Fungsi helper akan mengembalikan parameter yang benar dari tipe yang dibutuhkan, atau membatalkan handler. Segalanya tampak baik-baik saja.
Tetapi kenyataannya, inilah yang saya lakukan: Saya mengalihkan sebagian tanggung jawab untuk memutuskan apakah penangan ini harus berjalan untuk rute ini atau tidak, dari URLResolver ke penangan itu sendiri. Ternyata URLResolver melakukan tugasnya dengan buruk, dan penangan saya tidak hanya harus melakukan pekerjaan yang berguna, tetapi juga memutuskan apakah mereka harus melakukannya sama sekali. Ini jelas merupakan pelanggaran dari SOLID prinsip tanggung jawab . Ini tidak akan berhasil. Kami perlu meningkatkan.
Konverter standar
Django menyediakan pengonversi rute standar . Ini adalah mekanisme untuk menentukan apakah sebagian dari rute tersebut sesuai atau tidak oleh URLResolver itu sendiri. Bonus yang bagus: konverter dapat mengubah tipe parameter, yang berarti tipe yang kita butuhkan bisa langsung datang ke handler, dan bukan stringnya.
Pengonversi ditentukan sebelum nama parameter dalam rute, dipisahkan dengan titik dua. Faktanya, semua parameter memiliki konverter, jika tidak ditentukan secara eksplisit, maka konverter digunakan secara default
str
.
Hati-hati: beberapa konverter terlihat seperti tipe dalam Python, jadi mungkin terlihat seperti cast normal, tetapi sebenarnya tidak - misalnya, tidak ada konverter standarfloat
ataubool
. Nanti saya akan tunjukkan apa itu converter.
Setelah melihat konverter standar, menjadi jelas untuk apa
id
menggunakan konverter
int
:
path('users/<int:id>/reports/<date>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date>/', team_report, name='team_report'),
Tapi bagaimana dengan tanggalnya? Tidak ada konverter standar untuk itu.
Anda tentu saja dapat menghindari dan melakukan ini:
'users/<int:id>/reports/<int:year>-<int:month>-<int:day>/'
Memang, beberapa masalah telah dieliminasi, karena sekarang ada jaminan bahwa tanggal akan ditampilkan dalam tiga angka yang dipisahkan tanda hubung. Namun, Anda masih harus menangani kasus masalah di handler jika klien mengirimkan tanggal yang salah, misalnya 2021-02-29 atau 100-100-100 secara umum. Artinya, opsi ini tidak sesuai.
Kami membuat konverter kami sendiri
Django, sebagai tambahan untuk pengonversi standar, menyediakan kemampuan untuk membuat pengubah Anda sendiri dan menjelaskan aturan konversi sesuka Anda.
Untuk melakukan ini, Anda perlu mengambil dua langkah:
- Jelaskan kelas konverter.
- Daftarkan konverternya.
Kelas konverter adalah kelas dengan sekumpulan atribut dan metode tertentu yang dijelaskan dalam dokumentasi (menurut pendapat saya, agak aneh bahwa pengembang tidak membuat kelas abstrak dasar). Persyaratannya sendiri:
- Harus ada atribut yang
regex
menjelaskan ekspresi reguler untuk segera menemukan urutan yang diperlukan. Saya akan menunjukkan cara penggunaannya nanti. - Menerapkan metode
def to_python(self, value: str)
untuk mengonversi dari string (bagaimanapun, rute yang ditransmisikan selalu berupa string) menjadi objek python, yang akhirnya akan diteruskan ke penangan. - Menerapkan metode
def to_url(self, value) -> str
untuk mengonversi kembali dari objek python menjadi string (digunakan saat memanggildjango.urls.reverse
atau memberi tagurl
).
Kelas untuk mengubah tanggal akan terlihat seperti ini:
class DateConverter:
regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
def to_python(self, value: str) -> datetime:
return datetime.strptime(value, '%Y-%m-%d')
def to_url(self, value: datetime) -> str:
return value.strftime('%Y-%m-%d')
Saya menentang duplikasi, jadi saya akan meletakkan format tanggal di atribut - lebih mudah untuk mempertahankan konverter jika saya tiba-tiba ingin (atau perlu) mengubah format tanggal:
class DateConverter:
regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
format = '%Y-%m-%d'
def to_python(self, value: str) -> datetime:
return datetime.strptime(value, self.format)
def to_url(self, value: datetime) -> str:
return value.strftime(self.format)
Kelas dijelaskan, jadi sekarang saatnya mendaftarkannya sebagai konverter. Ini dilakukan dengan sangat sederhana: dalam fungsi
register_converter
Anda perlu menentukan kelas yang dijelaskan dan nama konverter untuk menggunakannya dalam rute:
from django.urls import register_converter
register_converter(DateConverter, 'date')
Sekarang Anda dapat mendeskripsikan rute dalam
urls.py
(Saya sengaja mengubah nama parameter
dt
agar tidak membingungkan entri
date:date
):
path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date:dt>/', team_report, name='team_report'),
Sekarang dijamin bahwa penangan akan dipanggil hanya jika konverter bekerja dengan benar, yang berarti bahwa parameter dari tipe yang diperlukan akan datang ke penangan:
def user_report(request, id: int, dt: datetime):
#
#
Tampak menakjubkan! Dan begitulah, Anda bisa memeriksanya.
Dibawah tenda
Jika Anda melihat lebih dekat, sebuah pertanyaan menarik muncul: tidak ada yang bisa memastikan bahwa tanggalnya benar. Iya ada regular season, tapi salah tanggal juga cocok untuk itu, misal 2021-01-77 yang artinya
to_python
pasti ada error di dalamnya. Mengapa ini berhasil?
Tentang ini saya katakan: "Mainkan dengan aturan kerangka kerja, dan itu akan bermain untuk Anda." Kerangka kerja mengambil sejumlah tugas umum. Jika kerangka tidak dapat melakukan sesuatu, maka kerangka kerja yang baik memberikan kesempatan untuk memperluas fungsinya. Karena itu, Anda tidak boleh terlibat dalam pembuatan sepeda, lebih baik melihat bagaimana kerangka kerja menawarkan untuk meningkatkan kemampuannya sendiri.
Django mempunyai subsistem perutean dengan kemampuan untuk menambahkan pengonversi yang menangani pemanggilan metode
to_python
dan menangkap kesalahan
ValueError
.
Berikut adalah kode dari subsistem perutean Django tanpa perubahan (versi 3.1, berkas
django/urls/resolvers.py
, kelas
RoutePattern
, metode
match
):
match = self.regex.search(path)
if match:
# RoutePattern doesn't allow non-named groups so args are ignored.
kwargs = match.groupdict()
for key, value in kwargs.items():
converter = self.converters[key]
try:
kwargs[key] = converter.to_python(value)
except ValueError:
return None
return path[match.end():], (), kwargs
return None
Langkah pertama adalah mencari kecocokan dalam rute yang dikirimkan dari klien menggunakan ekspresi reguler. Yang
regex
didefinisikan di kelas konverter berpartisipasi dalam formasi
self.regex
, yaitu, itu diganti alih-alih ekspresi dalam tanda kurung siku
<>
dalam rute.
Sebagai contoh,
berubah menjadiusers/<int:id>/reports/<date:dt>/
^users/(?P<id>[0-9]+)/reports/(?P<dt>[0-9]{4}-[0-9]{2}-[0-9]{2})/$
Pada akhirnya, sama biasa dari
DateConverter
.
Ini adalah pencarian cepat, dangkal. Jika tidak ditemukan rute yang cocok, maka rute tersebut sudah pasti tidak sesuai, tetapi jika ditemukan, maka itu merupakan rute yang berpotensi sesuai. Artinya, Anda perlu memulai tahap verifikasi berikutnya.
Setiap parameter memiliki konverternya sendiri, yang digunakan untuk memanggil metode
to_python
. Dan inilah hal yang paling menarik: panggilan
to_python
dibungkus
try/except
, dan kesalahan tipe tertangkap
ValueError
. Itulah mengapa konverter berfungsi bahkan dalam kasus tanggal yang salah: kesalahan jatuh
ValueError
, dan ini dianggap sehingga rute tidak sesuai.
Jadi dalam kasus
DateConverter
, kita dapat katakan, beruntung: dalam kasus tanggal yang salah, kesalahan jenis yang diperlukan jatuh. Jika ada kesalahan jenis lain, maka Django akan mengembalikan tanggapan 500.
Jangan berhenti
Tampaknya semuanya baik-baik saja, konverter berfungsi, jenis yang diperlukan segera datang ke penangan ... Atau tidak segera?
path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
Dalam penangan untuk membuat laporan, Anda mungkin membutuhkannya
User
, dan bukan
id
(meskipun ini mungkin kasusnya). Dalam situasi hipotetis saya, hanya satu objek yang dibutuhkan untuk membuat laporan
User
. Lalu apa yang ternyata, lagi-lagi dua puluh lima?
def user_report(request, id: int, dt: datetime):
user = get_object_or_404(User, id=id)
# ...
Mengalihkan tanggung jawab kepada pawang lagi.
Tetapi sekarang jelas apa yang harus dilakukan dengannya: tulis konverter Anda sendiri! Ini akan memastikan objek tersebut ada
User
dan akan meneruskannya ke pawang.
class UserConverter:
regex = r'[0-9]+'
def to_python(self, value: str) -> User:
try:
return User.objects.get(id=value)
except User.DoesNotExist:
raise ValueError('not exists') # ValueError
def to_url(self, value: User) -> str:
return str(value.id)
Setelah mendeskripsikan kelas, saya mendaftarkannya:
register_converter(UserConverter, 'user')
Akhirnya, saya jelaskan rutenya:
path('users/<user:u>/reports/<date:dt>/', user_report, name='user_report'),
Itu lebih baik:
def user_report(request, u: User, dt: datetime):
# ...
Pengonversi untuk model dapat sering digunakan, jadi akan lebih mudah untuk membuat kelas dasar dari konverter semacam itu (pada saat yang sama, saya menambahkan tanda centang untuk keberadaan semua atribut):
class ModelConverter:
regex: str = None
queryset: QuerySet = None
model_field: str = None
def __init__(self):
if None in (self.regex, self.queryset, self.model_field):
raise AttributeError('ModelConverter attributes are not set')
def to_python(self, value: str) -> models.Model:
try:
return self.queryset.get(**{self.model_field: value})
except ObjectDoesNotExist:
raise ValueError('not exists')
def to_url(self, value) -> str:
return str(getattr(value, self.model_field))
Kemudian deskripsi konverter baru ke model akan direduksi menjadi deskripsi deklaratif:
class UserConverter(ModelConverter):
regex = r'[0-9]+'
queryset = User.objects.all()
model_field = 'id'
Hasil
Pengonversi rute adalah mekanisme hebat yang membantu Anda membuat kode lebih bersih. Tetapi mekanisme ini muncul hanya dalam versi kedua Django - sebelumnya kami harus melakukannya tanpanya. Dari sinilah fungsi tambahan dari tipe tersebut
get_object_or_404
berasal; tanpa mekanisme ini, perpustakaan keren seperti DRF dibuat.
Tetapi ini tidak berarti bahwa konverter tidak boleh digunakan sama sekali. Ini berarti bahwa (belum) tidak mungkin untuk menggunakannya di mana-mana. Tetapi jika memungkinkan, saya mendorong Anda untuk tidak mengabaikannya.
Saya akan meninggalkan satu peringatan: di sini penting untuk tidak berlebihan dan tidak menyeret selimut ke arah lain - Anda tidak perlu memasukkan logika bisnis ke dalam konverter. Penting untuk menjawab pertanyaan: jika rute seperti itu pada prinsipnya tidak mungkin, maka ini adalah area tanggung jawab konverter; Jika rute seperti itu memungkinkan, tetapi dalam keadaan tertentu tidak diproses, maka ini sudah menjadi tanggung jawab pawang, pembuat serial, atau orang lain, tetapi yang pasti bukan konverternya.
PS Dalam praktiknya, saya hanya membuat dan menggunakan konverter untuk tanggal, hanya yang ditunjukkan di artikel, karena saya hampir selalu menggunakan DRF atau GraphQL. Beri tahu kami jika Anda menggunakan konverter rute dan, jika ya, yang mana?