it-swarm.com.de

Django dynamische Modellfelder

Ich arbeite an einer mandantenfähigen Anwendung, in der einige Benutzer ihre eigenen Datenfelder definieren können (über den Administrator), um zusätzliche Daten in Formularen zu erfassen und über die Daten zu berichten. Das letzte Bit macht JSONField nicht zu einer großartigen Option. Stattdessen habe ich die folgende Lösung:

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

Beachten Sie, wie CustomDataField einen ForeignKey für Site hat. Jede Site verfügt über einen anderen Satz benutzerdefinierter Datenfelder, verwendet jedoch dieselbe Datenbank. Dann können die verschiedenen konkreten Datenfelder definiert werden als:

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

Dies führt zu folgendem Nutzen:

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?

Dies ist jedoch sehr umständlich, insbesondere wenn die zugehörigen Daten manuell erstellt und mit dem konkreten Modell verknüpft werden müssen. Gibt es einen besseren Ansatz?

Optionen, die vorläufig verworfen wurden:

  • Benutzerdefiniertes SQL zum direkten Ändern von Tabellen. Zum Teil, weil dies nicht skaliert und zum Teil, weil es zu viel Hack ist.
  • Schemalose Lösungen wie NoSQL. Ich habe nichts gegen sie, aber sie passen immer noch nicht gut zusammen. Letztendlich sind diese Daten ist eingegeben, und es besteht die Möglichkeit, eine Berichtsanwendung eines Drittanbieters zu verwenden.
  • JSONField funktioniert, wie oben aufgeführt, nicht gut mit Abfragen.
156
GDorn

Ab heute sind vier Ansätze verfügbar, von denen zwei ein bestimmtes Speicher-Backend erfordern:

  1. Django-eav (Das Originalpaket wird nicht mehr gepflegt, hat aber einige blühende Gabeln)

    Diese Lösung basiert auf dem Datenmodell Entitätsattributwert ​​und verwendet im Wesentlichen mehrere Tabellen, um dynamische Attribute von Objekten zu speichern. Das Tolle an dieser Lösung ist, dass:

    • verwendet mehrere reine und einfache Django Modelle, um dynamische Felder darzustellen, wodurch es einfach zu verstehen und datenbankunabhängig wird;
    • ermöglicht das effektive Anhängen/Entfernen des dynamischen Attributspeichers an das Django - Modell mit einfachen Befehlen wie:

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Passt gut zu Django admin;

    • Zur gleichen Zeit wirklich mächtig zu sein.

    Nachteile:

    • Nicht sehr effizient. Dies ist eher eine Kritik am EAV-Muster selbst, bei der die Daten manuell aus einem Spaltenformat zu einer Reihe von Schlüssel-Wert-Paaren im Modell zusammengeführt werden müssen.
    • Schwieriger zu pflegen. Die Aufrechterhaltung der Datenintegrität erfordert eine mehrspaltige Einschränkung für eindeutige Schlüssel, die in einigen Datenbanken möglicherweise ineffizient ist.
    • Sie müssen eine der Gabeln auswählen, da das offizielle Paket nicht mehr gepflegt wird und es keinen eindeutigen Anführer gibt.

    Die Verwendung ist ziemlich einfach:

    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. Hstore-, JSON- oder JSONB-Felder in PostgreSQL

    PostgreSQL unterstützt mehrere komplexere Datentypen. Die meisten werden über Pakete von Drittanbietern unterstützt, aber in den letzten Jahren hat Django sie in Django.contrib.postgres.fields übernommen.

    HStoreField :

    Django-hstore war ursprünglich ein Paket eines Drittanbieters, aber Django 1.8 fügte HStoreField als eingebautes in, zusammen mit mehreren anderen von PostgreSQL unterstützten Feldtypen.

    Dieser Ansatz ist in einem Sinne gut, dass Sie das Beste aus beiden Welten erhalten: dynamische Felder und relationale Datenbanken. Hstore ist jedoch in Bezug auf die Leistung nicht ideal , insbesondere wenn Sie am Ende Tausende von Artikeln in einem Feld speichern werden. Es werden auch nur Zeichenfolgen für Werte unterstützt.

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

    In Django's Shell kannst du es so benutzen:

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

    Sie können indizierte Abfragen für Hstore-Felder ausgeben:

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

    JSON/JSONB-Felder unterstützen jeden JSON-codierbaren Datentyp, nicht nur Schlüssel/Wert-Paare, sondern sind in der Regel auch schneller und (für JSONB) kompakter als Hstore. Einige Pakete implementieren JSON/JSONB-Felder, einschließlich Django-pgfields, aber ab Django 1.9 JSONField ist eine integrierte Verwendung von JSONB für die Speicherung. JSONField ähnelt HStoreField und kann mit großen Wörterbüchern eine bessere Leistung erzielen. Es werden auch andere Typen als Zeichenfolgen unterstützt, z. B. Ganzzahlen, Boolesche Werte und verschachtelte Wörterbücher.

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

    Erstellen in der Shell:

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

    Indizierte Abfragen sind nahezu identisch mit HStoreField, außer dass eine Verschachtelung möglich ist. Für komplexe Indizes ist möglicherweise eine manuelle Erstellung (oder eine Skriptmigration) erforderlich.

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

    Oder andere NoSQL Django -Anpassungen - mit ihnen können Sie voll dynamische Modelle haben.

    NoSQL Django -Bibliotheken sind großartig, aber denken Sie daran, dass sie nicht zu 100% Django-kompatibel sind, um beispielsweise von Standard Django nach Django-nonrel zu migrieren müssen unter anderem ManyToMany durch ListField ersetzen.

    Kasse dieses Django MongoDB Beispiel:

    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, ...}
    

    Sie können sogar eingebettete Listen aller Django Modelle erstellen:

    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: Dynamische Modelle basierend auf Syncdb und South-Hooks

    Django-mutant ​​implementiert vollständig dynamische Fremdschlüssel- und m2m-Felder. Und ist inspiriert von unglaublichen, aber etwas hackigen Lösungen von Will Hardy und Michael Hall.

    All dies basiert auf Django South Hooks, die laut Will Hardys Vortrag auf der DjangoCon 2011 (aufgepasst!) dennoch sind robust und in der Produktion getestet ( relevanter Quellcode ).

    Zuerst dies implementieren war Michael Hall .

    Ja, das ist magisch. Mit diesen Ansätzen können Sie voll dynamische Django Apps, Modelle und Felder mit jedem relationalen Datenbank-Backend erreichen. Aber zu welchen Kosten? Leidet die Stabilität der Anwendung bei starker Beanspruchung? Dies sind die zu berücksichtigenden Fragen. Sie müssen sicherstellen, dass eine ordnungsgemäße Sperre beibehalten wird, um gleichzeitige Datenbankänderungsanforderungen zuzulassen.

    Wenn Sie Michael Halls lib verwenden, sieht Ihr Code folgendermaßen aus:

    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

Ich habe daran gearbeitet, die Django-Dynamo-Idee weiter voranzutreiben. Das Projekt ist noch nicht dokumentiert, aber Sie können den Code unter https://github.com/charettes/Django-mutant lesen.

Tatsächlich funktionieren auch FK- und M2M-Felder (siehe Contrib.Related), und es ist sogar möglich, Wrapper für Ihre eigenen benutzerdefinierten Felder zu definieren.

Es werden auch Modelloptionen wie unique_together und ordering sowie Modellbasen unterstützt, sodass Sie Modellproxys, Abstracts oder Mixins in Unterklassen unterteilen können.

Ich arbeite gerade an einem nicht im Speicher befindlichen Sperrmechanismus, um sicherzustellen, dass Modelldefinitionen für mehrere Django ausgeführte Instanzen freigegeben werden können, ohne dass sie veraltete Definitionen verwenden.

Das Projekt ist immer noch sehr Alpha, aber es ist eine Eckpfeilertechnologie für eines meiner Projekte, sodass ich es zur Serienreife bringen muss. Der große Plan sieht auch die Unterstützung von Django-Nonrel vor, damit wir den Mongodb-Treiber nutzen können.

13
Simon Charette

Weitere Untersuchungen haben ergeben, dass dies ein etwas spezieller Fall von Entity Attribute Value Design Pattern ist, der für Django von einigen Paketen implementiert wurde.

Erstens gibt es das ursprüngliche Projekt eav-Django , das sich auf PyPi befindet.

Zweitens gibt es eine neuere Abzweigung des ersten Projekts, Django-eav , das in erster Linie ein Refactor ist, um die Verwendung von EAV mit Djangos eigenen Modellen oder Modellen in Apps von Drittanbietern zu ermöglichen.

4
GDorn