Existe-t-il un moyen de créer un identifiant unique sur 2 champs?

14

Voici mon modèle:

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

Essentiellement, ce que je veux, c'est other_modelêtre unique dans ce tableau. Cela signifie que s'il y a un enregistrement où se trouve other_model_oneid 123, je ne dois pas autoriser la création d'un autre enregistrement avec other_model_twoid as 123. Je peux remplacer, cleanje suppose, mais je me demandais si Django avait quelque chose de intégré.

J'utilise la version 2.2.5 avec PSQL.

Edit: Ce n'est pas une situation unqiue ensemble. Si j'ajoute un enregistrement avec other_model_one_id=1et autre other_model_two_id=2, je ne devrais pas pouvoir ajouter un autre enregistrement avec other_model_one_id=2et autreother_model_two_id=1

Pittfall
la source
Quelle version de Django utilisez-vous?
Willem Van Onsem
J'utilise la version 2.2.5
Pittfall
Copie possible de Django Unique Together (avec clés étrangères)
Toan Quoc Ho
1
Ce n'est pas une situation unique ensemble, c'est unique mais sur 2 champs si cela a du sens.
Pittfall

Réponses:

10

J'explique plusieurs options ici, peut-être que l'une d'entre elles ou une combinaison peut vous être utile.

Primordial save

Votre contrainte est une règle métier, vous pouvez remplacer la saveméthode pour garder les données cohérentes:


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)

Changer la conception

Je mets un échantillon facile à comprendre. Supposons ce scénario:

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

Maintenant, vous voulez éviter qu'une équipe ne joue un match avec elle-même, l'équipe A ne peut jouer avec l'équipe B qu'une seule fois (presque vos règles). Vous pouvez repenser vos modèles comme:

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

Cela ressemble à un problème symétrique , Django peut le gérer pour vous. Au lieu de créer un GroupedModelsmodèle, créez simplement un champ ManyToManyField avec lui-même sur OtherModel:

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

C'est ce que django a intégré à ces scénarios.

dani herrera
la source
La première approche est celle que j'utilisais (mais en espérant une contrainte de base de données). L'approche 2 est un peu différente en ce que dans mon scénario, si une équipe a joué à un jeu, elle ne pourra plus jamais jouer à nouveau. Je n'ai pas utilisé l'approche 3 car il y avait plus de données que je voulais stocker sur le regroupement. Merci d'avoir répondu.
Pittfall
si une équipe a joué à un jeu, elle ne pourra plus jamais jouer à nouveau. parce que je l'ai inclus match_idsur la contrainte unike, pour permettre aux équipes de jouer des matchs illimités. Supprimez simplement ce champ pour restreindre à nouveau la lecture.
dani herrera
Ah oui! merci je l'ai manqué et mon autre modèle pourrait être un champ un à un.
Pittfall
1
Je pense que j'aime mieux l'option numéro 2. Le seul problème que j'ai avec cela est qu'il a sans doute besoin d'un formulaire personnalisé pour l'utilisateur "moyen", dans un monde où l'administrateur est utilisé comme FE. Malheureusement, je vis dans ce monde. Mais je pense que cela devrait être la réponse acceptée. Merci!
Pittfall
La deuxième option est la voie à suivre. C'est une excellente réponse. @Pitfall concernant l'administrateur J'ai ajouté une autre réponse. Le formulaire d'administration ne devrait pas être un gros problème à résoudre.
cezar
1

Ce n'est pas une réponse très satisfaisante, mais malheureusement la vérité est qu'il n'y a aucun moyen de faire ce que vous décrivez avec une simple fonctionnalité intégrée.

Ce que vous avez décrit cleanfonctionnerait, mais vous devez faire attention à l'appeler manuellement car je pense qu'il n'est appelé automatiquement que lorsque vous utilisez ModelForm. Vous pourriez être en mesure de créer une contrainte de base de données complexe mais qui vivrait en dehors de Django et vous auriez à gérer les exceptions de base de données (ce qui peut être difficile dans Django au milieu d'une transaction).

Peut-être existe-t-il une meilleure façon de structurer les données?

Tim Tisdall
la source
Oui, vous avez raison, il faut l'appeler manuellement, c'est pourquoi je n'ai pas aimé l'approche. Cela ne fonctionne que comme je le souhaite dans l'administrateur, comme vous l'avez mentionné.
Pittfall
0

Il y a déjà une excellente réponse de la part de dani herrera , mais je souhaite en dire plus.

Comme expliqué dans la deuxième option, la solution requise par l'OP consiste à modifier la conception et à mettre en œuvre deux contraintes uniques par paire. L'analogie avec les matchs de basket-ball illustre le problème d'une manière très pratique.

Au lieu d'un match de basket, j'utilise l'exemple avec des matchs de football (ou de football). Un match de football (que j'appelle cela Event) est joué par deux équipes (dans mes modèles, une équipe est Competitor). Il s'agit d'une relation plusieurs-à-plusieurs ( m:n), nlimitée à deux dans ce cas particulier, le principe convient à un nombre illimité.

Voici à quoi ressemblent nos modèles:

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

Un événement pourrait être:

  • titre: Coupe Carabao, 4e tour,
  • lieu: Anfield
  • Heure: 30. octobre 2019, 19:30 GMT
  • participants:
    • nom: Liverpool, ville: Liverpool
    • nom: Arsenal, ville: London

Maintenant, nous devons résoudre le problème à partir de la question. Django crée automatiquement une table intermédiaire entre les modèles avec une relation plusieurs-à-plusieurs, mais nous pouvons utiliser un modèle personnalisé et ajouter d'autres champs. J'appelle ce modèle Participant:

Participant à la classe (models.Model):
    RÔLES = (
        («H», «Domicile»),
        («V», «Visiteur»),
    )
    event = models.ForeignKey (Event, on_delete = models.CASCADE)
    concurrent = models.ForeignKey (concurrent, on_delete = models.CASCADE)
    role = models.CharField (max_length = 1, choix = ROLES)

    classe Meta:
        unique_together = (
            («événement», «rôle»),
            («événement», «concurrent»),
        )

    def __str __ (auto):
        retourne '{} - {}'. format (self.event, self.get_role_display ())

Le ManyToManyFieldpossède une option throughqui nous permet de spécifier le modèle intermédiaire. Changeons cela dans le modèle 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

Les contraintes uniques limiteront désormais automatiquement le nombre de concurrents par événement à deux (car il n'y a que deux rôles: Domicile et visiteur ).

Dans un événement particulier (match de football), il ne peut y avoir qu'une seule équipe à domicile et une seule équipe de visiteurs. Un club (Competitor ) peut apparaître comme équipe à domicile ou comme équipe de visiteurs.

Comment gérons-nous maintenant toutes ces choses dans l'administrateur? Comme ça:

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)

Nous avons ajouté le Participantas inline dans le EventAdmin. Lorsque nous créons de nouveaux, Eventnous pouvons choisir l'équipe d'accueil et l'équipe de visiteurs. L'optionmax_num limite le nombre d'entrées à 2, donc pas plus de 2 équipes peuvent être ajoutées par événement.

Cela peut être refactorisé pour différents cas d'utilisation. Disons que nos événements sont des compétitions de natation et au lieu de la maison et du visiteur, nous avons les couloirs 1 à 8. Nous refactorisons simplement 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())

Avec cette modification, nous pouvons avoir cet événement:

  • titre: FINA 2019, finale du 50 m dos hommes,

    • lieu: Nambu University Municipal Aquatics Center
    • heure: 28. juillet 2019, 20:02 UTC + 9
    • participants:

      • nom: Michael Andrew, ville: Edina, USA, rôle: voie 1
      • nom: Zane Waddell, ville: Bloemfontein, Afrique du Sud, rôle: voie 2
      • nom: Evgeny Rylov, ville: Novotroitsk, Russie, rôle: voie 3
      • nom: Kliment Kolesnikov, ville: Moscou, Russie, rôle: voie 4

      // et ainsi de suite voie 5 à voie 8 (source: Wikipedia

Un nageur ne peut apparaître qu'une seule fois dans une manche et un couloir ne peut être occupé qu'une seule fois dans une manche.

J'ai mis le code sur GitHub: https://github.com/cezar77/competition .

Encore une fois, tous les crédits reviennent à dani herrera. J'espère que cette réponse apporte une valeur ajoutée aux lecteurs.

cezar
la source