Чи є спосіб створити унікальний ідентифікатор для двох полів?


14

Ось моя модель:

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

По суті, я хочу, other_modelщоб ця таблиця була унікальною. Це означає, що якщо є запис, де other_model_oneє id 123, я не повинен дозволяти створювати інший запис з other_model_twoid як 123. Я можу перекрити, cleanмабуть, але мені було цікаво, чи є у джанго щось вбудоване.

Я використовую версію 2.2.5 з PSQL.

Редагувати: це не спільна ситуація. Якщо я додаю запис із other_model_one_id=1та іншими other_model_two_id=2, я не можу додати ще один запис із other_model_one_id=2та іншимother_model_two_id=1


Яку версію Django ви використовуєте?
Віллем Ван Онсем

Я використовую версію 2.2.5
Pittfall


1
Це не унікальна спільна ситуація, це унікальна, але понад 2 поля, якщо це має сенс.
Піттафл

Відповіді:


10

Я пояснюю тут кілька варіантів, можливо, один з них або комбінація може бути корисною для вас.

Переважаючий save

Ваше обмеження - це ділове правило, ви можете переопрацювати saveметод, щоб зберегти послідовність даних:


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)

Змінити дизайн

Я поставив зразок легко зрозуміти. Припустимо, такий сценарій:

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

Тепер ви хочете уникнути того, щоб команда грала в матчі з самою собою, і команда A лише один раз може грати з командою B (майже ваші правила). Ви можете змінити дизайн своїх моделей як:

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

Це виглядає як симетричне питання, джанго може вирішити це за вас. Замість створення GroupedModelsмоделі просто зробіть поле ManyToManyField із собою на OtherModel:

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

Це те, що було створено для цього сценарію джанго.


Підхід один - це той, який я використовував (але сподіваюся на обмеження бази даних). Підхід 2 трохи відрізняється від мого сценарію, якщо команда грала в гру, вони більше ніколи не зможуть зіграти гру. Я не використовував підхід 3, тому що в групі було більше даних, які я хотів би зберегти. Дякую за відповідь.
Піттафл

якщо команда зіграла в гру, вона більше ніколи не може зіграти гру. тому що це я включив match_idна єдине обмеження, щоб дозволити командам грати необмежені матчі. Просто видаліть це поле, щоб знову обмежити гру.
dani

ах так! дякую, я пропустив це, і моя інша модель могла б стати одним на один.
Pittfall

1
Я думаю, що мені найбільше подобається варіант №2. Єдине питання, яке я маю з цим, - це те, що він, ймовірно, потребує спеціальної форми для "середнього" користувача у світі, де адміністратор використовується як ЗП. На жаль, я живу в тому світі. Але я вважаю, що це має бути прийнятою відповіддю. Дякую!
Pittfall

Другий варіант - це шлях. Це чудова відповідь. @Pitfall щодо адміністратора Я додав ще одну відповідь. Форма адміністратора не повинна бути великою проблемою для вирішення.
cezar

1

Це не дуже задовольняє відповідь, але, на жаль, правда полягає в тому, що немає можливості зробити те, що ви описуєте, за допомогою простої вбудованої функції.

Те, що ви описали, cleanбуде працювати, але ви повинні бути обережними, щоб викликати його вручну, оскільки я думаю, що це викликається автоматично автоматично лише при використанні ModelForm. Можливо, вам вдасться створити складне обмеження для бази даних, але це може діяти за межами Django, і вам доведеться обробляти винятки з бази даних (що може бути складно у Django, коли посеред транзакції).

Можливо, є кращий спосіб структурувати дані?


Так, ви праві, що це потрібно викликати вручну, тому мені не сподобався підхід. Це працює лише так, як я хочу в адміністраторі, як ви згадали.
Pittfall

0

Від Dani Herrera вже є чудова відповідь , проте я хотів би детальніше розглянути її.

Як пояснено у другому варіанті, рішенням, яке вимагає ОП, є зміна конструкції та реалізація двох унікальних обмежень попарно. Аналогія з баскетбольними матчами ілюструє проблему дуже практичним чином.

Замість баскетбольного матчу я використовую приклад з футбольними (або футбольними) іграми. У футбольну гру (яку я так називаю Event) грають дві команди (у моїх моделях команда є Competitor). Це відношення "багато до багатьох" ( m:n), nв цьому конкретному випадку обмежено двома, принцип підходить для необмеженої кількості.

Ось як виглядають наші моделі:

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

Подія може бути:

  • Назва: Кубок Карабао, 4-й тур,
  • місце проведення: Anfield
  • час: 30. жовтня 2019 р., 19:30 за GMT
  • учасники:
    • назва: Ліверпуль, місто: Ліверпуль
    • назва: Арсенал, місто: Лондон

Тепер ми повинні вирішити питання з питання. Django автоматично створює проміжну таблицю між моделями, що мають відношення «багато до багатьох», але ми можемо використовувати власну модель і додавати додаткові поля. Я називаю цю модель Participant:

Учасник класу (моделі. Модель):
    РОЛИ = (
        ("Н", "Головна"),
        ("V", "Відвідувач"),
    )
    event = models.ForeignKey (подія, on_delete = моделі.CASCADE)
    конкурент = моделі.ForeignKey (Конкурент, on_delete = моделі.CASCADE)
    role = models.CharField (max_length = 1, вибір = ROLES)

    Мета мета:
        unique_together = (
            ("подія", "роль"),
            ("подія", "конкурент"),
        )

    def __str __ (само):
        повернути формат "{} - {}" (self.event, self.get_role_display ())

ManyToManyFieldМає опцію , throughяка дозволяє нам вказати проміжну модель. Давайте змінимо це в моделі 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

Унікальні обмеження тепер автоматично обмежуватимуть кількість конкурентів за подію до двох (адже є лише дві ролі: « Головна» та « Відвідувач» ).

У конкретному заході (футбольній грі) може бути лише одна домашня команда та лише одна команда відвідувачів. Клуб ( Competitor) може виступати як домашня команда або як команда відвідувачів.

Як ми зараз управляємо всіма цими речами в адміністраторі? Подобається це:

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)

Ми додали Participantяк вбудований текст у EventAdmin. Коли ми створюємо нове, Eventми можемо вибрати домашню команду та команду відвідувачів. Опція max_numобмежує кількість записів на 2, тому не можна додавати більше 2 команд на подію.

Це може бути відновлено для різних випадків використання. Скажімо, наші події - змагання з плавання, і замість дому та відвідувачів у нас є смуги від 1 до 8. Ми просто перетворюємо 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())

За допомогою цієї модифікації ми можемо мати цю подію:

  • титул: FINA 2019, чоловічий фінал на 50 м. на спині,

    • місце проведення: Університетський міський центр водного спорту в Намбу
    • час: 28. липня 2019, 20:02 UTC + 9
    • учасники:

      • ім'я: Майкл Ендрю, місто: Едіна, США, роль: провулок 1
      • ім'я: Зейн Вейдл, місто: Блумфонтейн, Південна Африка, роль: смуга 2
      • ім’я: Євген Рилов, місто: Новотроїцьк, Росія, роль: провулок 3
      • ім'я: Климент Колесніков, місто: Москва, Росія, роль: смуга 4

      // і так далі провулок 5 - смуга 8 (джерело: Вікіпедія

Плавець може з’явитися лише один раз у спеку, а смугу можна зайняти лише один раз у спеку.

Я ставлю код на GitHub: https://github.com/cezar77/comcharge .

Знову ж таки, всі кредити йдуть на дані герреру. Я сподіваюся, що ця відповідь надасть читачам деяку додаткову цінність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.