Champs de modèle dynamique Django

161

Je travaille sur une application multi-locataire dans laquelle certains utilisateurs peuvent définir leurs propres champs de données (via l'administrateur) pour collecter des données supplémentaires dans des formulaires et rendre compte des données. Ce dernier bit ne fait pas de JSONField une excellente option, j'ai donc la solution suivante:

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

Notez comment CustomDataField a une clé étrangère vers le site - chaque site aura un ensemble différent de champs de données personnalisés, mais utilisera la même base de données. Ensuite, les différents champs de données concrets peuvent être définis comme:

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

Cela conduit à l'utilisation suivante:

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?

Mais cela semble très maladroit, en particulier avec la nécessité de créer manuellement les données associées et de les associer au modèle concret. Est-ce qu'il y a une meilleure approche?

Options qui ont été rejetées de manière préventive:

  • SQL personnalisé pour modifier les tables à la volée. En partie parce que cela ne va pas à l'échelle et en partie parce que c'est trop un hack.
  • Solutions sans schéma comme NoSQL. Je n'ai rien contre eux, mais ils ne conviennent toujours pas. Au final, ces données sont saisies et il est possible d'utiliser une application de reporting tierce.
  • JSONField, comme indiqué ci-dessus, car il ne fonctionnera pas correctement avec les requêtes.
GDorn
la source
6
À titre préventif, ce n'est pas l'une de ces questions: stackoverflow.com/questions/7801729/… stackoverflow.com/questions/2854656/…
GDorn

Réponses:

278

À ce jour, quatre approches sont disponibles, dont deux nécessitent un certain backend de stockage:

  1. Django-eav (le paquet d'origine n'est plus maintenu mais a quelques fourches prospères )

    Cette solution est basée sur le modèle de données Entity Attribute Value , essentiellement, elle utilise plusieurs tables pour stocker les attributs dynamiques des objets. Les avantages de cette solution sont qu'elle:

    • utilise plusieurs modèles Django purs et simples pour représenter les champs dynamiques, ce qui le rend simple à comprendre et indépendant de la base de données;
    • vous permet d'attacher / détacher efficacement le stockage d'attributs dynamiques au modèle Django avec des commandes simples comme:

      eav.unregister(Encounter)
      eav.register(Patient)
    • S'intègre parfaitement à l'admin Django ;

    • En même temps, il est vraiment puissant.

    Inconvénients:

    • Pas très efficace. Il s'agit davantage d'une critique du modèle EAV lui-même, qui nécessite de fusionner manuellement les données d'un format de colonne à un ensemble de paires clé-valeur dans le modèle.
    • Plus difficile à entretenir. Le maintien de l'intégrité des données nécessite une contrainte de clé unique à plusieurs colonnes, ce qui peut être inefficace sur certaines bases de données.
    • Vous devrez sélectionner l' une des fourches , car le package officiel n'est plus maintenu et il n'y a pas de leader clair.

    L'utilisation est assez simple:

    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. Champs Hstore, JSON ou JSONB dans PostgreSQL

    PostgreSQL prend en charge plusieurs types de données plus complexes. La plupart sont pris en charge via des packages tiers, mais ces dernières années, Django les a adoptés dans django.contrib.postgres.fields.

    HStoreField :

    Django-hstore était à l'origine un package tiers, mais Django 1.8 a ajouté HStoreField en tant que module intégré, ainsi que plusieurs autres types de champs pris en charge par PostgreSQL.

    Cette approche est bonne en ce sens qu'elle vous permet d'avoir le meilleur des deux mondes: champs dynamiques et base de données relationnelle. Cependant, hstore n'est pas idéal en termes de performances , en particulier si vous allez finir par stocker des milliers d'éléments dans un seul champ. Il ne prend également en charge que les chaînes de valeurs.

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

    Dans le shell de Django, vous pouvez l'utiliser comme ceci:

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

    Vous pouvez émettre des requêtes indexées sur les champs 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 :

    Les champs JSON / JSONB prennent en charge tous les types de données encodables JSON, pas seulement les paires clé / valeur, mais ont également tendance à être plus rapides et (pour JSONB) plus compacts que Hstore. Plusieurs packages implémentent des champs JSON / JSONB, y compris django-pgfields , mais à partir de Django 1.9, JSONField est un intégré utilisant JSONB pour le stockage. JSONField est similaire à HStoreField et peut être plus performant avec de gros dictionnaires. Il prend également en charge les types autres que les chaînes, tels que les entiers, les booléens et les dictionnaires imbriqués.

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

    Création dans le shell:

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

    Les requêtes indexées sont presque identiques à HStoreField, sauf que l'imbrication est possible. Les index complexes peuvent nécessiter une création manuelle (ou une migration par 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 d'autres adaptations NoSQL Django - avec elles, vous pouvez avoir des modèles entièrement dynamiques.

    Les bibliothèques NoSQL Django sont excellentes, mais gardez à l'esprit qu'elles ne sont pas 100% compatibles avec Django, par exemple, pour migrer vers Django-nonrel depuis Django standard, vous devrez remplacer ManyToMany par ListField entre autres.

    Découvrez cet exemple 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, ...}

    Vous pouvez même créer des listes intégrées de tous les modèles 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: modèles dynamiques basés sur syncdb et South-hooks

    Django-mutant implémente des champs de clé étrangère et m2m entièrement dynamiques. Et s'inspire des solutions incroyables mais quelque peu hackers de Will Hardy et Michael Hall.

    Tous sont basés sur les hooks Django South, qui, selon le discours de Will Hardy à DjangoCon 2011 (regardez-le!) Sont néanmoins robustes et testés en production ( code source pertinent ).

    Le premier à mettre en œuvre ce fut Michael Hall .

    Oui, c'est magique, avec ces approches, vous pouvez créer des applications, des modèles et des champs Django entièrement dynamiques avec n'importe quel backend de base de données relationnelle. Mais à quel prix? La stabilité de l'application souffrira-t-elle d'une utilisation intensive? Telles sont les questions à considérer. Vous devez vous assurer de maintenir un verrou approprié afin d'autoriser les demandes de modification simultanées de la base de données.

    Si vous utilisez la librairie Michael Halls, votre code ressemblera à ceci:

    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
la source
3
ce sujet a récemment été abordé à la DjangoCon 2013 Europe: slideshare.net/schacki/… et youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
Il peut aussi être intéressant de noter que l'utilisation de django-pgjson sur Postgres> = 9.2 permet une utilisation directe du champ json de postgresql. Sur Django> = 1.7, l'API de filtrage pour les requêtes est relativement saine. Postgres> = 9.4 autorise également les champs jsonb avec de meilleurs index pour des requêtes plus rapides.
GDorn
1
Mis à jour aujourd'hui pour noter l'adoption par Django de HStoreField et JSONField dans contrib. Il comprend des widgets de formulaire qui ne sont pas géniaux, mais qui fonctionnent si vous avez besoin de modifier les données dans l'administrateur.
GDorn
13

J'ai travaillé sur l'idée de django-dynamo plus loin. Le projet n'est toujours pas documenté mais vous pouvez lire le code sur https://github.com/charettes/django-mutant .

En fait, les champs FK et M2M (voir contrib.related) fonctionnent également et il est même possible de définir un wrapper pour vos propres champs personnalisés.

Il existe également un support pour les options de modèle telles que unique_together et ordering plus Model bases afin que vous puissiez sous-classer des modèles proxy, abstract ou mixins.

Je travaille actuellement sur un mécanisme de verrouillage non en mémoire pour m'assurer que les définitions de modèle peuvent être partagées entre plusieurs instances en cours d'exécution de django tout en les empêchant d'utiliser une définition obsolète.

Le projet est toujours très alpha, mais c'est une technologie de base pour l'un de mes projets, je vais donc devoir le mettre en production. Le grand plan est également de prendre en charge django-nonrel afin que nous puissions tirer parti du pilote mongodb.

Simon Charette
la source
1
Salut Simon! J'ai inclus un lien vers votre projet dans ma réponse wiki juste après l'avoir créé sur github. :))) Ravi de vous voir sur stackoverflow!
Ivan Kharlamov le
4

Des recherches plus poussées révèlent qu'il s'agit d'un cas quelque peu particulier de modèle de conception de valeur d'attribut d'entité , qui a été implémenté pour Django par quelques packages.

Tout d'abord, il y a le projet original eav-django , qui se trouve sur PyPi.

Deuxièmement, il y a un fork plus récent du premier projet, django-eav, qui est principalement un refactor pour permettre l'utilisation d'EAV avec les propres modèles ou modèles de django dans des applications tierces.

GDorn
la source
Je vais l'inclure dans le wiki.
Ivan Kharlamov
1
Je dirais l'inverse, que l'EAV est un cas particulier de modélisation dynamique. Il est très utilisé dans la communauté "web sémantique" où il est appelé "triple" ou "quad" s'il inclut un identifiant unique. Cependant, il est peu probable qu'il soit aussi efficace qu'un mécanisme capable de créer et de modifier dynamiquement des tables SQL.
Cerin
@GDom est-ce que eav-django est votre premier choix? Je veux dire quelle option avez-vous choisie ci-dessus?
Moreno
1
@Moreno Le bon choix dépendra fortement de votre cas d'utilisation spécifique. J'ai utilisé à la fois EAV et JsonFields pour différentes raisons. Ce dernier est directement pris en charge par Django maintenant, donc pour un nouveau projet, je l'utiliserais en premier à moins d'avoir un besoin spécifique de pouvoir interroger la table EAV. Notez que vous pouvez également interroger sur JsonFields.
GDorn