Campos de modelo dinâmico do Django

161

Estou trabalhando em um aplicativo multilocatário no qual alguns usuários podem definir seus próprios campos de dados (por meio do administrador) para coletar dados adicionais em formulários e reportar os dados. O último bit faz do JSONField não uma ótima opção, então, em vez disso, tenho a seguinte solução:

class CustomDataField(models.Model):
    """
    Abstract specification for arbitrary data fields.
    Not used for holding data itself, but metadata about the fields.
    """
    site = models.ForeignKey(Site, default=settings.SITE_ID)
    name = models.CharField(max_length=64)

    class Meta:
        abstract = True

class CustomDataValue(models.Model):
    """
    Abstract specification for arbitrary data.
    """
    value = models.CharField(max_length=1024)

    class Meta:
        abstract = True

Observe como CustomDataField possui uma ForeignKey no site - cada site terá um conjunto diferente de campos de dados personalizados, mas use o mesmo banco de dados. Em seguida, os vários campos de dados concretos podem ser definidos como:

class UserCustomDataField(CustomDataField):
    pass

class UserCustomDataValue(CustomDataValue):
    custom_field = models.ForeignKey(UserCustomDataField)
    user = models.ForeignKey(User, related_name='custom_data')

    class Meta:
        unique_together=(('user','custom_field'),)

Isso leva ao seguinte uso:

custom_field = UserCustomDataField.objects.create(name='zodiac', site=my_site) #probably created in the admin
user = User.objects.create(username='foo')
user_sign = UserCustomDataValue(custom_field=custom_field, user=user, data='Libra')
user.custom_data.add(user_sign) #actually, what does this even do?

Mas isso parece muito desajeitado, principalmente com a necessidade de criar manualmente os dados relacionados e associá-los ao modelo concreto. Existe uma abordagem melhor?

Opções que foram descartadas preventivamente:

  • SQL personalizado para modificar tabelas on-the-fly. Em parte porque isso não vai ser dimensionado e em parte porque é um hack demais.
  • Soluções sem esquema, como NoSQL. Não tenho nada contra eles, mas eles ainda não se encaixam. Por fim, esses dados são digitados e existe a possibilidade de usar um aplicativo de relatório de terceiros.
  • JSONField, conforme listado acima, pois não funcionará bem com consultas.
GDorn
fonte
6
De preferência, essa não é uma das perguntas: stackoverflow.com/questions/7801729/… stackoverflow.com/questions/2854656/…
GDorn

Respostas:

277

Atualmente, existem quatro abordagens disponíveis, duas delas exigindo um certo back-end de armazenamento:

  1. Django-eav (o pacote original não é mais mantido, mas possui alguns garfos prósperos )

    Esta solução é baseada no modelo de dados Entity Attribute Value , essencialmente, usa várias tabelas para armazenar atributos dinâmicos de objetos. Grandes partes dessa solução é que ela:

    • usa vários modelos Django puros e simples para representar campos dinâmicos, o que facilita o entendimento e a agnóstico de banco de dados;
    • permite conectar / desanexar efetivamente o armazenamento de atributos dinâmicos ao modelo do Django com comandos simples como:

      eav.unregister(Encounter)
      eav.register(Patient)
    • Integra-se perfeitamente com o administrador do Django ;

    • Ao mesmo tempo, é realmente poderoso.

    Desvantagens:

    • Não é muito eficiente. Isso é mais uma crítica ao próprio padrão EAV, que requer mesclar manualmente os dados de um formato de coluna para um conjunto de pares de valores-chave no modelo.
    • Mais difícil de manter. A manutenção da integridade dos dados requer uma restrição de chave exclusiva de várias colunas, que pode ser ineficiente em alguns bancos de dados.
    • Você precisará selecionar um dos garfos , já que o pacote oficial não é mais mantido e não há um líder claro.

    O uso é bem direto:

    import eav
    from app.models import Patient, Encounter
    
    eav.register(Encounter)
    eav.register(Patient)
    Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
    Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
    Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
    Attribute.objects.create(name='city', datatype=Attribute.TYPE_TEXT)
    Attribute.objects.create(name='country', datatype=Attribute.TYPE_TEXT)
    
    self.yes = EnumValue.objects.create(value='yes')
    self.no = EnumValue.objects.create(value='no')
    self.unkown = EnumValue.objects.create(value='unkown')
    ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
    ynu.enums.add(self.yes)
    ynu.enums.add(self.no)
    ynu.enums.add(self.unkown)
    
    Attribute.objects.create(name='fever', datatype=Attribute.TYPE_ENUM,\
                                           enum_group=ynu)
    
    # When you register a model within EAV,
    # you can access all of EAV attributes:
    
    Patient.objects.create(name='Bob', eav__age=12,
                               eav__fever=no, eav__city='New York',
                               eav__country='USA')
    # You can filter queries based on their EAV fields:
    
    query1 = Patient.objects.filter(Q(eav__city__contains='Y'))
    query2 = Q(eav__city__contains='Y') |  Q(eav__fever=no)
  2. Campos Hstore, JSON ou JSONB no PostgreSQL

    O PostgreSQL suporta vários tipos de dados mais complexos. A maioria é suportada por pacotes de terceiros, mas nos últimos anos o Django os adotou no django.contrib.postgres.fields.

    HStoreField :

    O Django-hstore era originalmente um pacote de terceiros, mas o Django 1.8 adicionou o HStoreField como um built-in, junto com vários outros tipos de campos suportados pelo PostgreSQL.

    Essa abordagem é boa no sentido de permitir que você tenha o melhor dos dois mundos: campos dinâmicos e banco de dados relacional. No entanto, o hstore não é ideal em termos de desempenho , especialmente se você acabar armazenando milhares de itens em um campo. Ele também suporta apenas cadeias de valores.

    #app/models.py
    from django.contrib.postgres.fields import HStoreField
    class Something(models.Model):
        name = models.CharField(max_length=32)
        data = models.HStoreField(db_index=True)

    No shell do Django você pode usá-lo assim:

    >>> instance = Something.objects.create(
                     name='something',
                     data={'a': '1', 'b': '2'}
               )
    >>> instance.data['a']
    '1'        
    >>> empty = Something.objects.create(name='empty')
    >>> empty.data
    {}
    >>> empty.data['a'] = '1'
    >>> empty.save()
    >>> Something.objects.get(name='something').data['a']
    '1'

    Você pode emitir consultas indexadas nos campos hstore:

    # equivalence
    Something.objects.filter(data={'a': '1', 'b': '2'})
    
    # subset by key/value mapping
    Something.objects.filter(data__a='1')
    
    # subset by list of keys
    Something.objects.filter(data__has_keys=['a', 'b'])
    
    # subset by single key
    Something.objects.filter(data__has_key='a')    

    JSONField :

    Os campos JSON / JSONB suportam qualquer tipo de dados codificado em JSON, não apenas pares de chave / valor, mas também tendem a ser mais rápidos e (para JSONB) mais compactos que o Hstore. Vários pacotes implementam campos JSON / JSONB, incluindo django-pgfields , mas a partir do Django 1.9, o JSONField é um built-in usando JSONB para armazenamento. O JSONField é semelhante ao HStoreField e pode ter um desempenho melhor em dicionários grandes. Ele também suporta outros tipos de strings, como números inteiros, booleanos e dicionários aninhados.

    #app/models.py
    from django.contrib.postgres.fields import JSONField
    class Something(models.Model):
        name = models.CharField(max_length=32)
        data = JSONField(db_index=True)

    Criando no shell:

    >>> instance = Something.objects.create(
                     name='something',
                     data={'a': 1, 'b': 2, 'nested': {'c':3}}
               )

    As consultas indexadas são quase idênticas ao HStoreField, exceto que o aninhamento é possível. Índices complexos podem exigir criação manual (ou uma migração com script).

    >>> Something.objects.filter(data__a=1)
    >>> Something.objects.filter(data__nested__c=3)
    >>> Something.objects.filter(data__has_key='a')
  3. Django MongoDB

    Ou outras adaptações NoSQL Django - com elas você pode ter modelos totalmente dinâmicos.

    As bibliotecas NoSQL Django são ótimas, mas lembre-se de que elas não são 100% compatíveis com o Django, por exemplo, para migrar para o Django-nonrel a partir do Django padrão, você precisará substituir ManyToMany por ListField, entre outras coisas.

    Veja este exemplo do Django MongoDB:

    from djangotoolbox.fields import DictField
    
    class Image(models.Model):
        exif = DictField()
    ...
    
    >>> image = Image.objects.create(exif=get_exif_data(...))
    >>> image.exif
    {u'camera_model' : 'Spamcams 4242', 'exposure_time' : 0.3, ...}

    Você pode até criar listas incorporadas de qualquer modelo do Django:

    class Container(models.Model):
        stuff = ListField(EmbeddedModelField())
    
    class FooModel(models.Model):
        foo = models.IntegerField()
    
    class BarModel(models.Model):
        bar = models.CharField()
    ...
    
    >>> Container.objects.create(
        stuff=[FooModel(foo=42), BarModel(bar='spam')]
    )
  4. Django-mutant: Modelos dinâmicos baseados em syncdb e South-hooks

    O Django-mutant implementa campos de Chave Estrangeira e m2m totalmente dinâmicos. E é inspirado em soluções incríveis, mas um tanto hackistas, de Will Hardy e Michael Hall.

    Tudo isso é baseado nos ganchos do Django Sul, que, de acordo com a palestra de Will Hardy no DjangoCon 2011 (assista!) São, no entanto, robustos e testados em produção ( código fonte relevante ).

    O primeiro a implementar isso foi Michael Hall .

    Sim, isso é mágico, com essas abordagens você pode obter aplicativos, modelos e campos Django totalmente dinâmicos com qualquer back-end de banco de dados relacional. Mas a que custo? A estabilidade da aplicação sofrerá com o uso pesado? Estas são as questões a serem consideradas. Você precisa garantir um bloqueio adequado para permitir pedidos simultâneos de alteração do banco de dados.

    Se você estiver usando a biblioteca de Michael Halls, seu código ficará assim:

    from dynamo import models
    
    test_app, created = models.DynamicApp.objects.get_or_create(
                          name='dynamo'
                        )
    test, created = models.DynamicModel.objects.get_or_create(
                      name='Test',
                      verbose_name='Test Model',
                      app=test_app
                   )
    foo, created = models.DynamicModelField.objects.get_or_create(
                      name = 'foo',
                      verbose_name = 'Foo Field',
                      model = test,
                      field_type = 'dynamiccharfield',
                      null = True,
                      blank = True,
                      unique = False,
                      help_text = 'Test field for Foo',
                   )
    bar, created = models.DynamicModelField.objects.get_or_create(
                      name = 'bar',
                      verbose_name = 'Bar Field',
                      model = test,
                      field_type = 'dynamicintegerfield',
                      null = True,
                      blank = True,
                      unique = False,
                      help_text = 'Test field for Bar',
                   )
Ivan Kharlamov
fonte
3
este tópico foi discutido recentemente no DjangoCon 2013 Europe: slideshare.net/schacki/… e youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
Também pode ser interessante notar que o uso de django-pgjson no Postgres> = 9.2 permite o uso direto do campo json do postgresql. No Django> = 1.7, a API de filtro para consultas é relativamente sã. O Postgres> = 9.4 também permite campos jsonb com melhores índices para consultas mais rápidas.
GDorn
1
Atualizado hoje para observar a adoção do Django do HStoreField e JSONField no contrib. Ele inclui alguns widgets de formulário que não são impressionantes, mas funcionam se você precisar ajustar os dados no administrador.
GDorn
13

Eu tenho trabalhado para promover a idéia do django-dynamo. O projeto ainda não está documentado, mas você pode ler o código em https://github.com/charettes/django-mutant .

Na verdade, os campos FK e M2M (consulte contrib.related) também funcionam e é até possível definir o wrapper para seus próprios campos personalizados.

Também há suporte para opções de modelo, como unique_together e ordering, além de bases de modelo, para que você possa subclassar proxy de modelo, resumo ou mixins.

Na verdade, estou trabalhando em um mecanismo de bloqueio que não está na memória para garantir que as definições de modelo possam ser compartilhadas entre várias instâncias de execução do django, impedindo-as de usar definições obsoletas.

O projeto ainda é muito alfa, mas é uma tecnologia fundamental para um dos meus projetos, então terei que levá-lo à produção pronto. O grande plano é oferecer suporte ao django-nonrel também para que possamos aproveitar o driver mongodb.

Simon Charette
fonte
1
Olá Simon! Eu incluí um link para o seu projeto na minha resposta da wiki logo depois que você o criou no github. :))) Prazer em vê-lo no stackoverflow!
Ivan Kharlamov
4

Pesquisas adicionais revelam que este é um caso um tanto especial do padrão de design do Valor da Atribuição de Entidade , que foi implementado para o Django por alguns pacotes.

Primeiro, há o projeto eav-django original , que está no PyPi.

Segundo, há uma bifurcação mais recente do primeiro projeto, o django-eav, que é principalmente um refator para permitir o uso do EAV com os próprios modelos ou modelos do django em aplicativos de terceiros.

GDorn
fonte
Vou incluí-lo no wiki.
Ivan Kharlamov
1
Eu argumentaria o contrário, que o EAV é um caso especial de modelagem dinâmica. É muito usado na comunidade "web semântica", onde é chamado de "triplo" ou "quad" se incluir um ID exclusivo. No entanto, é improvável que seja tão eficiente quanto um mecanismo que possa criar e modificar dinamicamente tabelas SQL.
Cerin
@GDom é o eav-django a sua primeira escolha? Quero dizer qual opção acima você escolheu?
Moreno
1
@ Moreno A escolha certa vai depender muito do seu caso de uso específico. Eu usei o EAV e o JsonFields por diferentes razões. O último é suportado diretamente pelo Django agora, portanto, para um novo projeto, eu o usaria primeiro, a menos que tivesse uma necessidade específica de poder consultar a tabela EAV. Observe que você também pode consultar o JsonFields.
GDorn #