Criando um modelo com duas chaves estrangeiras opcionais, mas uma obrigatória

9

Meu problema é que tenho um modelo que pode usar uma das duas chaves estrangeiras para dizer que tipo de modelo é. Quero que leve pelo menos um, mas não ambos. Ainda posso ter um modelo ou devo dividi-lo em dois tipos. Aqui está o 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)

O problema é o Inspectionmodelo. Isso deve estar vinculado a um grupo ou site, mas não a ambos. Atualmente, com essa configuração, ele precisa de ambos.

Prefiro não ter que dividir isso em dois modelos quase idênticos GroupInspectione SiteInspection, portanto, qualquer solução que o mantenha como um modelo seria o ideal.

CalMac
fonte
Talvez o uso de subclassificação seja melhor aqui. Você pode criar uma Inspectionclasse e depois subclassar para SiteInspectione GroupInspectionpara as partes não comuns.
Willem Van Onsem
Possivelmente não relacionado, mas a unique=Trueparte em seus campos FK significa que apenas uma Inspectioninstância pode existir para uma determinada GroupIDou SiteIDinstância - IOW, é um relacionamento individual, não um para muitos. É mesmo isto que queres ?
bruno desthuilliers 14/01
"Atualmente, com esta configuração, ele precisa de ambos." => tecnicamente, não - no nível do banco de dados, você pode definir ambas, uma ou nenhuma dessas chaves (com a ressalva mencionada acima). É somente ao usar um ModelForm (diretamente ou via administrador do django) que esses campos serão marcados conforme necessário, e é porque você não passou o argumento 'blank = True'.
bruno desthuilliers 14/01
@brunodesthuilliers Sim, a idéia é ter Inspectionum elo entre o Groupor Sitee um InspectionID, para que eu possa ter várias "inspeções" na forma de InspectionReportum único relacionamento. Isso foi feito para que eu possa classificar mais facilmente Datetodos os registros relacionados a um Groupou Site. Espero que faça sentido
CalMac
@ Cm0295 Receio não ver o objetivo desse nível de indireção - colocar os FKs do grupo / site diretamente no InspectionReport gera exatamente o mesmo serviço AFAICT - filtre seus InspectionReports pela chave apropriada (ou siga o descritor reverso do Site ou Grupo), classifique-os por data e pronto.
bruno desthuilliers 14/01

Respostas:

5

Eu sugiro que você faça essa validação da maneira do Django

substituindo o cleanmétodo do Django Model

class Inspection(models.Model):
    ...

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

Observe, no entanto, que, como Model.full_clean (), o método clean () de um modelo não é chamado quando você chama o método save () do seu modelo. ele precisa ser chamado manualmente para validar os dados do modelo, ou você pode substituir o método save do modelo para fazer sempre chamar o método clean () antes de acionar o Modelmétodo save da classe


Outra solução que pode ajudar é usar o GenericRelations , a fim de fornecer um campo polimórfico que se relaciona com mais de uma tabela, mas pode ser o caso se essas tabelas / objetos puderem ser usados ​​de forma intercambiável no design do sistema desde o primeiro lugar.

Radwan Abu-Odeh
fonte
2

Como mencionado nos comentários, a razão pela qual "com essa configuração precisa de ambos" é apenas porque você esqueceu de adicionar os blank=Truecampos aos seus FK; portanto, seu ModelForm(personalizado ou o padrão gerado pelo administrador) fará com que o campo de formulário seja obrigatório . No nível do esquema db, você pode preencher ambos, um ou nenhum desses FKs, tudo bem, pois você tornou esses campos db anuláveis ​​(com o null=Trueargumento).

Além disso, (veja meus outros comentários), você pode querer verificar se realmente deseja que os FKs sejam únicos. Tecnicamente, isso transforma seu relacionamento de um para muitos em um para um - você só pode ter um único registro de 'inspeção' para um determinado GroupID ou SiteId (você não pode ter duas ou mais 'inspeção' para um GroupId ou SiteId) . Se isso é REALMENTE o que você deseja, convém usar um OneToOneField explícito (o esquema db será o mesmo, mas o modelo será mais explícito e o descritor relacionado muito mais utilizável para esse caso de uso).

Como uma observação lateral: em um Django Model, um campo ForeignKey se materializa como uma instância de modelo relacionada, não como um ID bruto. IOW, dado o seguinte:

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)

então bar.fooresolverá foo, não foo.id. Então, você certamente deseja renomear seus campos InspectionIDe SiteIDpara apropriado inspectione site. BTW, em Python, a convenção de nomenclatura é 'all_lower_with_underscores' para qualquer outra coisa além de nomes de classe e pseudo-constantes.

Agora, a sua pergunta principal: não existe uma maneira padrão específica do SQL de impor uma restrição "uma ou outra" no nível do banco de dados; portanto, isso geralmente é feito com a restrição CHECK , feita em um modelo Django com as meta "restrições" do modelo. opção .

Dito isto, como as restrições são realmente suportadas e aplicadas no nível db depende do seu fornecedor de banco de dados (o MySQL <8.0.16 simplesmente as ignora por exemplo), e o tipo de restrição que você precisará aqui não será aplicado no formato ou validação no nível do modelo , apenas ao tentar salvar o modelo, portanto, você também deseja adicionar a validação no nível do modelo (preferencialmente) ou no nível do formulário, nos dois casos no modelo (resp.) ou no clean()método do formulário .

Então, para resumir uma longa história:

  • primeiro verifique se você realmente deseja essa unique=Truerestrição e, se sim, substitua seu campo FK por um OneToOneField.

  • adicione um blank=Trueargumento aos campos FK (ou OneToOne)

  • adicione a restrição de verificação adequada na meta do seu modelo - o documento é sucinto, mas ainda suficientemente explícito se você souber fazer consultas complexas com o ORM (e se não o fizer, é hora de aprender ;-))
  • adicione um clean()método ao seu modelo que verifique se você tem um ou outro campo e gera um erro de validação

e você deve estar bem, supondo que seu RDBMS respeite as restrições de verificação, é claro.

Observe que, com esse design, seu Inspectionmodelo é um indireto totalmente inútil (mas caro!) - você obteria exatamente os mesmos recursos a um custo menor movendo os FKs (e restrições, validação etc.) diretamente para InspectionReport.

Agora pode haver outra solução - mantenha o modelo de Inspeção, mas coloque o FK como OneToOneField na outra extremidade do relacionamento (no Site e no 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)

E então você pode obter todos os relatórios de um determinado site ou grupo com yoursite.inspection.inspectionreport_set.all().

Isso evita a necessidade de adicionar qualquer restrição ou validação específica, mas ao custo de um nível de indireção adicional ( joincláusula SQL , etc.).

Qual dessas soluções seria "a melhor" é realmente dependente do contexto; portanto, você precisa entender as implicações de ambas e verificar como costuma usar seus modelos para descobrir qual é mais apropriada para suas próprias necessidades. No que me diz respeito e sem mais contexto (ou dúvida), prefiro usar a solução com os níveis menos indiretos, mas YMMV.

Nota: em relação às relações genéricas: essas podem ser úteis quando você realmente possui muitos modelos possíveis e / ou não sabe de antemão quais modelos você deseja relacionar aos seus. Isso é especialmente útil para aplicativos reutilizáveis ​​(pense em "comentários" ou "tags" etc.) ou extensíveis (estruturas de gerenciamento de conteúdo etc.). A desvantagem é que torna a consulta muito mais pesada (e pouco prática quando você deseja fazer consultas manuais no seu banco de dados). Por experiência, eles podem se tornar rapidamente um código / perf e código de bot da PITA, então é melhor mantê-los quando não houver uma solução melhor (e / ou quando a sobrecarga de manutenção e tempo de execução não for um problema).

Meus 2 centavos.

bruno desthuilliers
fonte
2

O Django tem uma nova interface (desde a 2.2) para criar restrições de banco de dados: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Você pode usar a CheckConstraintpara impor um e apenas um não é nulo. Eu uso dois para maior clareza:

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'),
            ),
        ]

Isso apenas aplicará a restrição no nível do banco de dados. Você precisará validar entradas em seus formulários ou serializadores manualmente.

Como uma nota lateral, você provavelmente deve usar em OneToOneFieldvez de ForeignKey(unique=True). Você também vai querer blank=True.

Jonathan Richards
fonte
0

Eu acho que você está falando sobre relações genéricas , documentos . Sua resposta é semelhante a esta .

Algum tempo atrás, eu precisava usar relações genéricas, mas li em um livro e em algum outro lugar que o uso deveria ser evitado, acho que era o Two Scoops of Django.

Acabei criando um modelo como este:

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

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

Não tenho certeza se é uma boa solução e, como você mencionou, prefere não usá-la, mas isso funciona no meu caso.

Luis Silva
fonte
"Eu li em um livro e em outro lugar" é o pior motivo possível para fazer (ou evitar fazer) alguma coisa.
bruno desthuilliers 14/01
@brunodesthuilliers Eu pensei que Two Scoops of Django era um bom livro.
Luis Silva
Não posso dizer, eu não li. Mas isso não tem relação: meu argumento é que, se você não entende por que o livro diz isso, não é conhecimento nem experiência, é crença religiosa. Não me importo com a crença religiosa quando se trata de religião, mas eles não têm lugar no CS. Ou você entende quais são os prós e os contras de algum recurso e pode julgar se é apropriado em um determinado contexto ou não, e não deve imitar o papagaio sem pensar no que leu. Há casos de uso muito válidos para relações genéricas, o objetivo não é evitá-las, mas saber quando evitá-las.
bruno desthuilliers 14/01
NB: Entendo perfeitamente que não se pode saber tudo sobre o CS - há domínios em que não tenho outras opções além de confiar em algum livro. Mas então provavelmente não responderei perguntas sobre esse tópico ;-)
bruno desthuilliers 14/01
0

Pode ser que seja tarde para responder à sua pergunta, mas achei que minha solução poderia se encaixar no caso de outra pessoa.

Eu criaria um novo modelo, vamos chamá-lo Dependencye aplicaria a lógica nesse 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)

Então eu escreveria a lógica para ser aplicável de maneira muito 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)

Agora você só precisa criar um único ForeignKeypara o seu Inspectionmodelo.

Em suas viewfunções, você precisa criar um Dependencyobjeto e atribuí-lo ao seu Inspectionregistro. Certifique-se de usar create_dependency_objectsuas viewfunções.

Isso praticamente torna seu código explícito e à prova de erros. A imposição pode ser ignorada com muita facilidade. Mas o ponto é que ele precisa de conhecimento prévio dessa limitação exata para ser contornado.

nima
fonte