Campos del modelo dinámico de Django

Resuelto GDorn asked hace 12 años • 3 respuestas

Estoy trabajando en una aplicación multiinquilino 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. Este ú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

Tenga en cuenta que CustomDataField tiene una ForeignKey para el sitio: cada sitio tendrá un conjunto diferente de campos de datos personalizados, pero utilizará la misma base de datos. Entonces 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 conduce 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 parece muy complicado, particularmente con la necesidad de crear manualmente los datos relacionados y asociarlos con el modelo concreto. ¿Existe un mejor enfoque?

Opciones que han sido descartadas preventivamente:

  • SQL personalizado para modificar tablas sobre la marcha. En parte porque esto no escalará y en parte porque es demasiado complicado.
  • Soluciones sin esquemas 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 mencionó anteriormente, ya que no funcionará bien con consultas.
GDorn avatar Oct 29 '11 01:10 GDorn
Aceptado

A día de hoy, hay cuatro enfoques disponibles, dos de ellos requieren un determinado backend de almacenamiento:

  1. Django-eav (el paquete original ya no se mantiene pero tiene algunas bifurcaciones prósperas )

    Esta solución se basa en el modelo de datos Entity Attribute Value ; esencialmente, utiliza varias tablas para almacenar atributos dinámicos de los objetos. Lo bueno de esta solución es que:

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

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Se integra muy bien con el administrador de Django ;

    • Al mismo tiempo ser realmente poderoso.

    Desventajas:

    • No muy eficiente. Esto es más bien una crítica al patrón EAV en sí, que requiere fusionar manualmente los datos de un formato de columna con 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 resultar ineficaz en algunas bases de datos.
    • Deberá seleccionar una de las bifurcaciones , 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 paquete integrado, 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 base de datos relacional. Sin embargo, hstore no es ideal en cuanto a rendimiento , especialmente si vas a terminar almacenando miles de elementos en un campo. También solo admite cadenas para 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 contra 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')    
    

    Campo JSON :

    Los campos JSON/JSONB admiten cualquier tipo de datos codificables en 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, incluido django-pgfields , pero a partir de Django 1.9, JSONField está integrado y utiliza JSONB para almacenamiento. JSONField es similar a HStoreField y puede funcionar mejor con diccionarios grandes. También admite tipos distintos de cadenas, como números 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 una creación manual (o una migración mediante script).

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

    U otras adaptaciones de NoSQL Django: con ellas puedes tener modelos completamente 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 necesitará reemplazar ManyToMany con ListField , entre otras cosas.

    Consulte 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 puedes 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-mutant: Modelos dinámicos basados ​​en syncdb y South-hooks

    Django-mutant implementa campos m2m y clave externa totalmente dinámicos. Y está inspirado en soluciones increíbles, aunque un tanto pirateadas, de Will Hardy y Michael Hall.

    Todos estos se basan en ganchos de Django South, que, según la charla de Will Hardy en DjangoCon 2011 (¡míralo!) , son sin embargo 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 puedes lograr aplicaciones, modelos y campos de Django completamente dinámicos con cualquier backend de base de datos relacional. ¿Pero a qué precio? ¿Se verá afectada la estabilidad de la aplicación con el uso intensivo? Estas son las preguntas a considerar. Debe asegurarse de mantener un bloqueo adecuado para permitir solicitudes simultáneas de modificación de la base de datos.

    Si está utilizando la biblioteca de Michael Halls, 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 avatar Oct 28 '2011 20:10 Ivan Kharlamov

He estado trabajando para impulsar aún más la idea de django-dynamo. El proyecto aún no está documentado pero puedes 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 Ordering Plus Model Bases para que puedas crear subclases de modelos proxy, abstractos o mixins.

De hecho, estoy trabajando en un mecanismo de bloqueo que no está en la memoria para asegurarme de que las definiciones del modelo se puedan compartir entre múltiples instancias en ejecución de Django y al mismo tiempo evitar que utilicen definiciones obsoletas.

El proyecto aún 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 admitir django-nonrel para que podamos aprovechar el controlador mongodb.

Simon Charette avatar Jan 29 '2012 17:01 Simon Charette

Investigaciones adicionales revelan que este es un caso algo especial del patrón de diseño Entity Attribute Value , que ha sido implementado para Django mediante un par de paquetes.

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

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

GDorn avatar Nov 17 '2011 21:11 GDorn