web-dev-qa-db-fra.com

Django champs de modèle dynamique)

Je travaille sur une application multi-tenanted 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 générer des rapports à partir de ces données. Ce dernier élément 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 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 suit:

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 fastidieux, notamment 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é supprimées de manière préventive:

  • SQL personnalisé pour modifier les tables à la volée. En partie parce que cela n'évolue pas et en partie parce que c'est trop compliqué.
  • Des solutions sans schéma comme NoSQL. Je n'ai rien contre eux, mais ils ne conviennent toujours pas. En fin de compte, ces données sont est saisies et il est possible d’utiliser une application de génération de rapports tierce.
  • JSONField, comme indiqué ci-dessus, car il ne fonctionnera pas bien avec les requêtes.
156
GDorn

À ce jour, il existe quatre approches, deux d'entre elles nécessitant un certain stockage:

  1. Django-eav (le paquet d'origine n'est plus conservé mais en contient - fourches en plein essor)

    Cette solution est basée sur le modèle de données Valeur d'attribut d'entité . Elle utilise essentiellement plusieurs tables pour stocker les attributs dynamiques des objets. Les points forts de cette solution sont les suivants:

    • utilise plusieurs modèles purs et simples Django 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 à Django modèle avec des commandes simples comme:

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • s'intègre parfaitement avec Django admin;

    • Dans le même temps, être vraiment 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 avec un ensemble de paires clé-valeur dans le modèle.
    • Plus difficile à maintenir. Le maintien de l'intégrité des données nécessite une contrainte de clé unique multi-colonnes, qui peut s'avérer inefficace sur certaines bases de données.
    • Vous devrez sélectionner ne des fourchettes , car le paquet 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 supporte 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 ajouté HStoreField intégré, ainsi que plusieurs autres types de champs pris en charge par PostgreSQL.

    Cette approche est bonne dans la mesure où elle vous permet d’avoir le meilleur des deux mondes: champs dynamiques et base de données relationnelle. Cependant, hstore est pas la performance idéale , surtout si vous allez stocker des milliers d’éléments dans un seul champ. En outre, il ne prend en charge que les chaînes 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 Django's Shell, 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 en JSON, pas seulement les paires clé/valeur, mais tendent également à être plus rapides et (pour JSONB) plus compacts que Hstore. Plusieurs paquets implémentent des champs JSON/JSONB, notamment Django-pgfields, mais à partir de Django 1.9, JSONField est intégré à JSONB pour le stockage. JSONField est similaire à HStoreField et peut être plus performant avec des dictionnaires volumineux. Il prend également en charge des 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éer 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 - vous pourrez ainsi disposer de modèles entièrement dynamiques.

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

    Checkout this Django Exemple de 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 any Django modèles:

    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 entièrement dynamiques de clé étrangère et m2m. Et est inspiré par des solutions incroyables mais quelque peu furtives de Will Hardy et Michael Hall.

    Tous ces éléments sont basés sur Django South hooks, ce 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 à implémenter ceci était Michael Hall .

    Oui, c'est magique, avec ces approches vous pouvez réaliser entièrement dynamique Django applications, modèles et champs avec n'importe quel backend de base de données relationnelle. Mais à quel prix? La stabilité de l'application sera-t-elle compromise lors d'une utilisation intensive? Ce sont les questions à considérer. Vous devez vous assurer de conserver un correct lock afin de permettre les demandes de modification simultanée de la base de données.

    Si vous utilisez Michael Halls lib, 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',
                   )
    
265
Ivan Kharlamov

Je travaille à pousser plus loin l'idée de Django-dynamo. Le projet n'est toujours pas documenté mais vous pouvez lire le code à l'adresse https://github.com/charettes/Django-mutant .

En réalité, 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.

Des options de modèle telles que unique_together et ordering plus Model Model sont également prises en charge afin que vous puissiez sous-classer des modèles de proxy, de résumé ou de mixine.

Je travaille actuellement sur un mécanisme de verrouillage qui ne fonctionne pas en mémoire pour m'assurer que les définitions de modèle peuvent être partagées entre plusieurs Django instances en cours, 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 soutient également Django-nonrel afin que nous puissions utiliser le pilote mongodb.

13
Simon Charette

Des recherches ultérieures révèlent qu'il s'agit d'un cas un peu particulier du modèle de conception valeur d'attribut d'entité , qui a été implémenté pour Django par quelques paquets.

Premièrement, il y a le projet 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 un refactor permettant d'utiliser EAV avec les propres modèles de Django ou des modèles dans des applications tierces.

4
GDorn