Membuat model dengan dua opsional, tetapi satu kunci asing wajib


9

Masalah saya adalah bahwa saya memiliki model yang dapat mengambil salah satu dari dua kunci asing untuk mengatakan model apa itu. Saya ingin mengambil setidaknya satu tetapi tidak keduanya. Dapatkah saya memiliki ini masih menjadi satu model atau haruskah saya membaginya menjadi dua jenis. Ini kodenya:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    @classmethod
    def create(cls, groupid, siteid):
        inspection = cls(GroupID = groupid, SiteID = siteid)
        return inspection

    def __str__(self):
        return str(self.InspectionID)

class InspectionReport(models.Model):
    ReportID = models.AutoField(primary_key=True, unique=True)
    InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
    Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
    Comment = models.CharField(max_length=255, blank=True)
    Signature = models.CharField(max_length=255, blank=True)

Masalahnya adalah Inspectionmodel. Ini harus dikaitkan dengan salah satu grup atau situs, tetapi tidak keduanya. Saat ini dengan pengaturan ini perlu keduanya.

Saya lebih suka tidak harus membagi ini menjadi dua model yang hampir identik GroupInspectiondan SiteInspection, jadi solusi apa pun yang menjadikannya sebagai satu model akan ideal.


Mungkin menggunakan subclass lebih baik di sini. Anda dapat membuat Inspectionkelas, dan kemudian subkelas ke dalam SiteInspectiondan GroupInspectionuntuk bagian-bagian yang tidak umum.
Willem Van Onsem

Mungkin tidak terkait, tetapi unique=Truebagian dalam bidang FK Anda berarti bahwa hanya satu Inspectioncontoh yang bisa ada untuk satu contoh GroupIDatau SiteIDcontoh - TKI, itu adalah hubungan satu ke satu, bukan hubungan satu ke banyak. Benarkah ini yang Anda inginkan?
bruno desthuilliers

"Saat ini dengan pengaturan ini perlu keduanya." => secara teknis, tidak - pada tingkat basis data, Anda dapat mengatur keduanya, salah satu atau tidak sama sekali dari kunci tersebut (dengan peringatan disebutkan di atas). Hanya ketika menggunakan ModelForm (langsung atau melalui admin django) bidang-bidang tersebut akan ditandai seperti yang diperlukan, dan itu karena Anda tidak melewati argumen 'blank = True'.
bruno desthuilliers

@brunodesthuilliers Ya idenya adalah untuk memiliki Inspectiontautan antara Groupatau Sitedan InspectionID, maka saya dapat memiliki beberapa "inspeksi" dalam bentuk InspectionReporthubungan satu itu. Ini dilakukan agar saya bisa lebih mudah mengurutkan berdasarkan Datesemua catatan yang terkait dengan satu Groupatau Site. Harapan itu masuk akal
CalMac

@ Cm0295 Saya khawatir tidak melihat titik tingkat tipuan ini - menempatkan langsung grup / situs FK ke dalam InspectionReport menghasilkan layanan yang sama persis dengan AFAICT - memfilter Laporan Inspeksi Anda dengan kunci yang sesuai (atau cukup ikuti deskriptor terbalik dari Situs atau Group), urutkan berdasarkan tanggal dan Anda selesai.
bruno desthuilliers

Jawaban:


5

Saya menyarankan agar Anda melakukan validasi seperti itu dengan cara Django

dengan menimpa cleanmetode Model Django

class Inspection(models.Model):
    ...

    def clean(self):
        if <<<your condition>>>:
            raise ValidationError({
                    '<<<field_name>>>': _('Reason for validation error...etc'),
                })
        ...
    ...

Namun, perhatikan bahwa seperti Model.full_clean (), metode clean () model tidak dipanggil ketika Anda memanggil metode save () model Anda. perlu dipanggil secara manual untuk memvalidasi data model, atau Anda dapat mengganti metode simpan model agar selalu memanggil metode clean () sebelum memicu Modelmetode save kelas


Solusi lain yang mungkin membantu adalah menggunakan GenericRelations , untuk memberikan bidang polimorfik yang berhubungan dengan lebih dari satu tabel, tetapi dapat menjadi kasus jika tabel / objek ini dapat digunakan secara bergantian dalam desain sistem sejak awal.


2

Seperti disebutkan dalam komentar, alasan bahwa "dengan pengaturan ini membutuhkan keduanya" hanya karena Anda lupa menambahkan blank=Trueke bidang FK Anda, sehingga Anda ModelForm(salah satu yang kustom atau default yang dihasilkan oleh admin) akan membuat bidang formulir diperlukan . Pada tingkat skema db, Anda dapat mengisi keduanya, salah satu atau tidak satupun dari FK tersebut, akan baik-baik saja karena Anda membuat bidang-bidang db tersebut dapat dibatalkan (dengan null=Trueargumen).

Juga, (lihat komentar saya yang lain), Anda mungkin ingin memeriksa apakah Anda benar-benar ingin FK menjadi unik. Ini secara teknis mengubah hubungan satu ke banyak menjadi hubungan satu ke satu - Anda hanya diperbolehkan satu catatan 'inspeksi' tunggal untuk GroupID atau SiteId yang diberikan (Anda tidak dapat memiliki dua atau lebih 'inspeksi' untuk satu GroupId atau SiteId) . Jika itu BENAR-BENAR apa yang Anda inginkan, Anda mungkin ingin menggunakan OneToOneField eksplisit sebagai gantinya (skema db akan sama tetapi model akan lebih eksplisit dan deskriptor terkait jauh lebih dapat digunakan untuk kasus penggunaan ini).

Sebagai catatan tambahan: dalam Model Django, bidang ForeignKey terwujud sebagai contoh model terkait, bukan sebagai id mentah. TKI, mengingat ini:

class Foo(models.Model):
    name = models.TextField()

class Bar(models.Model):
    foo = models.ForeignKey(Foo)


foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)

maka bar.fooakan memutuskan untuk foo, bukan untuk foo.id. Jadi, Anda tentu ingin mengubah nama InspectionIDdan SiteIDbidang menjadi benarinspection dan site. BTW, dengan Python, konvensi penamaan adalah 'all_lower_with_underscores' untuk hal lain selain nama kelas dan konstanta pseudo.

Sekarang untuk pertanyaan inti Anda: tidak ada cara standar SQL spesifik untuk menegakkan batasan "satu atau yang lain" di tingkat basis data, jadi biasanya dilakukan dengan menggunakan PERIKSA kendala , yang dilakukan dalam model Django dengan meta "kendala" model. opsi .

Ini dikatakan, bagaimana kendala sebenarnya didukung dan ditegakkan di tingkat db tergantung pada vendor DB Anda (MySQL <8.0.16 jelas mengabaikannya misalnya), dan jenis kendala yang Anda butuhkan di sini tidak akan ditegakkan di formulir atau validasi tingkat model , hanya ketika mencoba menyimpan model, jadi Anda juga ingin menambahkan validasi pada level model (lebih disukai) atau validasi level form, dalam kedua kasus dalam model (resp.) model atau clean()metode form .

Jadi untuk membuat cerita panjang pendek:

  • periksa dulu apakah Anda benar-benar menginginkan ini unique=True batasan , dan jika ya maka ganti bidang FK Anda dengan OneToOneField.

  • tambah sebuah blank=True argumen ke bidang FK (atau OneToOne) Anda

  • tambahkan yang tepat kendala pemeriksaan yang dalam meta model Anda - dokumen tersebut succint tetapi masih cukup eksplisit jika Anda tahu untuk melakukan pertanyaan kompleks dengan ORM (dan jika Anda tidak saatnya Anda belajar ;-))
  • tambahkan clean()metode ke model Anda yang memeriksa apakah Anda memiliki salah satu atau bidang lain dan memunculkan kesalahan validasi yang lain

dan Anda harus baik-baik saja, dengan asumsi RDBMS Anda menghormati batasan pemeriksaan tentu saja.

Perhatikan bahwa, dengan desain ini, Inspectionmodel Anda adalah tipuan yang sama sekali tidak berguna (namun mahal!) - Anda akan mendapatkan fitur yang sama persis dengan biaya lebih rendah dengan memindahkan FK (dan kendala, validasi dll) langsung ke InspectionReport.

Sekarang mungkin ada solusi lain - jaga model Inspeksi, tetapi letakkan FK sebagai OneToOneField di ujung lain hubungan (di Situs dan Grup):

class Inspection(models.Model):
    id = models.AutoField(primary_key=True) # a pk is always unique !

class InspectionReport(models.Model):
    # you actually don't need to manually specify a PK field,
    # Django will provide one for you if you don't
    # id = models.AutoField(primary_key=True)

    inspection = ForeignKey(Inspection, ...)
    date = models.DateField(null=True) # you should have a default then
    comment = models.CharField(max_length=255, blank=True default="")
    signature = models.CharField(max_length=255, blank=True, default="")


class Group(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

class Site(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

Dan kemudian Anda bisa mendapatkan semua laporan untuk Situs atau Grup tertentu yoursite.inspection.inspectionreport_set.all() .

Ini menghindari keharusan menambahkan kendala atau validasi tertentu, tetapi dengan biaya tingkat tipuan tambahan (SQL join klausa dll).

Solusi mana yang merupakan "yang terbaik" benar-benar tergantung pada konteks, jadi Anda harus memahami implikasi keduanya dan memeriksa bagaimana Anda biasanya menggunakan model Anda untuk mencari tahu mana yang lebih tepat untuk kebutuhan Anda sendiri. Sejauh yang saya ketahui dan tanpa lebih banyak konteks (atau ragu-ragu) saya lebih suka menggunakan solusi dengan tingkat tipuan yang lebih sedikit, tetapi YMMV.

NB tentang hubungan generik: itu bisa berguna ketika Anda benar-benar memiliki banyak model terkait yang mungkin dan / atau tidak tahu sebelumnya model mana yang ingin dihubungkan dengan milik Anda. Ini sangat berguna untuk aplikasi yang dapat digunakan kembali (pikirkan fitur "komentar" atau "tag" dll) atau yang dapat dikembangkan (kerangka kerja manajemen konten, dll.). The downside adalah bahwa itu membuat permintaan lebih berat (dan agak tidak praktis ketika Anda ingin melakukan permintaan manual pada db Anda). Dari pengalaman, mereka dapat dengan cepat menjadi PITA bot wrt / kode dan perf, jadi lebih baik menyimpannya ketika tidak ada solusi yang lebih baik (dan / atau ketika overhead pemeliharaan dan runtime tidak menjadi masalah).

2 sen saya.


2

Django memiliki antarmuka baru (sejak 2.2) untuk membuat batasan DB: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Anda dapat menggunakan a CheckConstraintuntuk menegakkan satu-dan-hanya-satu yang bukan nol. Saya menggunakan dua untuk kejelasan:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
    SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(SiteID=None) | ~Q(GroupId=None),
                name='at_least_1_non_null'),
            ),
            models.CheckConstraint(
                check=Q(SiteID=None) | Q(GroupId=None),
                name='at_least_1_null'),
            ),
        ]

Ini hanya akan menegakkan batasan di tingkat DB. Anda perlu memvalidasi input dalam formulir Anda atau serializers secara manual.

Sebagai catatan, Anda mungkin harus menggunakan OneToOneFieldbukan ForeignKey(unique=True). Anda juga akan menginginkannya blank=True.


0

Saya pikir Anda berbicara tentang hubungan Generik , dok . Jawaban Anda terlihat mirip dengan yang ini .

Beberapa waktu yang lalu saya perlu menggunakan hubungan Generik tetapi saya membaca di sebuah buku dan di tempat lain bahwa penggunaannya harus dihindari, saya pikir itu adalah Two Scoops of Django.

Saya akhirnya membuat model seperti ini:

class GroupInspection(models.Model):
    InspectionID = models.ForeignKey..
    GroupID = models.ForeignKey..

class SiteInspection(models.Model):
    InspectionID = models.ForeignKey..
    SiteID = models.ForeignKey..

Saya tidak yakin apakah ini solusi yang baik dan seperti yang Anda sebutkan, Anda lebih suka tidak menggunakannya, tetapi ini berhasil dalam kasus saya.


"Saya membaca di sebuah buku dan di tempat lain" adalah tentang kemungkinan alasan terburuk untuk melakukan (atau menghindari melakukan) sesuatu.
bruno desthuilliers

@brunodesthuilliers, saya pikir Two Scoops of Django adalah buku yang bagus.
Luis Silva

Tidak tahu, saya belum membacanya. Tapi itu tidak ada hubungannya: maksud saya adalah jika Anda tidak mengerti mengapa buku itu mengatakan demikian, maka itu bukan pengetahuan atau pengalaman, itu keyakinan agama. Saya tidak keberatan dengan kepercayaan agama dalam hal agama, tetapi mereka tidak memiliki tempat di CS. Entah Anda memahami apa pro dan kontra dari beberapa fitur dan kemudian Anda dapat menilai apakah itu sesuai dalam konteks yang diberikan , atau Anda tidak dan kemudian Anda tidak boleh dengan sembarangan menirukan apa yang telah Anda baca. Ada kasus penggunaan yang sangat valid untuk hubungan generik, intinya bukan untuk menghindarinya sama sekali tetapi untuk mengetahui kapan harus menghindarinya.
bruno desthuilliers

NB Saya sangat mengerti bahwa seseorang tidak dapat mengetahui segalanya tentang CS - ada domain di mana saya tidak memiliki pilihan lain selain mempercayai beberapa buku. Tapi saya mungkin tidak akan menjawab pertanyaan tentang topik itu ;-)
bruno desthuilliers

0

Mungkin sudah terlambat untuk menjawab pertanyaan Anda, tetapi saya pikir solusi saya mungkin cocok untuk kasus orang lain.

Saya akan membuat model baru, sebut saja Dependency, dan terapkan logika dalam model itu.

class Dependency(models.Model):
    Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

Kemudian saya akan menulis logika agar dapat diterapkan dengan sangat eksplisit.

class Dependency(models.Model):
    group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    _is_from_custom_logic = False

    @classmethod
    def create_dependency_object(cls, group=None, site=None):
        # you can apply any conditions here and prioritize the provided args
        cls._is_from_custom_logic = True
        if group:
            _new = cls.objects.create(group=group)
        elif site:
            _new = cls.objects.create(site=site)
        else:
            raise ValueError('')
        return _new

    def save(self, *args, **kwargs):
        if not self._is_from_custom_logic:
            raise Exception('')
        return super().save(*args, **kwargs)

Sekarang Anda hanya perlu membuat satu ForeignKeyuntuk Inspectionmodel Anda .

Dalam viewfungsi Anda, Anda perlu membuat Dependencyobjek dan kemudian menetapkannya ke Inspectioncatatan Anda . Pastikan Anda menggunakan create_dependency_objectdiview fungsi .

Ini cukup banyak membuat kode Anda eksplisit dan bukti bug. Penegakannya dapat dilewati dengan sangat mudah. Tetapi intinya adalah bahwa ia membutuhkan pengetahuan sebelumnya untuk batasan yang tepat ini untuk dilewati.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.