Apakah ada cara untuk membuat id unik di atas 2 bidang?

14

Inilah model saya:

class GroupedModels(models.Model):
    other_model_one = models.ForeignKey('app.other_model')
    other_model_two = models.ForeignKey('app.other_model')

Pada dasarnya, yang saya inginkan adalah other_modelmenjadi unik dalam tabel ini. Itu berarti bahwa jika ada catatan di mana other_model_oneid berada 123, saya seharusnya tidak mengizinkan catatan lain dibuat dengan other_model_twoid sebagai 123. Saya bisa mengesampingkan cleansaya kira tapi saya bertanya-tanya apakah Django memiliki sesuatu yang tertanam

Saya menggunakan versi 2.2.5 dengan PSQL.

Sunting: Ini bukan situasi bersama yang tidak tetap. Jika saya menambahkan catatan dengan other_model_one_id=1dan lainnya other_model_two_id=2, saya seharusnya tidak dapat menambahkan catatan lain dengan other_model_one_id=2dan lainnyaother_model_two_id=1

Pittfall
sumber
Versi Django apa yang Anda gunakan?
Willem Van Onsem
Saya menggunakan versi 2.2.5
Pittfall
Kemungkinan rangkap dari Django Unique Together (dengan kunci asing)
Toan Quoc Ho
1
Ini bukan situasi bersama yang unik, ini unik tetapi lebih dari 2 bidang jika itu masuk akal.
Pittfall

Jawaban:

10

Saya jelaskan beberapa opsi di sini, mungkin salah satunya atau kombinasi dapat bermanfaat bagi Anda.

Utama save

Batasan Anda adalah aturan bisnis, Anda bisa mengganti savemetode untuk menjaga data tetap konsisten:


class GroupedModels(models.Model): 
    # ...
    def clean(self):
        if (self.other_model_one.pk == self.other_model_two.pk):
            raise ValidationError({'other_model_one':'Some message'}) 
        if (self.other_model_one.pk < self.other_model_two.pk):
            #switching models
            self.other_model_one, self.other_model_two = self.other_model_two, self.other_model_one
    # ...
    def save(self, *args, **kwargs):
        self.clean()
        super(GroupedModels, self).save(*args, **kwargs)

Ubah desain

Saya menempatkan sampel yang mudah dimengerti. Anggap skenario ini:

class BasketballMatch(models.Model):
    local = models.ForeignKey('app.team')
    visitor = models.ForeignKey('app.team')

Sekarang, Anda ingin menghindari tim bermain dengan dirinya sendiri juga tim A hanya bisa bermain dengan tim B untuk sekali (hampir aturan Anda). Anda dapat mendesain ulang model Anda sebagai:

class BasketballMatch(models.Model):
    HOME = 'H'
    GUEST = 'G'
    ROLES = [
        (HOME, 'Home'),
        (GUEST, 'Guest'),
    ]
    match_id = models.IntegerField()
    role = models.CharField(max_length=1, choices=ROLES)
    player = models.ForeignKey('app.other_model')

    class Meta:
      unique_together = [ ( 'match_id', 'role', ) ,
                          ( 'match_id', 'player',) , ]

ManyToManyField.symmetrical

Ini terlihat seperti masalah simetris , Django dapat menanganinya untuk Anda. Alih-alih membuat GroupedModelsmodel, buat saja bidang ManyToManyField dengan sendirinya di OtherModel:

from django.db import models
class OtherModel(models.Model):
    ...
    grouped_models = models.ManyToManyField("self")

Inilah yang django miliki untuk skenario ini.

dani herrera
sumber
Pendekatan satu adalah yang saya gunakan (tapi berharap untuk kendala basis data). Pendekatan 2 sedikit berbeda dalam skenario saya, jika suatu tim telah memainkan permainan, mereka tidak akan pernah bisa memainkan permainan lagi. Saya tidak menggunakan pendekatan 3 karena ada lebih banyak data yang ingin saya simpan di pengelompokan. Terima kasih atas jawabannya.
Pittfall
jika suatu tim telah memainkan permainan, mereka tidak akan pernah bisa memainkan permainan lagi. karena ini saya sertakan match_idpada batasan unike, untuk memungkinkan tim memainkan pertandingan tanpa batas. Hapus saja bidang ini untuk membatasi pemutaran lagi.
dani herrera
ah iya! terima kasih saya melewatkannya dan model saya yang lain bisa menjadi bidang satu lawan satu.
Pittfall
1
Saya pikir saya suka opsi nomor 2 terbaik. Satu-satunya masalah yang saya miliki dengan itu adalah bahwa itu bisa dibilang membutuhkan bentuk khusus untuk pengguna "rata-rata", di dunia di mana admin digunakan sebagai FE. Sayangnya, saya hidup di dunia itu. Tapi saya pikir ini harus menjadi jawaban yang diterima. Terima kasih!
Pittfall
Opsi kedua adalah cara untuk pergi. Ini jawaban yang bagus. @ Turun mengenai admin saya telah menambahkan jawaban lebih lanjut. Formulir admin seharusnya tidak menjadi masalah besar untuk dipecahkan.
cezar
1

Ini bukan jawaban yang sangat memuaskan, tetapi sayangnya kebenarannya adalah tidak ada cara untuk melakukan apa yang Anda gambarkan dengan fitur bawaan yang sederhana.

Apa yang Anda gambarkan cleanakan berhasil, tetapi Anda harus berhati-hati untuk menyebutnya secara manual karena saya pikir itu hanya secara otomatis dipanggil ketika menggunakan ModelForm. Anda mungkin dapat membuat batasan basis data yang rumit tetapi itu akan hidup di luar Django dan Anda harus menangani pengecualian basis data (yang mungkin sulit di Django ketika di tengah transaksi).

Mungkin ada cara yang lebih baik untuk menyusun data?

Tim Tisdall
sumber
Ya, Anda benar bahwa itu harus secara manual dipanggil dan itulah sebabnya saya tidak suka pendekatan itu. Ini hanya berfungsi seperti yang saya inginkan di admin, seperti yang Anda sebutkan.
Pittfall
0

Sudah ada jawaban bagus dari dani herrera , namun saya ingin menguraikannya lebih lanjut.

Sebagaimana dijelaskan dalam opsi kedua, solusi seperti yang disyaratkan oleh OP adalah mengubah desain dan mengimplementasikan dua kendala unik berpasangan. Analogi dengan pertandingan bola basket menggambarkan masalah dengan cara yang sangat praktis.

Alih-alih pertandingan bola basket, saya menggunakan contoh dengan permainan sepak bola (atau sepak bola). Sebuah permainan sepak bola (yang saya sebut itu Event) dimainkan oleh dua tim (dalam model saya tim adalah Competitor). Ini adalah hubungan banyak ke banyak (m:n ), dengan nterbatas pada dua dalam kasus khusus ini, prinsipnya cocok untuk jumlah yang tidak terbatas.

Berikut tampilan model kami:

class Competitor(models.Model):
    name = models.CharField(max_length=100)
    city = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Event(models.Model):
    title = models.CharField(max_length=200)
    venue = models.CharField(max_length=100)
    time = models.DateTimeField()
    participants = models.ManyToManyField(Competitor)

    def __str__(self):
        return self.title

Suatu acara dapat:

  • judul: Piala Carabao, babak ke-4,
  • tempat: Anfield
  • waktu: 30. Oktober 2019, 19:30 GMT
  • peserta:
    • nama: Liverpool, kota: Liverpool
    • nama: Arsenal, kota: London

Sekarang kita harus menyelesaikan masalah dari pertanyaan. Django secara otomatis membuat tabel perantara antara model dengan hubungan banyak ke banyak, tetapi kita dapat menggunakan model khusus dan menambahkan bidang lebih lanjut. Saya menyebut model itu Participant:

Peserta kelas (model. Model):
    ROL =
        ('H', 'Rumah'),
        ('V', 'Pengunjung'),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    kompetitor = models.ForeignKey (Pesaing, on_delete = models.CASCADE)
    role = models.CharField (max_length = 1, choices = ROLES)

    kelas Meta:
        unique_together = (
            ('acara', 'peran'),
            ('acara', 'pesaing'),
        )

    def __str __ (mandiri):
        return '{} - {}'. format (self.event, self.get_role_display ())

The ManyToManyFieldmemiliki opsi throughyang memungkinkan kita untuk menentukan model menengah. Mari kita ubah itu dalam model Event:

class Event(models.Model):
    title = models.CharField(max_length=200)
    venue = models.CharField(max_length=100)
    time = models.DateTimeField()
    participants = models.ManyToManyField(
        Competitor,
        related_name='events', # if we want to retrieve events for a competitor
        through='Participant'
    )

    def __str__(self):
        return self.title

Batasan unik sekarang akan secara otomatis membatasi jumlah pesaing per peristiwa menjadi dua (karena hanya ada dua peran: Rumah dan Pengunjung ).

Dalam acara tertentu (pertandingan sepak bola) hanya ada satu tim tuan rumah dan satu tim tamu. Klub ( Competitor) dapat muncul sebagai tim tuan rumah atau sebagai tim pengunjung.

Bagaimana kita mengelola semua hal ini sekarang di admin? Seperti ini:

from django.contrib import admin

from .models import Competitor, Event, Participant


class ParticipantInline(admin.StackedInline): # or admin.TabularInline
    model = Participant
    max_num = 2


class CompetitorAdmin(admin.ModelAdmin):
    fields = ('name', 'city',)


class EventAdmin(admin.ModelAdmin):
    fields = ('title', 'venue', 'time',)
    inlines = [ParticipantInline]


admin.site.register(Competitor, CompetitorAdmin)
admin.site.register(Event, EventAdmin)

Kami telah menambahkan Participantsebagai sebaris dalam EventAdmin. Saat kami membuat yang baru, Eventkami dapat memilih tim tuan rumah dan tim tamu. Opsi max_nummembatasi jumlah entri menjadi 2, oleh karena itu tidak lebih dari 2 tim dapat ditambahkan per acara.

Ini dapat di refactored untuk kasus penggunaan yang berbeda. Katakanlah acara kami adalah kompetisi renang dan alih-alih rumah dan pengunjung, kami memiliki jalur 1 hingga 8. Kami hanya memperbaiki Participant:

class Participant(models.Model):
    ROLES = (
        ('L1', 'lane 1'),
        ('L2', 'lane 2'),
        # ... L3 to L8
    )
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    competitor = models.ForeignKey(Competitor, on_delete=models.CASCADE)
    role = models.CharField(max_length=1, choices=ROLES)

    class Meta:
        unique_together = (
            ('event', 'role'),
            ('event', 'competitor'),
        )

    def __str__(self):
        return '{} - {}'.format(self.event, self.get_role_display())

Dengan modifikasi ini kita dapat memiliki acara ini:

  • judul: FINA 2019, final gaya punggung 50m putra,

    • tempat: Pusat Akuatik Kota Universitas Nambu
    • waktu: 28 Juli 2019, 20:02 UTC + 9
    • peserta:

      • nama: Michael Andrew, kota: Edina, AS, peran: jalur 1
      • nama: Zane Waddell, kota: Bloemfontein, Afrika Selatan, peran: lane 2
      • nama: Evgeny Rylov, kota: Novotroitsk, Rusia, peran: jalur 3
      • nama: Kliment Kolesnikov, kota: Moskow, Rusia, peran: jalur 4

      // dan seterusnya jalur 5 ke jalur 8 (sumber: Wikipedia

Perenang hanya bisa muncul sekali dalam panas, dan jalur hanya bisa ditempati sekali dalam panas.

Saya memasukkan kode ke GitHub: https://github.com/cezar77/competition .

Sekali lagi, semua kredit jatuh ke dani herrera. Saya harap jawaban ini memberikan nilai tambah bagi pembaca.

cezar
sumber