Dynamiczne pola modelu Django

161

Pracuję nad aplikacją z wieloma dzierżawcami , w której niektórzy użytkownicy mogą definiować własne pola danych (za pośrednictwem administratora), aby zbierać dodatkowe dane w formularzach i raportować dane. Ten ostatni bit sprawia, że ​​JSONField nie jest świetną opcją, więc zamiast tego mam następujące rozwiązanie:

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

Zwróć uwagę, że CustomDataField ma ForeignKey to Site - każda witryna będzie miała inny zestaw niestandardowych pól danych, ale będzie używać tej samej bazy danych. Następnie różne konkretne pola danych można zdefiniować jako:

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

Prowadzi to do następującego zastosowania:

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?

Ale wydaje się to bardzo niezgrabne, szczególnie w przypadku konieczności ręcznego tworzenia powiązanych danych i kojarzenia ich z konkretnym modelem. Czy jest lepsze podejście?

Opcje, które zostały odrzucone z wyprzedzeniem:

  • Niestandardowy SQL do modyfikowania tabel w locie. Częściowo dlatego, że to się nie skaluje, a częściowo dlatego, że jest to zbyt duży hack.
  • Rozwiązania bez schematu, takie jak NoSQL. Nie mam nic przeciwko nim, ale nadal nie pasują. Ostatecznie dane te wpisywane na maszynie i istnieje możliwość korzystania z aplikacji raportującej innej firmy.
  • JSONField, jak wymieniono powyżej, ponieważ nie będzie dobrze działać z zapytaniami.
GDorn
źródło

Odpowiedzi:

278

Na dzień dzisiejszy dostępne są cztery podejścia, z których dwa wymagają określonego zaplecza pamięci masowej:

  1. Django-eav (oryginalny pakiet nie jest już utrzymywany, ale ma kilka dobrze prosperujących rozwidleń )

    To rozwiązanie jest oparte na modelu danych Entity Attribute Value , zasadniczo wykorzystuje kilka tabel do przechowywania dynamicznych atrybutów obiektów. Wspaniałe strony dotyczące tego rozwiązania są takie, że:

    • używa kilku czystych i prostych modeli Django do reprezentowania pól dynamicznych, co ułatwia zrozumienie i jest niezależne od bazy danych;
    • pozwala efektywnie dołączać / odłączać magazyn atrybutów dynamicznych do modelu Django za pomocą prostych poleceń, takich jak:

      eav.unregister(Encounter)
      eav.register(Patient)
    • Ładnie integruje się z administratorem Django ;

    • Jednocześnie jest naprawdę potężny.

    Wady:

    • Niezbyt wydajne. Jest to raczej krytyka samego wzorca EAV, który wymaga ręcznego scalania danych z formatu kolumny do zestawu par klucz-wartość w modelu.
    • Trudniejsze w utrzymaniu. Utrzymanie integralności danych wymaga ograniczenia klucza unikatowego w wielu kolumnach, które może być nieefektywne w niektórych bazach danych.
    • Będziesz musiał wybrać jeden z wideł , ponieważ oficjalny pakiet nie jest już utrzymywany i nie ma wyraźnego lidera.

    Użycie jest dość proste:

    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, pola JSON lub JSONB w PostgreSQL

    PostgreSQL obsługuje kilka bardziej złożonych typów danych. Większość jest obsługiwana przez pakiety firm trzecich, ale w ostatnich latach Django zaadoptowało je do django.contrib.postgres.fields.

    HStoreField :

    Django-hstore był pierwotnie pakietem innej firmy, ale Django 1.8 dodał HStoreField jako wbudowany, wraz z kilkoma innymi typami pól obsługiwanymi przez PostgreSQL.

    To podejście jest dobre w tym sensie, że pozwala uzyskać to, co najlepsze z obu światów: pola dynamiczne i relacyjną bazę danych. Jednak hstore nie jest idealnym rozwiązaniem pod względem wydajności , zwłaszcza jeśli zamierzasz przechowywać tysiące elementów na jednym polu. Obsługuje również tylko ciągi dla wartości.

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

    W powłoce Django możesz tego użyć w następujący sposób:

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

    Możesz wysyłać zindeksowane zapytania do pól 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 :

    Pola JSON / JSONB obsługują dowolny typ danych zakodowany w formacie JSON, nie tylko pary klucz / wartość, ale są również szybsze i (w przypadku JSONB) bardziej zwarte niż Hstore. Kilka pakietów implementuje pola JSON / JSONB, w tym django-pgfields , ale od Django 1.9, JSONField jest wbudowanym narzędziem używającym JSONB do przechowywania. JSONField jest podobny do HStoreField i może działać lepiej z dużymi słownikami. Obsługuje również typy inne niż ciągi, takie jak liczby całkowite, wartości logiczne i zagnieżdżone słowniki.

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

    Tworzenie w powłoce:

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

    Zapytania indeksowane są prawie identyczne z HStoreField, z wyjątkiem możliwości zagnieżdżania. Złożone indeksy mogą wymagać ręcznego utworzenia (lub migracji skryptowej).

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

    Lub inne adaptacje NoSQL Django - dzięki nim możesz mieć w pełni dynamiczne modele.

    Biblioteki NoSQL Django są świetne, ale pamiętaj, że nie są w 100% kompatybilne z Django, na przykład, aby migrować do Django-nonrel ze standardowego Django, będziesz musiał między innymi zastąpić ManyToMany ListField .

    Sprawdź ten przykład 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, ...}

    Możesz nawet tworzyć osadzone listy dowolnych modeli 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: Dynamiczne modele oparte na syncdb i South-hookach

    Django-mutant implementuje w pełni dynamiczne pola klucza obcego i m2m. I jest inspirowany niesamowitymi, ale nieco hackerskimi rozwiązaniami autorstwa Willa Hardy'ego i Michaela Halla.

    Wszystko to jest oparte na hookach Django South, które, zgodnie z przemówieniem Willa Hardy'ego na DjangoCon 2011 (patrzcie!), Są jednak solidne i przetestowane w produkcji ( odpowiedni kod źródłowy ).

    Po pierwsze, aby zaimplementować to był Michael Hall .

    Tak, to jest magia, dzięki takim podejściom można osiągnąć w pełni dynamiczne aplikacje, modele i pola Django z dowolnym zapleczem relacyjnej bazy danych. Ale jakim kosztem? Czy stabilność aplikacji ucierpi przy intensywnym użytkowaniu? Oto pytania, które należy rozważyć. Musisz upewnić się, że utrzymujesz odpowiednią blokadę , aby umożliwić jednoczesne żądania zmiany bazy danych.

    Jeśli używasz biblioteki Michael Halls, Twój kod będzie wyglądał następująco:

    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
źródło
3
ten temat był ostatnio omawiany na DjangoCon 2013 Europe: slideshare.net/schacki/… i youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf
Warto również zauważyć, że użycie django-pgjson na Postgres> = 9.2 umożliwia bezpośrednie użycie pola json postgresql. W Django> = 1.7, filtr API dla zapytań jest względnie rozsądny. Postgres> = 9.4 pozwala również na pola jsonb z lepszymi indeksami dla szybszych zapytań.
GDorn
1
Zaktualizowano dzisiaj, aby zwrócić uwagę na przyjęcie przez Django HStoreField i JSONField do contrib. Zawiera niektóre widżety formularzy, które nie są niesamowite, ale działają, jeśli musisz dostosować dane w panelu administracyjnym.
GDorn,
13

Pracowałem nad dalszym rozwinięciem idei django-dynamo. Projekt jest nadal nieudokumentowany, ale możesz przeczytać kod na https://github.com/charettes/django-mutant .

W rzeczywistości pola FK i M2M (patrz contrib.related) również działają, a nawet można zdefiniować opakowanie dla własnych pól niestandardowych.

Dostępna jest również obsługa opcji modelu, takich jak unique_together i ordering oraz podstawy modelu, dzięki czemu można podklasować model proxy, abstrakcyjny lub mieszany.

W rzeczywistości pracuję nad mechanizmem blokującym, który nie jest w pamięci, aby upewnić się, że definicje modeli mogą być współużytkowane przez wiele uruchomionych instancji django, jednocześnie zapobiegając używaniu przestarzałych definicji.

Projekt jest nadal bardzo alfa, ale jest podstawą technologii jednego z moich projektów, więc będę musiał zabrać go do produkcji. Wielki plan obejmuje również obsługę django-nonrel, abyśmy mogli wykorzystać sterownik mongodb.

Simon Charette
źródło
1
Cześć, Simon! Dołączyłem link do twojego projektu w mojej odpowiedzi wiki zaraz po utworzeniu go na github. :))) Miło cię widzieć na stackoverflow!
Ivan Kharlamov
4

Dalsze badania ujawniają, że jest to nieco szczególny przypadek wzorca projektowego wartości atrybutu jednostki , który został zaimplementowany w Django przez kilka pakietów.

Po pierwsze, jest oryginalny projekt eav-django , który jest na PyPi.

Po drugie, istnieje nowszy fork pierwszego projektu, django-eav, który jest przede wszystkim refaktorem umożliwiającym używanie EAV z własnymi modelami django lub modelami w aplikacjach innych firm.

GDorn
źródło
Umieszczę to na wiki.
Ivan Kharlamov
1
Powiedziałbym odwrotnie, że EAV jest szczególnym przypadkiem modelowania dynamicznego. Jest często używany w społeczności „sieci semantycznej”, gdzie jest nazywany „potrójnym” lub „poczwórnym”, jeśli zawiera unikalny identyfikator. Jednak jest mało prawdopodobne, aby kiedykolwiek był tak wydajny, jak mechanizm, który może dynamicznie tworzyć i modyfikować tabele SQL.
Cerin
@GDom czy eav-django jest Twoim pierwszym wyborem? Chodzi mi o to, którą opcję powyżej wybrałeś?
Moreno
1
@Moreno Właściwy wybór będzie w dużym stopniu zależał od konkretnego przypadku użycia. Użyłem zarówno EAV, jak i JsonFields z różnych powodów. Ta ostatnia jest teraz bezpośrednio obsługiwana przez Django, więc w nowym projekcie użyłbym tego jako pierwszy, chyba że miałem konkretną potrzebę wykonywania zapytań w tabeli EAV. Zauważ, że możesz również wykonywać zapytania dotyczące JsonFields.
GDorn