Czy istnieje sposób na utworzenie unikalnego identyfikatora na 2 polach?

14

Oto mój model:

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

Zasadniczo chcę, other_modelaby była wyjątkowa w tej tabeli. Oznacza to, że jeśli istnieje rekord, w którym other_model_oneznajduje się id 123, nie powinienem zezwalać na tworzenie kolejnego rekordu z other_model_twoid jako 123. Chyba mogę to zmienić clean, ale zastanawiałem się, czy django ma coś wbudowanego.

Używam wersji 2.2.5 z PSQL.

Edycja: To nie jest wspólna sytuacja. Jeśli dodam rekord za pomocą other_model_one_id=1i innych other_model_two_id=2, nie powinienem być w stanie dodać kolejnego rekordu za pomocą other_model_one_id=2i innychother_model_two_id=1

Pittfall
źródło
Jakiej wersji Django używasz?
Willem Van Onsem,
Używam wersji 2.2.5
Pittfall
Możliwy duplikat Django Unique Razem (z kluczami obcymi)
Toan Quoc Ho
1
Nie jest to wyjątkowa sytuacja razem, jest to wyjątkowa, ale jeśli ma to sens, ponad 2 pola.
Pittfall,

Odpowiedzi:

10

Wyjaśniam tutaj kilka opcji, może jedna z nich lub ich kombinacja może być dla Ciebie przydatna.

Nadrzędny save

Twoje ograniczenie jest regułą biznesową, możesz zastąpić savemetodę, aby zachować spójność danych:


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)

Zmień projekt

Podaję próbkę łatwą do zrozumienia. Załóżmy, że ten scenariusz:

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

Teraz chcesz uniknąć drużyny, która rozegra mecz ze sobą, a także drużyna A może zagrać tylko raz z drużyną B (prawie twoje zasady). Możesz przeprojektować swoje modele jako:

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

Wygląda to na symetryczny problem, django może sobie z tym poradzić. Zamiast tworzyć GroupedModelsmodel, po prostu stwórz pole ManyToManyField na sobie OtherModel:

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

Właśnie to ma wbudowane django dla tych scenariuszy.

Dani Herrera
źródło
Podejdź do tego, którego używałem (ale mając nadzieję na ograniczenie bazy danych). Podejście 2 różni się nieco od tego w moim scenariuszu, jeśli zespół grał w grę, nigdy więcej nie może grać. Nie zastosowałem podejścia 3, ponieważ w grupie było więcej danych, które chciałem przechowywać. Dziękuję za odpowiedź.
Pittfall,
jeśli drużyna rozegrała grę, nigdy więcej nie może jej zagrać. ponieważ match_idzawarłem to na zasadzie wyjątków, aby umożliwić drużynom rozgrywać nieograniczoną liczbę meczów. Wystarczy usunąć to pole, aby ponownie ograniczyć odtwarzanie.
dani herrera
o tak! dziękuję, że mi tego brakowało, a mój drugi model może być polem jeden do jednego.
Pittfall,
1
Myślę, że najbardziej podoba mi się opcja numer 2. Jedyny problem, jaki mam z tym, to to, że prawdopodobnie potrzebuje niestandardowego formularza dla „przeciętnego” użytkownika w świecie, w którym administrator jest używany jako FE. Niestety mieszkam w tym świecie. Ale myślę, że to powinna być zaakceptowana odpowiedź. Dzięki!
Pittfall,
Druga opcja to droga. To świetna odpowiedź. @Pitfall odnośnie administratora Dodałem kolejną odpowiedź. Formularz administratora nie powinien stanowić dużego problemu do rozwiązania.
cezar
1

To niezbyt satysfakcjonująca odpowiedź, ale niestety prawda jest taka, że ​​nie da się zrobić tego, co opisujesz, za pomocą prostej wbudowanej funkcji.

To, co opisałeś clean, działałoby, ale musisz uważać, aby ręcznie to wywołać, ponieważ myślę, że jest wywoływane automatycznie tylko podczas korzystania z ModelForm. Możesz być w stanie utworzyć złożone ograniczenie bazy danych ale będzie ono działać poza Django i będziesz musiał obsługiwać wyjątki bazy danych (co może być trudne w Django, gdy jest w trakcie transakcji).

Może istnieje lepszy sposób na uporządkowanie danych?

Tim Tisdall
źródło
Tak, masz rację, że trzeba to wywołać ręcznie, dlatego nie podobało mi się to podejście. Działa tylko tak, jak chcę w adminie, jak wspomniałeś.
Pittfall,
0

Jest już świetna odpowiedź od Dani Herrera , jednak chciałbym ją dalej rozwinąć.

Jak wyjaśniono w drugiej opcji, rozwiązaniem wymaganym przez PO jest zmiana projektu i wdrożenie dwóch unikalnych ograniczeń parami. Analogia z meczami koszykówki ilustruje problem w bardzo praktyczny sposób.

Zamiast meczu koszykówki używam przykładu z grami piłkarskimi (lub piłkarskimi). Mecz piłki nożnej (który nazywam Event) jest rozgrywany przez dwie drużyny (w moich modelach jest drużyna Competitor). Jest to relacja wiele do wielu ( m:n), z nograniczeniem do dwóch w tym konkretnym przypadku, zasada jest odpowiednia dla nieograniczonej liczby.

Oto jak wyglądają nasze modele:

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

Wydarzeniem może być:

  • tytuł: Puchar Carabao, 4. runda,
  • miejsce: Anfield
  • czas: 30. października 2019, 19:30 GMT
  • Uczestnicy:
    • nazwa: Liverpool, miasto: Liverpool
    • nazwa: Arsenał, miasto: Londyn

Teraz musimy rozwiązać problem z pytania. Django automatycznie tworzy tabelę pośrednią między modelami z relacją wiele do wielu, ale możemy użyć niestandardowego modelu i dodać kolejne pola. Nazywam ten model Participant:

Uczestnik klasy (modele. model):
    ROLE = (
        („H”, „Home”),
        („V”, „Visitor”),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    konkurent = modele.ForeignKey (konkurent, on_delete = modele.CASCADE)
    rola = modele. CharField (max_length = 1, wybory = ROLE)

    klasa Meta:
        unikalny_together = (
            („wydarzenie”, „rola”),
            („wydarzenie”, „zawodnik”),
        )

    def __str __ (self):
        zwróć format „{} - {}”. (self.event, self.get_role_display ())

ManyToManyFieldPosiada opcję through, która pozwala nam określić pośredni model. Zmieńmy to w modelu 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

Unikatowe ograniczenia teraz automatycznie ograniczyć liczbę konkurentów na razie do dwóch (bo tam są tylko dwie role: Start i Visitor ).

W konkretnym wydarzeniu (meczu piłki nożnej) może być tylko jedna drużyna gospodarzy i tylko jedna drużyna gości. Club ( Competitor) może występować jako drużyna gospodarzy lub drużyna gości.

Jak zarządzamy teraz tymi wszystkimi rzeczami w adminie? Lubię to:

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)

Dodaliśmy Participantjako wbudowany w EventAdmin. Kiedy tworzymy nowy Event, możemy wybrać zespół gospodarzy i zespół gości. Opcja max_numogranicza liczbę zgłoszeń do 2, dlatego nie można dodać więcej niż 2 drużyn na wydarzenie.

Można to zmienić dla różnych przypadków użycia. Powiedzmy, że nasze imprezy to zawody pływackie, a zamiast domu i gościa mamy ścieżki od 1 do 8. Po prostu zmieniliśmy 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())

Dzięki tej modyfikacji możemy mieć to wydarzenie:

  • tytuł: FINA 2019, 50-metrowy finał mężczyzn w stylu grzbietowym,

    • miejsce: Nambu University Municipal Aquatics Center
    • czas: 28. lipca 2019, 20:02 UTC + 9
    • Uczestnicy:

      • imię: Michael Andrew, miasto: Edina, USA, rola: linia 1
      • nazwa: Zane Waddell, miasto: Bloemfontein, Republika Południowej Afryki, rola: linia 2
      • nazwa: Evgeny Rylov, miasto: Novotroitsk, Rosja, rola: linia 3
      • nazwa: Kliment Kolesnikov, miasto: Moskwa, Rosja, rola: linia 4

      // i tak dalej na linii 5 do linii 8 (źródło: Wikipedia

Pływak może pojawić się tylko raz w upale, a tor może być zajęty tylko raz w upale.

Umieszczam kod w GitHub: https://github.com/cezar77/competition .

Ponownie wszystkie kredyty trafiają do Dani Herrera. Mam nadzieję, że ta odpowiedź zapewni czytelnikom pewną wartość dodaną.

Cezar
źródło