Gibt es eine Möglichkeit, eine eindeutige ID über 2 Felder zu erstellen?

14

Hier ist mein Modell:

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

Im Wesentlichen möchte ich, dass other_modeldiese Tabelle eindeutig ist. Das heißt, wenn es einen Datensatz gibt, in dem sich other_model_oneid befindet 123, sollte ich nicht zulassen, dass ein anderer Datensatz mit other_model_twoid as erstellt wird 123. Ich kann es wohl überschreiben, cleanaber ich habe mich gefragt, ob in Django etwas eingebaut ist.

Ich verwende Version 2.2.5 mit PSQL.

Bearbeiten: Dies ist keine eindeutige Situation zusammen. Wenn ich einen Datensatz mit other_model_one_id=1und anderen hinzufüge other_model_two_id=2, sollte ich keinen weiteren Datensatz mit other_model_one_id=2und anderen hinzufügen könnenother_model_two_id=1

Pittfall
quelle
Welche Django-Version verwenden Sie?
Willem Van Onsem
Ich benutze Version 2.2.5
Pittfall
Mögliches Duplikat von Django Unique Together (mit Fremdschlüsseln)
Toan Quoc Ho
1
Dies ist keine eindeutige Situation zusammen, dies ist eindeutig, aber über 2 Felder, wenn dies sinnvoll ist.
Pittfall

Antworten:

10

Ich erkläre hier verschiedene Optionen, vielleicht kann eine davon oder eine Kombination für Sie nützlich sein.

Überschreiben save

Ihre Einschränkung ist eine Geschäftsregel. Sie können die saveMethode überschreiben , um die Datenkonsistenz zu gewährleisten:


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)

Design ändern

Ich habe eine Probe leicht verständlich gemacht. Nehmen wir dieses Szenario an:

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

Jetzt möchten Sie vermeiden, dass eine Mannschaft ein Spiel mit sich selbst spielt. Auch Team A kann nur einmal mit Team B spielen (fast Ihre Regeln). Sie können Ihre Modelle wie folgt umgestalten:

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

Dies sieht aus wie ein symmetrisches Problem, das Django für Sie erledigen kann. Anstatt ein GroupedModelsModell zu erstellen , erstellen Sie einfach ein ManyToManyField-Feld mit sich selbst auf OtherModel:

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

Dies ist, was Django für diese Szenarien eingebaut hat.

dani herrera
quelle
Ansatz eins ist der, den ich verwendet habe (aber auf eine Datenbankbeschränkung gehofft). Ansatz 2 unterscheidet sich ein wenig darin, dass in meinem Szenario ein Team, das ein Spiel gespielt hat, nie wieder ein Spiel spielen kann. Ich habe Ansatz 3 nicht verwendet, da ich mehr Daten in der Gruppierung speichern wollte. Danke für die Antwort.
Pittfall
Wenn eine Mannschaft ein Spiel gespielt hat, kann sie nie wieder ein Spiel spielen. weil dies ich match_idauf unike Einschränkung aufgenommen habe, um Teams zu erlauben, unbegrenzte Spiele zu spielen. Entfernen Sie einfach dieses Feld, um die Wiedergabe wieder einzuschränken.
Dani Herrera
Ah ja! Danke, das habe ich verpasst und mein anderes Modell könnte ein Eins-zu-Eins-Feld sein.
Pittfall
1
Ich denke, Option 2 gefällt mir am besten. Das einzige Problem, das ich damit habe, ist, dass es in einer Welt, in der der Administrator als FE verwendet wird, möglicherweise ein benutzerdefiniertes Formular für den "durchschnittlichen" Benutzer benötigt. Leider lebe ich in dieser Welt. Aber ich denke, das sollte die akzeptierte Antwort sein. Vielen Dank!
Pittfall
Die zweite Option ist der richtige Weg. Dies ist eine großartige Antwort. @Pitfall bezüglich des Administrators Ich habe eine weitere Antwort hinzugefügt. Das Administrationsformular sollte kein großes Problem sein.
Cezar
1

Es ist keine sehr befriedigende Antwort, aber leider gibt es keine Möglichkeit, das, was Sie beschreiben, mit einer einfachen integrierten Funktion zu tun.

Was Sie beschrieben haben, cleanwürde funktionieren, aber Sie müssen vorsichtig sein, um es manuell aufzurufen, da es meiner Meinung nach nur bei Verwendung von ModelForm automatisch aufgerufen wird. Möglicherweise können Sie eine komplexe Datenbankeinschränkung erstellen , die jedoch außerhalb von Django liegt, und Sie müssten Datenbankausnahmen behandeln (was in Django während einer Transaktion schwierig sein kann).

Vielleicht gibt es einen besseren Weg, um die Daten zu strukturieren?

Tim Tisdall
quelle
Ja, Sie haben Recht, dass es manuell aufgerufen werden muss, weshalb mir der Ansatz nicht gefallen hat. Es funktioniert nur so, wie ich es im Administrator möchte, wie Sie erwähnt haben.
Pittfall
0

Es gibt bereits eine großartige Antwort von Dani Herrera , aber ich möchte darauf näher eingehen .

Wie in der zweiten Option erläutert, besteht die vom OP geforderte Lösung darin, das Design zu ändern und zwei eindeutige Einschränkungen paarweise zu implementieren. Die Analogie zu den Basketballspielen verdeutlicht das Problem auf sehr praktische Weise.

Anstelle eines Basketballspiels verwende ich ein Beispiel für Fußballspiele. Ein Fußballspiel (das ich es nenne Event) wird von zwei Teams gespielt (in meinen Modellen ist es ein Team Competitor). Dies ist eine Viele-zu-Viele-Beziehung ( m:n), die nin diesem speziellen Fall auf zwei begrenzt ist. Das Prinzip ist für eine unbegrenzte Anzahl geeignet.

So sehen unsere Modelle aus:

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

Ein Ereignis könnte sein:

  • Titel: Carabao Cup, 4. Runde,
  • Veranstaltungsort: Anfield
  • Zeit: 30. Oktober 2019, 19:30 GMT
  • Teilnehmer:
    • Name: Liverpool, Stadt: Liverpool
    • Name: Arsenal, Stadt: London

Jetzt müssen wir das Problem aus der Frage lösen. Django erstellt automatisch eine Zwischentabelle zwischen den Modellen mit einer Viele-zu-Viele-Beziehung. Wir können jedoch ein benutzerdefiniertes Modell verwenden und weitere Felder hinzufügen. Ich nenne das Modell Participant:

Klassenteilnehmer (models.Model):
    ROLES = (
        ('H', 'Zuhause'),
        ('V', 'Besucher'),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    Wettbewerber = Modelle.ForeignKey (Wettbewerber, on_delete = Modelle.CASCADE)
    Rolle = Modelle.CharField (max_length = 1, Auswahl = ROLES)

    Klasse Meta:
        unique_together = (
            ('Ereignis', 'Rolle'),
            ('Veranstaltung', 'Konkurrent'),
        )

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

Das ManyToManyFieldhat eine Option through, mit der wir das Zwischenmodell angeben können. Lassen Sie uns das im Modell ändern 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

Die eindeutigen Einschränkungen begrenzen jetzt automatisch die Anzahl der Teilnehmer pro Veranstaltung auf zwei (da es nur zwei Rollen gibt: Heim und Besucher ).

In einem bestimmten Ereignis (Fußballspiel) kann es nur eine Heimmannschaft und nur eine Besucherteams geben. Ein Verein ( Competitor) kann entweder als Heimmannschaft oder als Besucherteam auftreten.

Wie verwalten wir jetzt all diese Dinge im Admin? So was:

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)

Wir haben das Participantals Inline in das hinzugefügt EventAdmin. Wenn wir neue erstellen Event, können wir die Heimmannschaft und die Besucherteams auswählen. Die Option max_numbegrenzt die Anzahl der Einträge auf 2, daher können nicht mehr als 2 Teams pro Event hinzugefügt werden.

Dies kann für andere Anwendungsfälle überarbeitet werden. Nehmen wir an, unsere Veranstaltungen sind Schwimmwettkämpfe und statt Heim und Besucher haben wir die Bahnen 1 bis 8. Wir überarbeiten nur Folgendes 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())

Mit dieser Modifikation können wir dieses Ereignis haben:

  • Titel: FINA 2019, 50m Backstroke Herrenfinale,

    • Veranstaltungsort: Nambu University Municipal Aquatics Center
    • Zeit: 28. Juli 2019, 20:02 UTC + 9
    • Teilnehmer:

      • Name: Michael Andrew, Stadt: Edina, USA, Rolle: Spur 1
      • Name: Zane Waddell, Stadt: Bloemfontein, Südafrika, Rolle: Spur 2
      • Name: Evgeny Rylov, Stadt: Novotroitsk, Russland, Rolle: Spur 3
      • Name: Kliment Kolesnikov, Stadt: Moskau, Russland, Rolle: Spur 4

      // und so weiter Spur 5 bis Spur 8 (Quelle: Wikipedia

Ein Schwimmer kann nur einmal in einem Lauf auftreten, und eine Bahn kann nur einmal in einem Lauf besetzt werden.

Ich habe den Code an GitHub gesendet: https://github.com/cezar77/competition .

Alle Credits gehen wieder an dani herrera. Ich hoffe, diese Antwort bietet den Lesern einen Mehrwert.

Cezar
quelle