Dynamische Django-Modellfelder

161

Ich arbeite an einer mandantenfähigen Anwendung, in der einige Benutzer ihre eigenen Datenfelder (über den Administrator) definieren können, um zusätzliche Daten in Formularen zu sammeln und über die Daten zu berichten. Das letztere Bit macht JSONField nicht zu einer großartigen Option, daher habe ich stattdessen 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, dass CustomDataField einen ForeignKey to 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 folgender Verwendung:

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 angesichts der Notwendigkeit, die zugehörigen Daten manuell zu erstellen und mit dem konkreten Modell zu verknüpfen. Gibt es einen besseren Ansatz?

Optionen, die vorbeugend verworfen wurden:

  • Benutzerdefiniertes SQL zum Ändern von Tabellen im laufenden Betrieb. Teilweise, weil dies nicht skalierbar ist und teilweise, weil es zu viel Hack ist.
  • Schemalose Lösungen wie NoSQL. Ich habe nichts gegen sie, aber sie passen immer noch nicht gut zusammen. Letztlich wird diese Daten sind eingegeben, und die Möglichkeit besteht , von einer Drittanbieter - Reporting - Anwendung.
  • JSONField, wie oben aufgeführt, da es mit Abfragen nicht gut funktioniert.
GDorn
quelle

Antworten:

277

Derzeit stehen vier Ansätze zur Verfügung, von denen zwei ein bestimmtes Speicher-Backend erfordern:

  1. Django-eav (die Originalverpackung ist nicht mehr erhalten, hat aber einige blühende Gabeln )

    Diese Lösung basiert auf dem Datenmodell " Entitätsattributwert". Im Wesentlichen werden mehrere Tabellen zum Speichern dynamischer Attribute von Objekten verwendet. Das Tolle an dieser Lösung ist, dass es:

    • verwendet mehrere reine und einfache Django-Modelle zur Darstellung dynamischer Felder, wodurch es einfach zu verstehen und datenbankunabhängig ist;
    • Ermöglicht das effektive Anhängen / Trennen des dynamischen Attributspeichers an das Django-Modell mit einfachen Befehlen wie:

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Integriert sich gut in den Django-Administrator .

    • Gleichzeitig wirklich mächtig.

    Nachteile:

    • Nicht sehr effizient. Dies ist eher eine Kritik am EAV-Muster selbst, bei dem die Daten manuell aus einem Spaltenformat mit 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 eindeutige Schlüsselbeschränkung, 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 klaren 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 von Drittanbietern, aber Django 1.8 fügte HStoreField als integriertes Element hinzu , zusammen mit mehreren anderen von PostgreSQL unterstützten Feldtypen.

    Dieser Ansatz ist insofern gut, als Sie das Beste aus beiden Welten haben: dynamische Felder und relationale Datenbanken. In Bezug auf die Leistung ist hstore jedoch nicht ideal , insbesondere wenn Sie am Ende Tausende von Artikeln in einem Feld speichern möchten . 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 Djangos Shell können Sie es folgendermaßen verwenden:

    >>> 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 auch tendenziell schneller und (für JSONB) kompakter als Hstore. Einige Pakete implementieren JSON / JSONB-Felder, einschließlich django-pgfields. Ab Django 1.9 ist JSONField jedoch in JSONB als Speicher integriert. JSONField ähnelt HStoreField und kann bei 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. Komplexe Indizes müssen möglicherweise manuell erstellt (oder per Skript migriert) werden.

    >>> 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 volldynamische 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 auf Django-nonrel zu migrieren, müssen Sie unter anderem ManyToMany durch ListField ersetzen .

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

    Django-Mutante implementiert volldynamische 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 ( siehe da!) Trotzdem robust und in der Produktion getestet sind ( relevanter Quellcode ).

    Der erste, der dies umsetzte, war Michael Hall .

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

    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',
                   )
    
Ivan Kharlamov
quelle
3
Dieses Thema wurde kürzlich auf der DjangoCon 2013 in Europa besprochen : slidehare.net/schacki/… und youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
Es kann auch erwähnenswert sein, dass die Verwendung von django-pgjson auf Postgres> = 9.2 die direkte Verwendung des json-Felds von postgresql ermöglicht. Bei Django> = 1.7 ist die Filter-API für Abfragen relativ vernünftig. Postgres> = 9.4 ermöglicht auch Jsonb-Felder mit besseren Indizes für schnellere Abfragen.
GDorn
1
Heute aktualisiert, um Djangos Übernahme von HStoreField und JSONField als Beitrag zu vermerken. Es enthält einige Formular-Widgets, die nicht besonders gut sind, aber funktionieren, wenn Sie Daten im Administrator optimieren müssen.
GDorn
13

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 gibt auch Unterstützung für Modelloptionen wie unique_together und order plus Model Base, sodass Sie Modell-Proxy, Abstract oder Mixins in Unterklassen unterteilen können.

Ich arbeite derzeit an einem nicht speicherinternen Sperrmechanismus, um sicherzustellen, dass Modelldefinitionen für mehrere Django-Instanzen gemeinsam genutzt werden können, während verhindert wird, dass sie veraltete Definitionen verwenden.

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

Simon Charette
quelle
1
Hallo Simon! Ich habe einen Link zu Ihrem Projekt in meine Wiki-Antwort aufgenommen, kurz nachdem Sie es auf Github erstellt haben. :))) Schön dich auf stackoverflow zu sehen!
Ivan Kharlamov
4

Weitere Untersuchungen haben ergeben, dass dies ein etwas spezieller Fall des Entity Attribute Value- Entwurfsmusters ist, das von einigen Paketen für Django implementiert wurde.

Erstens gibt es das ursprüngliche eav-django- Projekt, das auf PyPi läuft.

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

GDorn
quelle
Ich werde es in das Wiki aufnehmen.
Ivan Kharlamov
1
Ich würde umgekehrt argumentieren, dass EAV ein Sonderfall der dynamischen Modellierung ist. Es wird häufig in der "Semantic Web" -Community verwendet, wo es als "Triple" oder "Quad" bezeichnet wird, wenn es eine eindeutige ID enthält. Es ist jedoch unwahrscheinlich, dass es jemals so effizient ist wie ein Mechanismus, mit dem SQL-Tabellen dynamisch erstellt und geändert werden können.
Cerin
@ GDom ist eav-django deine erste Wahl? Ich meine, welche Option haben Sie oben gewählt?
Moreno
1
@Moreno Die richtige Wahl wird sehr stark von Ihrem spezifischen Anwendungsfall abhängen. Ich habe sowohl EAV als auch JsonFields aus verschiedenen Gründen verwendet. Letzteres wird jetzt direkt von Django unterstützt. Für ein neues Projekt würde ich das zuerst verwenden, es sei denn, ich hätte ein spezielles Bedürfnis, in der EAV-Tabelle abfragen zu können. Beachten Sie, dass Sie auch JsonFields abfragen können.
GDorn