C'è un modo per creare un ID univoco su 2 campi?


14

Ecco il mio modello:

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

In sostanza, quello che voglio è other_modelessere unico in questa tabella. Ciò significa che se esiste un record in cui other_model_oneid è 123, non dovrei consentire che venga creato un altro record con other_model_twoid as 123. Posso scavalcare, cleansuppongo, ma mi chiedevo se Django abbia qualcosa incorporato.

Sto usando la versione 2.2.5 con PSQL.

Modifica: questa non è una situazione unqiue insieme. Se aggiungo un record con other_model_one_id=1e altro other_model_two_id=2, non dovrei essere in grado di aggiungere un altro record con other_model_one_id=2e altroother_model_two_id=1


Quale versione di Django stai usando?
Willem Van Onsem,

Sto usando la versione 2.2.5
Pittfall il


1
Questa non è una situazione unica insieme, è unica ma su 2 campi se questo ha un senso.
Pittfall,

Risposte:


10

Spiego diverse opzioni qui, forse una di esse o una combinazione può essere utile per te.

Override save

Il tuo vincolo è una regola aziendale, puoi ignorare il savemetodo per mantenere coerenti i dati:


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)

Cambia design

Ho messo un campione facile da capire. Supponiamo che questo scenario:

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

Ora, vuoi evitare che una squadra giochi una partita con se stessa, anche la squadra A può giocare una volta sola con la squadra B (quasi le tue regole). Puoi riprogettare i tuoi modelli come:

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

Sembra un problema simmetrico , Django può gestirlo per te. Invece di creare un GroupedModelsmodello, basta creare un campo ManyToManyField con se stesso su OtherModel:

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

Questo è ciò che Django ha integrato in questi scenari.


Approccio uno è quello che stavo usando (ma sperando in un vincolo di database). L'approccio 2 è un po 'diverso da quello nel mio scenario, se una squadra ha giocato una partita, non potrà mai più giocare una partita. Non ho usato l'approccio 3 perché c'erano più dati che volevo archiviare nel raggruppamento. Grazie per la risposta.
Pittfall,

se una squadra ha giocato una partita, non potrà mai più giocare una partita. perché questo l'ho incluso match_idin un vincolo diverso, per consentire alle squadre di giocare partite illimitate. Basta rimuovere questo campo per limitare nuovamente il gioco.
dani herrera,

Ah sì! grazie l'ho perso e l'altro mio modello potrebbe essere un campo uno a uno.
Pittfall,

1
Penso che mi piaccia di più l'opzione numero 2. L'unico problema che ho con esso è che probabilmente ha bisogno di un modulo personalizzato per l'utente "medio", in un mondo in cui l'amministratore viene utilizzato come FE. Sfortunatamente vivo in quel mondo. Ma penso che questa dovrebbe essere la risposta accettata. Grazie!
Pittfall,

La seconda opzione è la strada da percorrere. Questa è un'ottima risposta @Pitfall per quanto riguarda l'amministratore ho aggiunto un'ulteriore risposta. Il modulo di amministrazione non dovrebbe essere un grosso problema da risolvere.
Cezar,

1

Non è una risposta molto soddisfacente, ma purtroppo la verità è che non c'è modo di fare quello che stai descrivendo con una semplice funzione integrata.

Quello che hai descritto cleanfunzionerebbe, ma devi fare attenzione a chiamarlo manualmente poiché penso che sia chiamato automaticamente solo quando usi ModelForm. Potresti essere in grado di creare un vincolo di database complesso ma che vivrebbe al di fuori di Django e dovresti gestire le eccezioni del database (che può essere difficile in Django quando sei nel mezzo di una transazione).

Forse c'è un modo migliore per strutturare i dati?


Sì, hai ragione che deve essere chiamato manualmente, motivo per cui non mi è piaciuto l'approccio. Funziona solo come voglio nell'amministratore, come hai detto.
Pittfall,

0

C'è già un'ottima risposta da parte di dani herrera , tuttavia desidero approfondirla ulteriormente.

Come spiegato nella seconda opzione, la soluzione richiesta dall'OP è quella di modificare il design e implementare due vincoli univoci a coppie. L'analogia con le partite di basket illustra il problema in modo molto pratico.

Invece di una partita di basket, uso esempi con partite di calcio (o di calcio). Una partita di calcio (che io la chiamo Event) è giocata da due squadre (nei miei modelli una squadra è Competitor). Questa è una relazione molti-a-molti ( m:n), con un nlimite a due in questo caso particolare, il principio è adatto per un numero illimitato.

Ecco come appaiono i nostri modelli:

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

Un evento potrebbe essere:

  • titolo: Coppa Carabao, 4 ° turno,
  • sede: Anfield
  • orario: 30. ottobre 2019, 19:30 GMT
  • partecipanti:
    • nome: Liverpool, città: Liverpool
    • nome: Arsenal, città: Londra

Ora dobbiamo risolvere il problema dalla domanda. Django crea automaticamente una tabella intermedia tra i modelli con una relazione molti-a-molti, ma possiamo usare un modello personalizzato e aggiungere altri campi. Chiamo quel modello Participant:

partecipante alla classe (models.Model):
    RUOLI = (
        ('H', 'Home'),
        ("V", "Visitatore"),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    competitor = models.ForeignKey (Competitor, on_delete = models.CASCADE)
    ruolo = models.CharField (max_length = 1, scelte = ROLES)

    classe Meta:
        unique_together = (
            ("evento", "ruolo"),
            ("evento", "concorrente"),
        )

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

L' ManyToManyFieldha un'opzione throughche ci permette di specificare il modello intermedio. Cambiamo quello nel modello 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

I vincoli univoci ora limitano automaticamente il numero di concorrenti per evento a due (perché ci sono solo due ruoli: Casa e Visitatore ).

In un particolare evento (partita di calcio) può esserci solo una squadra di casa e una sola squadra ospite. Un club ( Competitor) può apparire come squadra di casa o come squadra ospite.

Come gestiamo ora tutte queste cose nell'amministratore? Come questo:

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)

Abbiamo aggiunto Participantcome inline in EventAdmin. Quando ne creiamo di nuovi Eventpossiamo scegliere la squadra di casa e la squadra di visitatori. L'opzione max_numlimita il numero di voci a 2, quindi non è possibile aggiungere più di 2 squadre per evento.

Questo può essere refactored per diversi casi d'uso. Supponiamo che i nostri eventi siano gare di nuoto e invece di casa e visitatori, abbiamo corsie da 1 a 8. Rifattorizziamo semplicemente 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())

Con questa modifica possiamo avere questo evento:

  • titolo: FINA 2019, finale maschile a dorso 50m,

    • sede: Centro acquatico comunale dell'Università di Nambu
    • tempo: 28. luglio 2019, 20:02 UTC + 9
    • partecipanti:

      • nome: Michael Andrew, città: Edina, USA, ruolo: corsia 1
      • nome: Zane Waddell, città: Bloemfontein, Sudafrica, ruolo: corsia 2
      • nome: Evgeny Rylov, città: Novotroitsk, Russia, ruolo: corsia 3
      • nome: Kliment Kolesnikov, città: Mosca, Russia, ruolo: corsia 4

      // e così via da corsia 5 a corsia 8 (fonte: Wikipedia

Un nuotatore può apparire solo una volta in un caldo e una corsia può essere occupata solo una volta in un caldo.

Ho inserito il codice su GitHub: https://github.com/cezar77/competition .

Ancora una volta, tutti i crediti vanno a dani herrera. Spero che questa risposta offra un valore aggiunto ai lettori.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.