"Menghapus" objek di Django



Cepat atau lambat, pengembang menghadapi tugas menghapus data yang tidak perlu. Dan semakin rumit layanannya, semakin banyak nuansa yang perlu Anda pertimbangkan. Pada artikel ini, saya akan menjelaskan bagaimana kami menerapkan "penghapusan" dalam database dengan ratusan tautan.

Latar belakang


Untuk memantau operasi sebagian besar proyek, Mail.ru Group dan VKontakte menggunakan layanan pengembangan mereka sendiri - Pemantauan . Memulai sejarahnya dari akhir 2012, lebih dari 6 tahun proyek ini telah berkembang menjadi sistem besar, yang telah memperoleh banyak fungsi. Pemantauan secara teratur memeriksa ketersediaan server dan kebenaran tanggapan terhadap permintaan, mengumpulkan statistik pada memori yang digunakan, pemanfaatan CPU, dll. Ketika parameter dari server yang dipantau melebihi nilai yang diizinkan, mereka yang bertanggung jawab untuk server menerima pemberitahuan di sistem dan melalui SMS.

Semua pemeriksaan dan insiden dicatat untuk melacak dinamika kinerja server, sehingga database telah mencapai urutan ratusan juta catatan. Server baru muncul secara berkala, dan yang lama tidak lagi digunakan. Informasi tentang server yang tidak digunakan harus dihapus dari sistem Pemantauan untuk: a) tidak membebani antarmuka dengan informasi yang tidak perlu, dan b) melepaskan pengidentifikasi unik .

Hapus


Saya sadar dalam judul artikel kata "delete" ditulis dalam tanda kutip. Ada beberapa cara untuk menghapus objek dari sistem:

  • sepenuhnya dihapus dari basis data;
  • menandai objek sebagai terhapus dan bersembunyi dari antarmuka. Sebagai penanda, Anda dapat menggunakan Boolean, atau DateTime untuk pencatatan yang lebih akurat.

Iterasi # 1


Awalnya, pendekatan pertama digunakan, ketika kita cukup mengeksekusi object.delete() dan objek itu dihapus dengan semua dependensi. Tetapi seiring berjalannya waktu, kami harus meninggalkan pendekatan ini, karena satu objek dapat memiliki ketergantungan dengan jutaan objek lainnya, dan penghapusan cascading tabel yang diblokir secara kaku. Dan karena layanan melakukan ribuan cek setiap detik dan mencatatnya, memblokir tabel menyebabkan perlambatan serius dalam layanan, yang tidak dapat diterima bagi kami.

Iterasi # 2


Untuk menghindari kunci panjang, kami memutuskan untuk menghapus data dalam batch. Ini akan memungkinkan perekaman data pemantauan aktual dalam interval antara penghapusan objek. Daftar semua objek yang akan dihapus dalam kaskade dapat diperoleh dengan metode yang digunakan di panel admin saat menghapus objek (saat mengkonfirmasi penghapusan):

 from django.contrib.admin.util import NestedObjects from django.db import DEFAULT_DB_ALIAS collector = NestedObjects(using=DEFAULT_DB_ALIAS) collector.collect([obj]) objects_to_delete = collector.nested() # Recursive delete objects 

Situasi membaik: beban didistribusikan seiring waktu, data baru mulai direkam lebih cepat. Tapi kami segera berlari ke perangkap berikutnya. Faktanya adalah bahwa daftar objek yang akan dihapus dibentuk pada awal penghapusan, dan jika objek dependen baru ditambahkan dalam proses penghapusan "sebagian", elemen induk tidak dapat dihapus.

Kami segera meninggalkan ide kesalahan dalam penghapusan rekursif untuk mengumpulkan kembali data pada dependensi baru atau melarang menambahkan catatan dependen saat menghapus, karena a) Anda dapat masuk ke loop tak terbatas atau b) Anda harus menemukan semua objek dependen dalam seluruh kode .

Iterasi # 3


Kami memikirkan penghapusan tipe kedua, saat data ditandai dan disembunyikan dari antarmuka. Awalnya, pendekatan ini ditolak, karena sepertinya tugas setidaknya selama seminggu untuk menemukan semua pertanyaan dan menambahkan filter karena tidak adanya orang tua yang dihapus. Selain itu, ada kemungkinan besar kehilangan kode yang diperlukan, yang akan menyebabkan konsekuensi yang tidak terduga.

Kemudian kami memutuskan untuk menggunakan dekorator untuk mengganti manajer kueri. Lebih jauh lebih baik melihat kode daripada menulis seratus kata.

 def exclude_objects_for_deleted_hosts(*fields): """ Decorator that adds .exclude({field__}is_deleted=True) for model_class.objects.get_queryset :param fields: fields for exclude condition """ def wrapper(model_class): def apply_filters(qs): for field in filter_fields: qs = qs.exclude(**{ '{}is_deleted'.format('{}__'.format(field) if field else ''): True, }) return qs model_class.all_objects = copy.deepcopy(model_class.objects) filter_fields = set(fields) get_queryset = model_class.objects.get_queryset model_class.objects.get_queryset = lambda: apply_filters(get_queryset()) # save info about model decorator setattr(model_class, DECORATOR_DEL_HOST_ATTRIBUTE, filter_fields) return model_class return wrapper 

exclude_objects_for_deleted_hosts(fields) untuk bidang yang ditentukan dari model bidang secara otomatis menambahkan filter exclude untuk setiap permintaan, yang hanya menghilangkan entri yang tidak boleh ditampilkan di antarmuka.

Sekarang sudah cukup bagi semua model yang akan terkena dampak karena beberapa cara untuk menambahkan dekorator:

 @exclude_objects_for_deleted_hosts('host') class Alias(models.Model): host = models.ForeignKey(to=Host, verbose_name='Host', related_name='alias') 

Sekarang, untuk menghapus objek Host , cukup ubah atribut is_deleted :

 host.is_deleted = True # after this save the host and all related objects will be inaccessible host.save() 

Semua kueri akan secara otomatis mengecualikan catatan yang merujuk objek jarak jauh:

 # model decorator @exclude_objects_for_deleted_hosts('checker__monhost', 'alias__host') CheckerToAlias.objects.filter( alias__hostname__in=['cloud.spb.s', 'cloud.msk.s'] ).values('id') 

Ternyata permintaan SQL berikut:

 SELECT monitoring_checkertoalias.id FROM monitoring_checkertoalias INNER JOIN monitoring_checker ON (`monitoring_checkertoalias`.`checker_id` = monitoring_checker.`id`) INNER JOIN Hosts ON (`monitoring_checker`.`monhost_id` = Hosts.`id`) INNER JOIN dcmap_alias ON (`monitoring_checkertoalias`.`alias_id` = dcmap_alias.`id`) INNER JOIN Hosts T5 ON (`dcmap_alias`.`host_id` = T5.`id`) WHERE ( NOT (`Hosts`.`is_deleted` = TRUE) -- ,   monitoring_checker AND NOT (T5.`is_deleted` = TRUE) -- ,   dcmap_alias AND dcmap_alias.name IN ('dir1.server.p', 'dir2.server.p') ); 

Seperti yang Anda lihat, `is_deleted` = TRUE tambahan untuk bidang yang ditentukan dalam dekorator dan pemeriksaan untuk `is_deleted` = TRUE ditambahkan ke permintaan.

Sedikit tentang angka


Adalah logis bahwa bergabung dan kondisi tambahan meningkatkan waktu eksekusi permintaan. Studi tentang masalah ini menunjukkan bahwa tingkat "komplikasi" tergantung pada struktur database, jumlah catatan, dan keberadaan indeks.

Secara khusus, dalam kasus kami, untuk setiap tingkat ketergantungan permintaan didenda sekitar 30%. Ini adalah penalti maksimum yang kami dapatkan di meja terbesar dengan jutaan catatan, pada tabel yang lebih kecil, penalti dikurangi menjadi beberapa persen. Untungnya, kami memiliki indeks yang perlu dikonfigurasi, dan untuk sebagian besar pertanyaan kritis, gabungan yang diperlukan sudah ada di sana, jadi kami tidak merasakan perbedaan besar dalam kinerja.

Pengidentifikasi unik


Sebelum menghapus data, mungkin perlu untuk membebaskan pengidentifikasi yang direncanakan untuk digunakan di masa depan, karena ini dapat menimbulkan kesalahan non-unik saat membuat objek baru. Terlepas dari kenyataan bahwa tidak ada objek yang akan terlihat di aplikasi Django, mereka masih akan ada di database. Oleh karena itu, untuk objek yang dihapus, kami menambahkan uuid ke pengidentifikasi.

 host.hostname = '{}_{}'.format(host.hostname, uuid.uuid4()) host.is_deleted = True host.save() 

Operasi


Untuk setiap model atau ketergantungan baru, dekorator perlu diperbarui jika perlu. Untuk menyederhanakan pencarian model dependen, kami menulis tes "pintar":

 def test_deleted_host_decorator_for_models(self): def recursive_host_finder(model, cache, path, filters): # cache for skipping looked models cache.add(model) # process all related models for field in (f for f in model._meta.fields if isinstance(f, ForeignKey)): if field.related_model == Host: filters.add(path + field.name) elif field.related_model not in cache: recursive_host_finder(field.related_model, cache.copy(), path + field.name + '__', filters) # check all models for current_model in apps.get_models(): model_filters = getattr(current_model, DECORATOR_DEL_HOST_ATTRIBUTE, set()) found_filters = set() if current_model == Host: found_filters.add('') else: recursive_host_finder(current_model, set(), '', found_filters) if found_filters or model_filters: try: self.assertSetEqual(model_filters, found_filters) except AssertionError as err: err.args = ( '{}\n !!! Fix decorator "exclude_objects_for_deleted_hosts" ' 'for model {}'.format(err.args[0], current_model), ) raise err 

Tes secara rekursif memeriksa semua model apakah ada ketergantungan pada model yang akan dihapus, kemudian terlihat untuk melihat apakah dekorator untuk bidang yang diperlukan telah ditetapkan untuk model ini. Jika ada sesuatu yang hilang, tes akan dengan lembut memberi tahu Anda di mana harus menambahkan dekorator.

Epilog


Dengan demikian, dengan bantuan dekorator, dimungkinkan untuk menerapkan "penghapusan kecil" data yang memiliki banyak dependensi. Semua permintaan secara otomatis menerima filter exclude diperlukan. Pengenaan kondisi tambahan memperlambat proses memperoleh data, tingkat "komplikasi" tergantung pada struktur database, jumlah catatan, dan ketersediaan indeks. Tes yang diusulkan akan memberi tahu Anda model mana yang perlu Anda tambahkan dekorator, dan di masa depan akan memantau konsistensi mereka.

Source: https://habr.com/ru/post/id438280/


All Articles