Campi di modelli dinamici di Django


161

Sto lavorando a un'applicazione multi-tenant in cui alcuni utenti possono definire i propri campi di dati (tramite l'amministratore) per raccogliere ulteriori dati nei moduli e creare rapporti sui dati. Quest'ultimo bit rende JSONField non un'ottima opzione, quindi ho la seguente soluzione:

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

Nota come CustomDataField ha una chiave esterna al sito: ogni sito avrà un set diverso di campi dati personalizzati, ma utilizzerà lo stesso database. Quindi i vari campi di dati concreti possono essere definiti come:

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

Questo porta al seguente uso:

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?

Ma questo sembra molto goffo, in particolare con la necessità di creare manualmente i dati correlati e associarli al modello concreto. C'è un approccio migliore?

Opzioni che sono state scartate preventivamente:

  • SQL personalizzato per modificare le tabelle al volo. In parte perché questo non si ridimensionerà e in parte perché è troppo un hack.
  • Soluzioni senza schema come NoSQL. Non ho nulla contro di loro, ma non sono ancora adatti. Alla fine questi dati vengono digitati e esiste la possibilità di utilizzare un'applicazione di reporting di terze parti.
  • JSONField, come elencato sopra, poiché non funzionerà bene con le query.

6
Preventivamente, questa non è una di queste domande: stackoverflow.com/questions/7801729/... stackoverflow.com/questions/2854656/...
GDorn

Risposte:


278

Ad oggi, ci sono quattro approcci disponibili, due dei quali richiedono un certo back-end di archiviazione:

  1. Django-eav (il pacchetto originale non è più mantenuto ma ha alcune forcelle fiorenti )

    Questa soluzione si basa sul modello di dati Valore attributo entità , essenzialmente utilizza diverse tabelle per memorizzare gli attributi dinamici degli oggetti. Gran parte di questa soluzione è che:

    • utilizza diversi modelli Django puri e semplici per rappresentare i campi dinamici, il che rende semplice da capire e indipendente dal database;
    • consente di collegare / scollegare in modo efficace la memorizzazione dinamica degli attributi nel modello Django con semplici comandi come:

      eav.unregister(Encounter)
      eav.register(Patient)
    • Si integra perfettamente con l'amministratore di Django ;

    • Allo stesso tempo, è davvero potente.

    Svantaggi:

    • Non molto efficiente. Questa è più una critica al modello EAV stesso, che richiede l'unione manuale dei dati da un formato di colonna a un set di coppie chiave-valore nel modello.
    • Più difficile da mantenere. Il mantenimento dell'integrità dei dati richiede un vincolo chiave univoco a più colonne, che può essere inefficace su alcuni database.
    • Dovrai selezionare una delle forcelle , poiché il pacchetto ufficiale non è più mantenuto e non esiste un leader chiaro.

    L'utilizzo è piuttosto semplice:

    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. Campi Hstore, JSON o JSONB in ​​PostgreSQL

    PostgreSQL supporta diversi tipi di dati più complessi. La maggior parte sono supportati da pacchetti di terze parti, ma negli ultimi anni Django li ha adottati in django.contrib.postgres.fields.

    HStoreField :

    Django-hstore era originariamente un pacchetto di terze parti, ma Django 1.8 ha aggiunto HStoreField come integrato, insieme a molti altri tipi di campi supportati da PostgreSQL.

    Questo approccio è buono in un certo senso che ti consente di avere il meglio di entrambi i mondi: campi dinamici e database relazionale. Tuttavia, hstore non è l' ideale dal punto di vista delle prestazioni , soprattutto se si intende archiviare migliaia di articoli in un campo. Supporta anche solo stringhe per valori.

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

    Nella shell di Django puoi usarlo in questo modo:

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

    È possibile inviare query indicizzate nei campi 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 :

    I campi JSON / JSONB supportano qualsiasi tipo di dati codificabile JSON, non solo coppie chiave / valore, ma tendono anche a essere più veloci e (per JSONB) più compatti di Hstore. Diversi pacchetti implementano i campi JSON / JSONB tra cui django-pgfields , ma a partire da Django 1.9, JSONField è un JSONB incorporato che utilizza per l'archiviazione. JSONField è simile a HStoreField e potrebbe funzionare meglio con dizionari di grandi dimensioni. Supporta anche tipi diversi dalle stringhe, come numeri interi, booleani e dizionari nidificati.

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

    Creare nella shell:

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

    Le query indicizzate sono quasi identiche a HStoreField, tranne che è possibile l'annidamento. Gli indici complessi possono richiedere la creazione manuale (o una migrazione tramite script).

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

    O altri adattamenti NoSQL Django - con loro puoi avere modelli completamente dinamici.

    Le librerie NoSQL Django sono fantastiche, ma tieni presente che non sono compatibili al 100% con Django, ad esempio, per migrare a Django-nonrel dallo standard Django dovrai sostituire ManyToMany con ListField tra le altre cose.

    Dai un'occhiata a questo esempio di 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, ...}

    Puoi persino creare elenchi incorporati di qualsiasi modello 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: modelli dinamici basati su syncdb e ganci sud

    Il mutante Django implementa campi chiave dinamica e m2m completamente dinamici. Ed è ispirato da soluzioni incredibili ma in qualche modo hacker di Will Hardy e Michael Hall.

    Tutti questi sono basati su ami Django South, che, secondo il discorso di Will Hardy a DjangoCon 2011 (guardalo!) Sono comunque robusti e testati in produzione ( codice sorgente pertinente ).

    Il primo ad implementare questo fu Michael Hall .

    Sì, questo è magico, con questi approcci è possibile ottenere app, modelli e campi Django completamente dinamici con qualsiasi backend di database relazionale. Ma a quale costo? La stabilità dell'applicazione ne risentirà in caso di uso intenso? Queste sono le domande da considerare. È necessario assicurarsi di mantenere un blocco adeguato per consentire richieste di modifica simultanea del database.

    Se stai usando la libreria Michael Halls, il tuo codice sarà simile al seguente:

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

3
questo argomento è stato recentemente discusso a DjangoCon 2013 Europa: slideshare.net/schacki/… e youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf

Vale anche la pena notare che l'uso di django-pgjson su Postgres> = 9.2 consente l'uso diretto del campo json di postgresql. Su Django> = 1.7, l'API del filtro per le query è relativamente sana. Postgres> = 9.4 consente anche campi jsonb con indici migliori per query più veloci.
GDorn

1
Aggiornato oggi per notare l'adozione da parte di Django di HStoreField e JSONField nel contrib. Include alcuni widget dei moduli che non sono fantastici, ma funzionano se è necessario modificare i dati nell'amministratore.
GDorn,

13

Ho lavorato per spingere ulteriormente l'idea della django-dinamo. Il progetto non è ancora documentato ma è possibile leggere il codice su https://github.com/charettes/django-mutant .

In realtà funzionano anche i campi FK e M2M (vedi contrib.related) ed è persino possibile definire il wrapper per i propri campi personalizzati.

C'è anche il supporto per le opzioni del modello come unique_together e l'ordinazione più le basi del modello in modo da poter sottoclassare proxy del modello, abstract o mixin.

In realtà sto lavorando a un meccanismo di blocco non in memoria per assicurarmi che le definizioni dei modelli possano essere condivise tra più istanze in esecuzione di django impedendole di utilizzare definizioni obsolete.

Il progetto è ancora molto alfa ma è una tecnologia fondamentale per uno dei miei progetti, quindi dovrò portarlo pronto per la produzione. Il grande piano supporta django-nonrel anche in modo da poter sfruttare il driver mongodb.


1
Ciao Simone! Ho incluso un link al tuo progetto nella mia risposta wiki subito dopo averlo creato su github. :))) Piacere di vederti su StackOverflow!
Ivan Kharlamov,

4

Ulteriori ricerche rivelano che questo è un caso un po 'speciale del modello di progettazione del valore dell'attributo dell'entità , che è stato implementato per Django da un paio di pacchetti.

Innanzitutto, c'è il progetto originale eav-django , che è su PyPi.

In secondo luogo, c'è un fork più recente del primo progetto, django-eav che è principalmente un refactor per consentire l'uso di EAV con i propri modelli o modelli di django in app di terze parti.


Lo includerò nel wiki.
Ivan Kharlamov,

1
Direi al contrario che l'EAV è un caso speciale di modellazione dinamica. È ampiamente utilizzato nella comunità del "web semantico" dove viene chiamato "triplo" o "quad" se include un ID univoco. Tuttavia, è improbabile che sia mai efficiente quanto un meccanismo in grado di creare e modificare dinamicamente tabelle SQL.
Cerin,

@GDom è eav-django la tua prima scelta? Voglio dire quale opzione sopra hai scelto?
Moreno,

1
@Moreno La scelta giusta dipenderà fortemente dal tuo caso d'uso specifico. Ho usato sia EAV che JsonFields per diversi motivi. Quest'ultimo è supportato direttamente da Django ora, quindi per un nuovo progetto lo userò prima a meno che non avessi una necessità specifica di poter eseguire query sulla tabella EAV. Si noti che è possibile eseguire query anche su JsonFields.
GDorn
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.