Campos de modelo dinámico de Django

161

Estoy trabajando en una aplicación multicliente en la que algunos usuarios pueden definir sus propios campos de datos (a través del administrador) para recopilar datos adicionales en formularios e informar sobre los datos. El último bit hace que JSONField no sea una gran opción, por lo que tengo la siguiente solución:

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 cómo CustomDataField tiene una ForeignKey to Site: cada sitio tendrá un conjunto diferente de campos de datos personalizados, pero usará la misma base de datos. Luego, los diversos campos de datos concretos se pueden definir 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'),)

Esto lleva al siguiente 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?

Pero esto se siente muy torpe, particularmente con la necesidad de crear manualmente los datos relacionados y asociarlos con el modelo concreto. ¿Hay un mejor enfoque?

Opciones que se han descartado preventivamente:

  • SQL personalizado para modificar tablas sobre la marcha. En parte porque esto no escalará y en parte porque es demasiado pirateo.
  • Soluciones sin esquema como NoSQL. No tengo nada en contra de ellos, pero todavía no encajan bien. En última instancia, estos datos se escriben y existe la posibilidad de utilizar una aplicación de informes de terceros.
  • JSONField, como se enumeró anteriormente, ya que no funcionará bien con las consultas.
GDorn
fuente
66
De manera preventiva, esta no es ninguna de estas preguntas: stackoverflow.com/questions/7801729/… stackoverflow.com/questions/2854656/…
GDorn

Respuestas:

278

A partir de hoy, hay cuatro enfoques disponibles, dos de los cuales requieren un cierto backend de almacenamiento:

  1. Django-eav (el paquete original ya no se mantiene pero tiene algunos tenedores prósperos )

    Esta solución se basa en el modelo de datos de Valor de atributo de entidad , esencialmente, utiliza varias tablas para almacenar atributos dinámicos de objetos. Lo mejor de esta solución es que:

    • utiliza varios modelos Django puros y simples para representar campos dinámicos, lo que lo hace fácil de entender y agnóstico a la base de datos;
    • le permite adjuntar / desconectar eficazmente el almacenamiento dinámico de atributos al modelo Django con comandos simples como:

      eav.unregister(Encounter)
      eav.register(Patient)
    • Muy bien se integra con Django admin ;

    • Al mismo tiempo ser realmente poderoso.

    Desventajas:

    • No muy eficiente Esto es más una crítica del patrón EAV en sí, que requiere fusionar manualmente los datos de un formato de columna a un conjunto de pares clave-valor en el modelo.
    • Más difícil de mantener. Mantener la integridad de los datos requiere una restricción de clave única de varias columnas, que puede ser ineficiente en algunas bases de datos.
    • Deberá seleccionar una de las horquillas , ya que el paquete oficial ya no se mantiene y no hay un líder claro.

    El uso es bastante sencillo:

    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 o JSONB en PostgreSQL

    PostgreSQL admite varios tipos de datos más complejos. La mayoría son compatibles con paquetes de terceros, pero en los últimos años Django los ha adoptado en django.contrib.postgres.fields.

    HStoreField :

    Django-hstore era originalmente un paquete de terceros, pero Django 1.8 agregó HStoreField como una función integrada, junto con varios otros tipos de campos compatibles con PostgreSQL.

    Este enfoque es bueno en el sentido de que le permite tener lo mejor de ambos mundos: campos dinámicos y bases de datos relacionales. Sin embargo, hstore no es ideal en cuanto al rendimiento , especialmente si va a terminar almacenando miles de elementos en un campo. También solo admite cadenas 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)

    En el shell de Django puedes usarlo así:

    >>> 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'

    Puede emitir consultas indexadas en los campos de 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 :

    Los campos JSON / JSONB admiten cualquier tipo de datos codificables JSON, no solo pares clave / valor, sino que también tienden a ser más rápidos y (para JSONB) más compactos que Hstore. Varios paquetes implementan campos JSON / JSONB, incluidos django-pgfields , pero a partir de Django 1.9, JSONField está integrado con JSONB para el almacenamiento. JSONField es similar a HStoreField y puede funcionar mejor con diccionarios grandes. También admite tipos distintos de cadenas, como enteros, booleanos y diccionarios anidados.

    #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)

    Creando en el shell:

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

    Las consultas indexadas son casi idénticas a HStoreField, excepto que es posible anidar. Los índices complejos pueden requerir la creación manual (o una migración programada).

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

    U otras adaptaciones NoSQL Django: con ellas puede tener modelos totalmente dinámicos.

    Las bibliotecas NoSQL Django son geniales, pero tenga en cuenta que no son 100% compatibles con Django, por ejemplo, para migrar a Django-nonrel desde Django estándar, deberá reemplazar ManyToMany con ListField, entre otras cosas.

    Vea este ejemplo de 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, ...}

    Incluso puede crear listas incrustadas de cualquier modelo de 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-mutante: modelos dinámicos basados ​​en syncdb y South-hooks

    Django-mutant implementa campos foráneos y m2m completamente dinámicos. Y está inspirado en soluciones increíbles pero algo hack de Will Hardy y Michael Hall.

    Todos estos se basan en los ganchos Django South, que, según la charla de Will Hardy en DjangoCon 2011 (¡cuidado!) , Sin embargo, son robustos y probados en producción ( código fuente relevante ).

    El primero en implementar esto fue Michael Hall .

    Sí, esto es mágico, con estos enfoques puede lograr aplicaciones, modelos y campos de Django completamente dinámicos con cualquier base de datos relacional. ¿Pero a qué precio? ¿La estabilidad de la aplicación se verá afectada por el uso intensivo? Estas son las preguntas a considerar. Debe asegurarse de mantener un bloqueo adecuado para permitir solicitudes de alteración simultánea de la base de datos.

    Si está utilizando Michael Halls lib, su código se verá así:

    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
fuente
3
Este tema se habló recientemente en DjangoCon 2013 Europa: slideshare.net/schacki/… y youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
También vale la pena señalar que el uso de django-pgjson en Postgres> = 9.2 permite el uso directo del campo json de postgresql. En Django> = 1.7, la API de filtro para consultas es relativamente sana. Postgres> = 9.4 también permite campos jsonb con mejores índices para consultas más rápidas.
GDorn
1
Se actualizó hoy para observar la adopción de Dtorego de HStoreField y JSONField en contrib. Incluye algunos widgets de formulario que no son geniales, pero funcionan si necesita modificar los datos en el administrador.
GDorn
13

He estado trabajando para impulsar aún más la idea django-dynamo. El proyecto aún no está documentado, pero puede leer el código en https://github.com/charettes/django-mutant .

En realidad, los campos FK y M2M (ver contrib.related) también funcionan e incluso es posible definir un contenedor para sus propios campos personalizados.

También hay soporte para opciones de modelo como unique_together y pedidos más bases de modelo para que pueda subclasificar proxy de modelo, resumen o mixins.

De hecho, estoy trabajando en un mecanismo de bloqueo que no está en la memoria para asegurarme de que las definiciones de modelo se puedan compartir entre varias instancias de ejecución de django, evitando que usen definiciones obsoletas.

El proyecto todavía es muy alfa, pero es una tecnología fundamental para uno de mis proyectos, por lo que tendré que llevarlo a producción. El gran plan también es compatible con django-nonrel para que podamos aprovechar el controlador mongodb.

Simon Charette
fuente
1
Hola simon He incluido un enlace a su proyecto en mi respuesta wiki justo después de haberlo creado en github. :))) ¡Me alegro de verte en stackoverflow!
Ivan Kharlamov
4

La investigación adicional revela que este es un caso algo especial del patrón de diseño de Valor de atributo de entidad , que ha sido implementado para Django por un par de paquetes.

Primero, está el proyecto original eav-django , que está en PyPi.

En segundo lugar, hay una bifurcación más reciente del primer proyecto, django-eav, que es principalmente un refactor para permitir el uso de EAV con modelos propios o modelos de django en aplicaciones de terceros.

GDorn
fuente
Lo incluiré en la wiki.
Ivan Kharlamov
1
Yo diría al revés, que EAV es un caso especial de modelado dinámico. Se usa mucho en la comunidad de "web semántica", donde se llama "triple" o "cuádruple" si incluye una identificación única. Sin embargo, es poco probable que sea tan eficiente como un mecanismo que pueda crear y modificar dinámicamente tablas SQL.
Cerin
@GDom ¿Es eav-django su primera opción? Quiero decir, ¿qué opción elegiste arriba?
Moreno
1
@Moreno La elección correcta dependerá en gran medida de su caso de uso específico. He usado EAV y JsonFields por diferentes razones. Django ahora admite directamente este último, por lo que para un nuevo proyecto lo usaría primero a menos que tuviera una necesidad específica de poder consultar en la tabla EAV. Tenga en cuenta que también puede consultar en JsonFields.
GDorn