Есть ли способ создать уникальный идентификатор над 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


Какую версию Django вы используете?
Виллем Ван Онсем

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


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 .

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.