Crear un modelo con dos claves foráneas opcionales, pero una obligatoria

9

Mi problema es que tengo un modelo que puede tomar una de las dos claves externas para decir qué tipo de modelo es. Quiero que tome al menos uno pero no los dos. ¿Puedo hacer que esto siga siendo un modelo o debería dividirlo en dos tipos? Aquí está el código:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    @classmethod
    def create(cls, groupid, siteid):
        inspection = cls(GroupID = groupid, SiteID = siteid)
        return inspection

    def __str__(self):
        return str(self.InspectionID)

class InspectionReport(models.Model):
    ReportID = models.AutoField(primary_key=True, unique=True)
    InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
    Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
    Comment = models.CharField(max_length=255, blank=True)
    Signature = models.CharField(max_length=255, blank=True)

El problema es el Inspectionmodelo. Esto debe estar vinculado a un grupo o un sitio, pero no a ambos. Actualmente con esta configuración necesita ambos.

Yo preferiría no tener que dividir esto en dos modelos casi idénticos GroupInspectiony SiteInspection, por lo que cualquier solución que lo mantiene como uno modelo sería ideal.

CalMac
fuente
Quizás usar subclases es mejor aquí. Puede hacer una Inspectionclase y luego subclase en SiteInspectiony GroupInspectionpara las partes no comunes.
Willem Van Onsem
Posiblemente no relacionado, pero la unique=Trueparte en sus campos de FK significa que solo Inspectionpuede existir una instancia para una determinada GroupIDo SiteIDinstancia: AHORA, es una relación uno a uno, no una para muchos. Es esto realmente lo que quieres ?
bruno desthuilliers
"Actualmente con esta configuración necesita ambos". => técnicamente, no lo hace: en el nivel de la base de datos, puede configurar ambas, una o ninguna de esas teclas (con la advertencia mencionada anteriormente). Es solo cuando se usa un ModelForm (directamente o mediante django admin) que esos campos se marcarán como necesarios, y eso es porque no pasó el argumento 'blank = True'.
bruno desthuilliers
@brunodesthuilliers Sí, la idea es tener Inspectionun vínculo entre Groupo Sitey an InspectionID, entonces puedo tener múltiples "inspecciones" en forma de InspectionReportesa relación. Esto se hizo para poder ordenar más fácilmente Datetodos los registros relacionados con uno Groupo Site. Espero que tenga sentido
CalMac
@ Cm0295 Me temo que no veo el punto de este nivel de indirección: poner los FK del grupo / sitio directamente en InspectionReport produce exactamente el mismo servicio AFAICT: filtre sus InspectionReports por la tecla apropiada (o simplemente siga el descriptor inverso del Sitio o Grupo), ordénelos por fecha y listo.
bruno desthuilliers

Respuestas:

5

Te sugiero que hagas tal validación a la manera de Django

anulando el cleanmétodo del modelo Django

class Inspection(models.Model):
    ...

    def clean(self):
        if <<<your condition>>>:
            raise ValidationError({
                    '<<<field_name>>>': _('Reason for validation error...etc'),
                })
        ...
    ...

Sin embargo, tenga en cuenta que, al igual que Model.full_clean (), el método clean () de un modelo no se invoca cuando llama al método save () de su modelo. debe llamarse manualmente para validar los datos del modelo, o puede anular el método de guardar del modelo para que siempre llame al método clean () antes de activar el Modelmétodo de guardar la clase


Otra solución que podría ayudar es usar GenericRelations , para proporcionar un campo polimórfico que se relacione con más de una tabla, pero puede ser el caso si estas tablas / objetos se pueden usar indistintamente en el diseño del sistema desde el primer lugar.

Radwan Abu-Odeh
fuente
2

Como se mencionó en los comentarios, la razón por la que "con esta configuración necesita ambos" es que olvidó agregar los blank=Truecampos FK, por lo que su ModelForm(ya sea uno personalizado o el predeterminado generado por el administrador) hará que el campo del formulario sea obligatorio . En el nivel de esquema de base de datos, puede completar ambos, ya sea uno o ninguno de esos FK, estaría bien ya que hizo que los campos de base de datos fueran nulos (con el null=Trueargumento).

Además, (vea mis otros comentarios), es posible que desee comprobar que realmente desea que los FK sean únicos. Esto técnicamente convierte su relación uno a muchos en una relación uno a uno: solo se le permite un único registro de 'inspección' para un GroupID o SiteId determinado (no puede tener dos o más 'inspección' para un GroupId o SiteId) . Si eso es REALMENTE lo que desea, es posible que desee utilizar un OneToOneField explícito (el esquema db será el mismo pero el modelo será más explícito y el descriptor relacionado será mucho más útil para este caso de uso).

Como nota al margen: en un modelo Django, un campo ForeignKey se materializa como una instancia de modelo relacionada, no como una identificación sin formato. IOW, dado esto:

class Foo(models.Model):
    name = models.TextField()

class Bar(models.Model):
    foo = models.ForeignKey(Foo)


foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)

entonces bar.fooresolverá a foo, no a foo.id. Por lo tanto, sin duda desea cambiar el nombre de sus campos InspectionIDy SiteIDa apropiado inspectiony site. Por cierto, en Python, la convención de nomenclatura es 'all_lower_with_underscores' para cualquier otra cosa que no sean nombres de clase y pseudo-constantes.

Ahora para su pregunta central: no hay una forma SQL estándar específica de imponer una restricción "uno u otro" en el nivel de la base de datos, por lo que generalmente se hace usando una restricción CHECK , que se realiza en un modelo Django con las meta "restricciones" del modelo opción .

Dicho esto, la forma en que las restricciones son realmente compatibles y aplicadas en el nivel de base de datos depende de su proveedor de base de datos (MySQL <8.0.16 simplemente ignórelas , por ejemplo), y el tipo de restricción que necesitará aquí no se aplicará en el formulario o validación a nivel de modelo , solo cuando se intenta guardar el modelo, por lo que también desea agregar validación a nivel de modelo (preferiblemente) o validación a nivel de formulario, en ambos casos en el modelo (resp.) o clean()método de formulario .

Para resumir una larga historia:

  • primero verifique que realmente desea esta unique=Truerestricción, y en caso afirmativo, reemplace su campo FK con OneToOneField.

  • agregue un blank=Trueargumento a sus dos campos FK (o OneToOne)

  • agregue la restricción de verificación adecuada en el meta de su modelo: el documento es breve pero aún lo suficientemente explícito si sabe hacer consultas complejas con el ORM (y si no lo hace, es hora de aprender ;-))
  • agregue un clean()método a su modelo que verifique que tenga uno u otro campo y genere un error de validación

y debería estar bien, suponiendo que su RDBMS respete las restricciones de verificación, por supuesto.

Solo tenga en cuenta que, con este diseño, su Inspectionmodelo es una indirección totalmente inútil (¡pero costosa!): Obtendrá exactamente las mismas características a un costo menor al mover los FK (y las restricciones, la validación, etc.) directamente InspectionReport.

Ahora puede haber otra solución: mantener el modelo de inspección, pero colocar el FK como OneToOneField en el otro extremo de la relación (en el sitio y el grupo):

class Inspection(models.Model):
    id = models.AutoField(primary_key=True) # a pk is always unique !

class InspectionReport(models.Model):
    # you actually don't need to manually specify a PK field,
    # Django will provide one for you if you don't
    # id = models.AutoField(primary_key=True)

    inspection = ForeignKey(Inspection, ...)
    date = models.DateField(null=True) # you should have a default then
    comment = models.CharField(max_length=255, blank=True default="")
    signature = models.CharField(max_length=255, blank=True, default="")


class Group(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

class Site(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

Y luego puede obtener todos los informes de un sitio o grupo determinado con yoursite.inspection.inspectionreport_set.all().

Esto evita tener que agregar ninguna restricción o validación específica, pero a costa de un nivel de indirección adicional ( joincláusula SQL, etc.).

Cuál de esas soluciones sería "la mejor" depende realmente del contexto, por lo que debe comprender las implicaciones de ambas y verificar cómo usa típicamente sus modelos para descubrir cuál es más apropiado para sus propias necesidades. En lo que a mí respecta y sin más contexto (o dudas) prefiero usar la solución con los niveles de indirección menos, pero YMMV.

NB con respecto a las relaciones genéricas: pueden ser útiles cuando realmente tiene muchos posibles modelos relacionados y / o no sabe de antemano qué modelos querrá relacionar con los suyos. Esto es especialmente útil para aplicaciones reutilizables (piense en "comentarios" o "etiquetas", etc.) o extensibles (marcos de administración de contenido, etc.). La desventaja es que hace que las consultas sean mucho más pesadas (y bastante poco prácticas cuando quieres hacer consultas manuales en tu base de datos). Por experiencia, pueden convertirse rápidamente en un código / error y rendimiento de bots de PITA, por lo que es mejor mantenerlos para cuando no haya una mejor solución (y / o cuando la sobrecarga de mantenimiento y tiempo de ejecución no sea un problema).

Mis 2 centavos

bruno desthuilliers
fuente
2

Django tiene una nueva interfaz (desde 2.2) para crear restricciones de DB: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Puede usar a CheckConstraintpara forzar one-and-only-one no es nulo. Yo uso dos para mayor claridad:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
    SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(SiteID=None) | ~Q(GroupId=None),
                name='at_least_1_non_null'),
            ),
            models.CheckConstraint(
                check=Q(SiteID=None) | Q(GroupId=None),
                name='at_least_1_null'),
            ),
        ]

Esto solo impondrá la restricción en el nivel de base de datos. Deberá validar las entradas en sus formularios o serializadores manualmente.

Como nota al margen, probablemente debería usar en OneToOneFieldlugar de ForeignKey(unique=True). También querrá blank=True.

Jonathan Richards
fuente
0

Creo que estás hablando de relaciones genéricas , documentos . Tu respuesta es similar a esta .

Hace algún tiempo necesitaba usar relaciones genéricas, pero leí en un libro y en otro lugar que debería evitarse el uso, creo que fueron dos cucharadas de Django.

Terminé creando un modelo como este:

class GroupInspection(models.Model):
    InspectionID = models.ForeignKey..
    GroupID = models.ForeignKey..

class SiteInspection(models.Model):
    InspectionID = models.ForeignKey..
    SiteID = models.ForeignKey..

No estoy seguro de si es una buena solución y, como mencionaste, preferirías no usarla, pero esto funciona en mi caso.

Luis Silva
fuente
"Leí en un libro y en otro lugar" es la peor razón posible para hacer (o evitar) hacer algo.
bruno desthuilliers
@brunodesthuilliers Pensé que Two Scoops of Django era un buen libro.
Luis Silva
No puedo decir, no lo he leído. Pero eso no está relacionado: mi punto es que si no entiendes por qué el libro lo dice, no es conocimiento ni experiencia, es creencia religiosa. No me importa la creencia religiosa cuando se trata de religión, pero no tienen cabida en CS. O entiendes cuáles son los pros y los contras de alguna característica y luego puedes juzgar si es apropiado en un contexto dado , o no lo entiendes y luego no debes decir sin pensar lo que has leído. Existen casos de uso muy válidos para las relaciones genéricas, el punto no es evitarlos en absoluto, sino saber cuándo evitarlos.
bruno desthuilliers
Nota: entiendo perfectamente que no se puede saber todo sobre CS: hay dominios en los que no tengo otras opciones que confiar en algún libro. Pero probablemente no responderé preguntas sobre ese tema ;-)
bruno desthuilliers
0

Puede que sea tarde para responder a su pregunta, pero pensé que mi solución podría ajustarse al caso de otra persona.

Crearía un nuevo modelo, llamémoslo Dependency, y aplicaría la lógica en ese modelo.

class Dependency(models.Model):
    Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

Entonces escribiría la lógica para que sea aplicable de manera muy explícita.

class Dependency(models.Model):
    group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    _is_from_custom_logic = False

    @classmethod
    def create_dependency_object(cls, group=None, site=None):
        # you can apply any conditions here and prioritize the provided args
        cls._is_from_custom_logic = True
        if group:
            _new = cls.objects.create(group=group)
        elif site:
            _new = cls.objects.create(site=site)
        else:
            raise ValueError('')
        return _new

    def save(self, *args, **kwargs):
        if not self._is_from_custom_logic:
            raise Exception('')
        return super().save(*args, **kwargs)

Ahora solo necesita crear un single ForeignKeypara su Inspectionmodelo.

En sus viewfunciones, debe crear un Dependencyobjeto y luego asignarlo a su Inspectionregistro. Asegúrese de usarlo create_dependency_objecten sus viewfunciones.

Esto hace que su código sea explícito y a prueba de errores. La aplicación de la ley puede pasarse por alto con demasiada facilidad. Pero el punto es que necesita conocimiento previo a esta limitación exacta para ser ignorado.

nima
fuente