Existe uma maneira de criar um ID exclusivo em 2 campos?

14

Aqui está o meu modelo:

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

Essencialmente, o que eu quero é other_modelser único nesta tabela. Isso significa que, se houver um registro em que other_model_oneid está 123, não devo permitir que outro registro seja criado com o other_model_twoid como 123. Eu posso substituir, cleaneu acho, mas eu queria saber se django tem algo embutido.

Estou usando a versão 2.2.5 com PSQL.

Edit: Esta não é uma situação unqiue juntos. Se eu adicionar um registro com other_model_one_id=1e outro other_model_two_id=2, não será possível adicionar outro registro com other_model_one_id=2e comother_model_two_id=1

Pittfall
fonte
Qual versão do Django você está usando?
Willem Van Onsem 23/10/19
Estou usando a versão 2.2.5
Pittfall 23/10/19
Possível duplicado de Django exclusivo Together (com chaves estrangeiras)
Toan Quoc Ho
11
Esta não é uma situação única em conjunto, é única, mas com mais de 2 campos, se isso fizer algum sentido.
Pittfall 23/10/19

Respostas:

10

Eu explico várias opções aqui, talvez uma delas ou uma combinação possa ser útil para você.

Substituindo save

Sua restrição é uma regra comercial, você pode substituir o savemétodo para manter os dados consistentes:


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)

Alterar design

Eu coloquei uma amostra fácil de entender. Vamos supor este cenário:

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

Agora, você deseja evitar que um time jogue uma partida consigo mesmo, também o time A só pode jogar com o time B pela primeira vez (quase suas regras). Você pode redesenhar seus modelos como:

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

Parece um problema simétrico , o django pode lidar com isso para você. Em vez de criar GroupedModelsmodelo, basta criar um campo ManyToManyField com ele mesmo OtherModel:

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

Isto é o que o django incorporou para esses cenários.

dani herrera
fonte
A abordagem um é a que eu estava usando (mas esperando por uma restrição de banco de dados). A abordagem 2 é um pouco diferente. No meu cenário, se um time jogou um jogo, ele nunca poderá jogar novamente. Não usei a abordagem 3 porque havia mais dados que queria armazenar no agrupamento. Obrigado pela resposta.
Pittfall
se um time jogou um jogo, nunca mais poderá jogar. porque eu incluí isso match_idem restrições não iguais, para permitir que as equipes joguem partidas ilimitadas. Basta remover este campo para restringir a reprodução novamente.
Dani herrera
Ah sim! obrigado, eu perdi isso e meu outro modelo pode ser um para um campo.
Pittfall 01/11/19
11
Eu acho que gosto mais da opção número 2. O único problema que tenho é que, sem dúvida, ele precisa de um formulário personalizado para o usuário "médio", em um mundo em que o administrador é usado como o FE. Infelizmente, eu moro nesse mundo. Mas acho que essa deve ser a resposta aceita. Obrigado!
Pittfall
A segunda opção é o caminho a percorrer. Esta é uma ótima resposta. @ Pitfall em relação ao administrador Eu adicionei uma resposta adicional. O formulário de administração não deve ser um grande problema para resolver.
Cezar
1

Não é uma resposta muito satisfatória, mas infelizmente a verdade é que não há como fazer o que você está descrevendo com um simples recurso embutido.

O que você descreveu cleanfuncionaria, mas você deve ter cuidado para chamá-lo manualmente, pois acho que ele é chamado automaticamente somente ao usar o ModelForm. Você pode criar uma restrição de banco de dados complexa, mas que residiria fora do Django e você precisaria lidar com exceções de banco de dados (o que pode ser difícil no Django quando no meio de uma transação).

Talvez haja uma maneira melhor de estruturar os dados?

Tim Tisdall
fonte
Sim, você está certo de que deve ser chamado manualmente e é por isso que não gostei da abordagem. Funciona apenas como eu quero no administrador, como você mencionou.
Pittfall 31/10/19
0

Já existe uma ótima resposta de dani herrera , mas desejo aprofundar a questão.

Conforme explicado na segunda opção, a solução, conforme exigido pelo OP, é alterar o design e implementar duas restrições exclusivas aos pares. A analogia com as partidas de basquete ilustra o problema de uma maneira muito prática.

Em vez de uma partida de basquete, uso exemplo em jogos de futebol (ou futebol). Um jogo de futebol (como eu chamo Event) é jogado por duas equipes (nos meus modelos, uma equipe Competitor). Essa é uma relação de muitos para muitos ( m:n), com nlimitado a dois nesse caso específico, o princípio é adequado para um número ilimitado.

Aqui está a aparência dos nossos modelos:

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

Um evento pode ser:

  • título: Taça Carabao, 4ª rodada,
  • local do evento: Anfield
  • hora: 30. outubro 2019, 19:30 GMT
  • participantes:
    • nome: Liverpool, cidade: Liverpool
    • nome: Arsenal, cidade: Londres

Agora temos que resolver o problema da questão. O Django cria automaticamente uma tabela intermediária entre os modelos com uma relação de muitos para muitos, mas podemos usar um modelo personalizado e adicionar mais campos. Eu chamo esse modelo Participant:

classe Participant (models.Model):
    ROLES = (
        ('H', 'Casa'),
        ('V', 'Visitante'),
    )
    event = models.ForeignKey (Evento, on_delete = models.CASCADE)
    concorrente = models.ForeignKey (Concorrente, on_delete = models.CASCADE)
    role = models.CharField (max_length = 1, escolhas = ROLES)

    classe Meta:
        unique_together = (
            ('evento', 'papel'),
            ('evento', 'concorrente'),
        )

    def __str __ (próprio):
        retornar '{} - {}'. formato (self.event, self.get_role_display ())

O ManyToManyFieldpossui uma opção throughque nos permite especificar o modelo intermediário. Vamos mudar isso no modelo 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

As restrições exclusivas agora limitarão automaticamente o número de competidores por evento a dois (porque existem apenas duas funções: Casa e Visitante ).

Em um evento específico (jogo de futebol), pode haver apenas um time em casa e apenas um time de visitantes. Um clube ( Competitor) pode aparecer como time da casa ou como visitante.

Como gerenciamos agora todas essas coisas no administrador? Como isso:

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)

Adicionamos o Participantinline no EventAdmin. Quando criamos novos Event, podemos escolher a equipe da casa e a equipe do visitante. A opção max_numlimita o número de entradas a 2, portanto, não é possível adicionar mais de 2 equipes por evento.

Isso pode ser refatorado para diferentes casos de uso. Digamos que nossos eventos são competições de natação e, em vez de casa e visitante, temos as faixas de 1 a 8. Apenas refatoramos o 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())

Com esta modificação, podemos ter este evento:

  • título: FINA 2019, final dos 50m costas masculina,

    • local do evento: Nambu University Municipal Aquatics Center
    • horário: 28. julho 2019, 20:02 UTC + 9
    • participantes:

      • nome: Michael Andrew, cidade: Edina, EUA, papel: pista 1
      • nome: Zane Waddell, cidade: Bloemfontein, África do Sul, papel: pista 2
      • nome: Evgeny Rylov, cidade: Novotroitsk, Rússia, papel: pista 3
      • nome: Kliment Kolesnikov, cidade: Moscou, Rússia, papel: pista 4

      // e assim por diante na faixa 5 à faixa 8 (fonte: Wikipedia

Um nadador pode aparecer apenas uma vez no calor e uma pista pode ser ocupada apenas uma vez no calor.

Coloquei o código no GitHub: https://github.com/cezar77/competition .

Mais uma vez, todos os créditos vão para dani herrera. Espero que esta resposta ofereça algum valor agregado aos leitores.

cezar
fonte