157 votes

Champs de modèle dynamiques de Django

Je travaille sur un multi-locataires application 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 établir des rapports sur ces données. Ce dernier point fait que JSONField n'est pas une bonne option, donc à la place j'ai 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 que CustomDataField a un ForeignKey pour 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 lourd, notamment parce qu'il faut créer manuellement les données connexes et les associer au modèle concret. Existe-t-il une meilleure approche ?

Des options qui ont été écartées de manière préventive :

  • SQL personnalisé pour modifier les tables à la volée. En partie parce que cela n'évoluera pas et en partie parce que c'est un peu trop compliqué.
  • Solutions sans schéma comme NoSQL. Je n'ai rien contre elles, mais elles ne sont toujours pas adaptées. En fin de compte, ces données est saisie, et il est possible d'utiliser une application de rapport tierce.
  • JSONField, comme indiqué ci-dessus, car il ne fonctionnera pas bien avec les requêtes.

270voto

Ivan Kharlamov Points 1037

À l'heure actuelle, il existe quatre approches disponibles, dont deux requièrent un certain backend de stockage :

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

    Cette solution est basée sur Valeur de l'attribut de l'entité modèle de données, essentiellement, il utilise plusieurs tables pour stocker les attributs dynamiques des objets. L'avantage de cette solution est 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 des bases de données ;

    • vous permet d'attacher/détacher efficacement le stockage dynamique des attributs au modèle Django avec des commandes simples comme :

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

    • Tout en étant très puissant.

    Inconvénients :

    • Pas très efficace. Il s'agit plutôt 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 une des fourches puisque le paquet officiel n'est plus maintenu et qu'il n'y a pas de leader incontesté.

    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 supporte plusieurs types de données plus complexes. La plupart sont pris en charge par des paquets tiers, mais ces dernières années, Django les a adoptés dans django.contrib.postgres.fields.

    HStoreField :

    Django-hstore était à l'origine un paquetage tiers, mais Django 1.8 a ajouté la fonction HStoreField comme un élément intégré, ainsi que plusieurs autres types de champs supportés par PostgreSQL.

    Cette approche est bonne dans le sens où elle vous permet d'avoir le meilleur des deux mondes : champs dynamiques et base de données relationnelle. Cependant, hstore est pas idéal du point de vue des performances surtout si vous êtes amené à stocker des milliers d'éléments dans un seul champ. Il ne prend également en charge que les chaînes de caractères pour les 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 dans JSON, et pas seulement les paires clé/valeur. Ils ont également tendance à être plus rapides et (pour JSONB) plus compacts que Hstore. Plusieurs paquets implémentent les champs JSON/JSONB, notamment 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 les grands dictionnaires. Il prend également en charge des types autres que les chaînes de caractères, 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 scriptée).

    >>> 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 de Django - avec elles, vous pouvez avoir des modèles entièrement dynamiques.

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

    Consultez cet exemple 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, ...}

    Vous pouvez même créer listes intégrées de tout modèle 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 met en œuvre des clés étrangères et des champs m2m entièrement dynamiques. Elle est inspirée par les solutions incroyables mais quelque peu bricolées de Will Hardy et Michael Hall.

    Tous ces éléments sont basés sur les hooks Sud de Django, qui, selon Conférence de Will Hardy à la DjangoCon 2011 (regardez-le !) sont néanmoins robustes et testés en production ( le code source correspondant ).

    Premier à mettre en œuvre cette était Michael Hall .

    Oui, c'est magique, avec ces approches vous pouvez réaliser des applications, modèles et champs Django entièrement dynamiques avec n'importe quel backend de base de données relationnelle. Mais à quel prix ? La stabilité de l'application va-t-elle souffrir d'une utilisation intensive ? Telles sont les questions qu'il faut se poser. Vous devez vous assurer de maintenir une serrure afin de permettre des demandes simultanées de modification de la base de données.

    Si vous utilisez la librairie de 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',
                   )

13voto

Simon Charette Points 490

J'ai travaillé à pousser l'idée de django-dynamo plus loin. Le projet est encore non documenté mais vous pouvez lire le code à l'adresse suivante https://github.com/charettes/django-mutant .

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

Il existe également une prise en charge des options de modèle telles que unique_together et ordering, ainsi que des bases de modèle permettant de sous-classer les proxy, abstract ou mixins de modèle.

Je travaille actuellement sur un mécanisme de verrouillage qui n'est pas en mémoire afin de s'assurer que les définitions de modèles peuvent être partagées entre plusieurs instances de django tout en les empêchant d'utiliser des définitions obsolètes.

Le projet est encore en phase alpha, mais il s'agit d'une technologie fondamentale pour l'un de mes projets, je dois donc le rendre prêt pour la production. Le grand projet est de supporter django-nonrel également afin que nous puissions tirer parti du pilote mongodb.

4voto

GDorn Points 1944

Des recherches plus poussées révèlent qu'il s'agit d'un cas un peu particulier de Valeur de l'attribut de l'entité qui a été implémenté pour Django par quelques paquets.

D'abord, il y a l'original eav-django qui est sur PyPi.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X