Есть ли способ создать уникальный идентификатор над 2 полями?

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_twoидентификатором as 123. Я могу переопределить, cleanя думаю, но мне было интересно, есть ли в Django что-то встроенное.

Я использую версию 2.2.5 с PSQL.

Изменить: Это не сложная ситуация вместе. Если я добавлю запись с other_model_one_id=1и другим other_model_two_id=2, я не смогу добавить еще одну запись с other_model_one_id=2иother_model_two_id=1

Pittfall
источник
Какую версию Django вы используете?
Виллем Ван Онсем
Я использую версию 2.2.5
Pittfall
Возможный дубликат Django Unique Together (с внешними ключами)
Тоан Куок Хо,
1
Это не уникальная ситуация вместе, это уникально, но более 2 полей, если это имеет смысл.
Pittfall

Ответы:

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')

Теперь вы хотите, чтобы команда не играла матч с самой собой, а команда А может играть только с командой Б один раз (почти по вашим правилам). Вы можете перепроектировать свои модели как:

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

Это похоже на симметричную проблему, Django может решить ее для вас. Вместо того, чтобы создавать GroupedModelsмодель, просто создайте поле ManyToManyField с самим собой на OtherModel:

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

Это то, что django имеет как встроенный для этих сценариев.

Дани Эррера
источник
Первый подход - тот, который я использовал (но надеялся на ограничение базы данных). Подход 2 немного отличается тем, что в моем сценарии, если команда играла в игру, она никогда больше не сможет играть в игру. Я не использовал подход 3, потому что в группе было больше данных, которые я хотел сохранить. Спасибо за ответ.
Pittfall
если команда играла в игру, она никогда больше не сможет играть в игру. потому что это я включил match_idв ограничение unike, чтобы позволить командам играть неограниченное количество матчей. Просто удалите это поле, чтобы снова ограничить игру.
дани Эррера
о да! спасибо, что я пропустил это, и моя другая модель могла бы быть один к одному.
Pittfall
1
Я думаю, что мне больше нравится вариант № 2. Единственная проблема, с которой я столкнулся, заключается в том, что для «среднего» пользователя, возможно, ему нужна пользовательская форма в мире, где администратор используется как FE. К сожалению, я живу в этом мире. Но я думаю, что это должен быть принятый ответ. Спасибо!
Pittfall
Второй вариант - путь. Это отличный ответ. @ Касаемо админа Я добавил еще один ответ. Форма администратора не должна быть большой проблемой для решения.
Цезарь
1

Это не очень удовлетворительный ответ, но, к сожалению, правда в том, что нет способа сделать то, что вы описываете, с помощью простой встроенной функции.

То, что вы описали, cleanбудет работать, но вы должны быть осторожны, чтобы вызывать его вручную, так как я думаю, что он вызывается только автоматически при использовании ModelForm. Возможно, вы сможете создать сложное ограничение базы данных, но оно будет находиться за пределами Django, и вам придется обрабатывать исключения из базы данных (что может быть сложно в Django, когда выполняется транзакция).

Может быть, есть лучший способ структурировать данные?

Тим Тисдалл
источник
Да, вы правы, что его нужно вызывать вручную, поэтому мне не понравился подход. Это работает только так, как я хочу в админе, как вы упоминали.
Pittfall
0

От Дани Эрреры уже есть отличный ответ , однако я хотел бы уточнить его.

Как объясняется во втором варианте, решение, требуемое OP, состоит в том, чтобы изменить конструкцию и реализовать два уникальных ограничения попарно. Аналогия с баскетбольными матчами очень наглядно иллюстрирует проблему.

Вместо баскетбольного матча я использую пример с футбольными (или футбольными) играми. В футбольную игру (которую я называю это 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 по Гринвичу
  • участники:
    • название: Ливерпуль, город: Ливерпуль
    • название: Арсенал, город: Лондон

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

Участник класса (модели. Модель):
    РОЛИ = (
        («Н», «Дом»),
        («V», «Посетитель»),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    Competitor = models.ForeignKey (Competitor, on_delete = models.CASCADE)
    role = models.CharField (max_length = 1, выбор = РОЛИ)

    Класс Meta:
        unique_together = (
            («событие», «роль»),
            («событие», «конкурент»),
        )

    def __str __ (self):
        return '{} - {}'. format (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/competition .

Опять же, все кредиты идут к Дани Эррера. Я надеюсь, что этот ответ обеспечит читателям дополнительную ценность.

Cezar
источник