¿Hay alguna manera de crear una identificación única en 2 campos?

14

Aquí está mi modelo:

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

Esencialmente, lo que quiero es other_modelque sea único en esta tabla. Eso significa que si hay un registro donde other_model_oneestá la identificación 123, no debería permitir que se cree otro registro con la other_model_twoidentificación como 123. Supongo que puedo anularlo clean, pero me preguntaba si django tiene algo incorporado.

Estoy usando la versión 2.2.5 con PSQL.

Editar: Esta no es una situación única. Si agrego un registro con other_model_one_id=1y otro other_model_two_id=2, no debería poder agregar otro registro con other_model_one_id=2y otroother_model_two_id=1

Pittfall
fuente
¿Qué versión de Django estás usando?
Willem Van Onsem
Estoy usando la versión 2.2.5
Pittfall
Posible duplicado de Django Unique Together (con claves foráneas)
Toan Quoc Ho
1
Esta no es una situación única, es única, pero tiene más de 2 campos si tiene sentido.
Pittfall

Respuestas:

10

Aquí explico varias opciones, tal vez una de ellas o una combinación puede ser útil para usted.

Primordial save

Su restricción es una regla de negocio, puede anular el savemétodo para mantener los datos 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)

Cambiar diseño

Puse una muestra fácil de entender. Supongamos este escenario:

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

Ahora, quieres evitar que un equipo juegue un partido consigo mismo y el equipo A solo puede jugar con el equipo B por una vez (casi tus reglas). Puede rediseñar sus 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

Esto parece un problema simétrico , django puede manejarlo por usted. En lugar de crear un GroupedModelsmodelo, simplemente haga un campo ManyToManyField consigo mismo en OtherModel:

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

Esto es lo que django tiene incorporado para estos escenarios.

dani herrera
fuente
El primer enfoque es el que estaba usando (pero esperando una restricción de la base de datos). El Enfoque 2 es un poco diferente en que en mi escenario, si un equipo ha jugado un juego, nunca podrá volver a jugarlo. No utilicé el enfoque 3 porque había más datos que quería almacenar en la agrupación. Gracias por la respuesta.
Pittfall
Si un equipo ha jugado un juego, nunca podrá volver a jugarlo. porque esto lo match_idincluí en la restricción de unike, para permitir que los equipos jueguen partidos ilimitados. Simplemente elimine este campo para restringir el juego nuevamente.
dani herrera
¡Ah, sí! gracias, me perdí eso y mi otro modelo podría ser uno a uno.
Pittfall, el
1
Creo que me gusta más la opción número 2. El único problema que tengo con él es que posiblemente necesita un formulario personalizado para el usuario "promedio", en un mundo donde el administrador se utiliza como FE. Lamentablemente, vivo en ese mundo. Pero creo que esta debería ser la respuesta aceptada. ¡Gracias!
Pittfall, el
La segunda opción es el camino a seguir. Esta es una respuesta genial. @Pitfall con respecto al administrador He agregado una respuesta adicional. El formulario de administrador no debería ser un gran problema para resolver.
cezar
1

No es una respuesta muy satisfactoria, pero desafortunadamente la verdad es que no hay forma de hacer lo que está describiendo con una característica incorporada simple.

Lo que describiste cleanfuncionaría, pero debes tener cuidado de llamarlo manualmente, ya que creo que solo se llama automáticamente al usar ModelForm. Es posible que pueda crear una restricción de base de datos compleja, pero que viviría fuera de Django y tendría que manejar las excepciones de la base de datos (lo que puede ser difícil en Django cuando está en medio de una transacción).

¿Quizás haya una mejor manera de estructurar los datos?

Tim Tisdall
fuente
Sí, tiene razón en que debe llamarse manualmente, por eso no me gustó el enfoque. Solo funciona como quiero en el administrador, como mencionaste.
Pittfall
0

Ya hay una gran respuesta de dani herrera , sin embargo, deseo dar más detalles al respecto.

Como se explica en la segunda opción, la solución requerida por el OP es cambiar el diseño e implementar dos restricciones únicas por pares. La analogía con los partidos de baloncesto ilustra el problema de una manera muy práctica.

En lugar de un partido de baloncesto, utilizo ejemplos con juegos de fútbol (o fútbol). Un juego de fútbol (que yo llamo Event) es jugado por dos equipos (en mis modelos es un equipo Competitor). Esta es una relación de muchos a muchos ( m:n), con un nlímite de dos en este caso particular, el principio es adecuado para un número ilimitado.

Así es como se ven nuestros 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

Un evento podría ser:

  • título: Copa Carabao, cuarta ronda,
  • lugar: Anfield
  • hora: 30. octubre 2019, 19:30 GMT
  • Participantes:
    • nombre: Liverpool, ciudad: Liverpool
    • nombre: Arsenal, ciudad: Londres

Ahora tenemos que resolver el problema a partir de la pregunta. Django crea automáticamente una tabla intermedia entre los modelos con una relación de muchos a muchos, pero podemos usar un modelo personalizado y agregar más campos. Yo llamo a ese modelo Participant:

Participante de clase (modelos.Modelo):
    ROLES = (
        ('H', 'Inicio'),
        ('V', 'Visitante'),
    )
    event = models.ForeignKey (Evento, on_delete = models.CASCADE)
    competitor = models.ForeignKey (Competitor, on_delete = models.CASCADE)
    role = models.CharField (max_length = 1, options = ROLES)

    clase Meta:
        unique_together = (
            ('evento', 'rol'),
            ('evento', 'competidor'),
        )

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

El ManyToManyFieldtiene una opción throughque nos permite especificar el modelo intermedio. Cambiemos eso en el 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

Las restricciones únicas ahora limitarán automáticamente el número de competidores por evento a dos (porque solo hay dos roles: Hogar y Visitante ).

En un evento particular (juego de fútbol) solo puede haber un equipo local y solo un equipo visitante. Un club ( Competitor) puede aparecer como equipo local o como equipo visitante.

¿Cómo gestionamos ahora todas estas cosas en el administrador? Me gusta esto:

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)

Hemos agregado Participantcomo en línea en el EventAdmin. Cuando creamos nuevos Event, podemos elegir el equipo local y el equipo visitante. La opción max_numlimita el número de entradas a 2, por lo tanto, no se pueden agregar más de 2 equipos por evento.

Esto se puede refactorizar para diferentes casos de uso. Digamos que nuestros eventos son competiciones de natación y, en lugar de casa y visitante, tenemos carriles 1 a 8. Simplemente refactorizamos 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())

Con esta modificación podemos tener este evento:

  • título: FINA 2019, final masculino de espalda de 50 m,

    • Lugar: Centro de Acuáticos Municipales de la Universidad de Nambu
    • hora: 28 de julio de 2019, 20:02 UTC + 9
    • Participantes:

      • nombre: Michael Andrew, ciudad: Edina, EE. UU., función: carril 1
      • nombre: Zane Waddell, ciudad: Bloemfontein, Sudáfrica, función: carril 2
      • nombre: Evgeny Rylov, ciudad: Novotroitsk, Rusia, función: carril 3
      • nombre: Kliment Kolesnikov, ciudad: Moscú, Rusia, función: carril 4

      // y así sucesivamente del carril 5 al carril 8 (fuente: Wikipedia

Un nadador puede aparecer solo una vez en celo, y un carril puede estar ocupado solo una vez en celo.

Puse el código en GitHub: https://github.com/cezar77/competition .

Una vez más, todos los créditos van a Dani Herrera. Espero que esta respuesta brinde algún valor agregado a los lectores.

cezar
fuente