Dynamiczne pola Modelu Django

Pracuję nadmulti-tenanted aplikacją, 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ć o danych. 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

Zauważ, że CustomDataField ma ForeignKey do witryny - każda witryna będzie miała inny zestaw niestandardowych pól danych, ale będzie korzystać z 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 z potrzebą ręcznego tworzenia powiązanych danych i powiązania ich z konkretnym modelem. Czy jest lepsze podejście?

Opcje, które zostały wcześniej odrzucone:

  • Niestandardowy SQL do modyfikowania tabel w locie. Częściowo dlatego, że to nie będzie skalowane, a częściowo dlatego, że jest to zbyt dużo włamania.
  • rozwiązania bez schematu, takie jak NoSQL. I nie mają nic przeciwko nim, ale nadal nie pasują. Ostatecznie dane są wpisane i istnieje możliwość korzystania z aplikacji raportującej innej firmy.
  • JSONField, jak wspomniano powyżej, ponieważ nie będzie działać dobrze z zapytaniami.
Author: Ivan Kharlamov, 2011-10-28

3 answers

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ż obsługiwany, ale ma kilka Kwitnące widelce)

    To rozwiązanie jest oparte na modelu danych wartość atrybutu encji , zasadniczo używa kilku tabel do przechowywania dynamicznych atrybutów obiektów. Świetne części o tym rozwiązaniu jest to, że it:

    • używa kilku czystych i prostych modeli Django do reprezentowania dynamicznych pól, co sprawia, że jest to proste do zrozumienia i niezależne od bazy danych; {]}
    • Pozwala na skuteczne dołączanie/odłączanie dynamicznego przechowywania atrybutów do modelu Django za pomocą prostych poleceń, takich jak:

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

    • Jednocześnie będąc naprawdę potężnym.

    Wady:

    • nie bardzo wydajny. Jest to bardziej krytyka samego wzorca EAV, który wymaga ręcznego łączenia danych z formatu kolumny do zestawu par klucz-wartość w modelu.
    • [[18]}trudniejsze w utrzymaniu. Zachowanie integralności danych wymaga jednokolumnowego unikalnego ograniczenia klucza, które może być nieefektywne w niektórych bazach danych.
  2. będziesz musiał wybrać jeden z widelców, ponieważ oficjalny pakiet nie jest już utrzymywany i nie ma wyraźnego lidera.
  3. The 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)
    
  4. Pola Hstore, JSON lub JSONB w PostgreSQL

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

    HStoreField :

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

    To podejście jest dobre w tym sensie, że pozwala na to, co najlepsze z obu światów: dynamicznych pól i relacyjnej bazy danych. Jednak hstore nie jest idealny pod względem wydajności, zwłaszcza jeśli masz zamiar skończyć przechowywanie tysięcy przedmiotów w jednym polu. Obsługuje również tylko łańcuchy 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 użyć tego 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 wydać indeksowane zapytania dotyczące 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ą każdy kodowalny typ danych JSON, nie tylko pary klucz / wartość, ale także są szybsze i (dla JSONB) bardziej kompaktowe niż Hstore. Kilka pakietów implementuje pola JSON/JSONB, w tym django-pgfields, ale od Django 1.9, JSONField jest wbudowany przy użyciu JSONB do przechowywania. JSONField jest podobny do HStoreField i może wykonywać lepiej z dużymi słownikami. Obsługuje również typy inne niż ciągi znaków, takie jak liczby całkowite, booleany 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 jak w HStoreField, z tym że zagnieżdżanie jest możliwe. Złożone indeksy mogą wymagać ręcznego tworzenia (lub migracji skryptowej).

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

    Lub inne adaptacje Django NoSQL - z nimi możesz mieć w pełni modele dynamiczne.

    NoSQL Biblioteki Django są świetne, ale pamiętaj, że nie są w 100% kompatybilne z Django, na przykład, aby przenieść się do Django-Nonrel ze standardowego Django, będziesz musiał wymienić ManyToMany między innymi na 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 utworzyć wbudowane 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')]
    )
    
  6. Django-mutant: Dynamic modele oparte na syncdb i South-hooks

    Django-mutant implementuje w pełni dynamiczny klucz obcy i pola m2m. Jest inspirowana niesamowitymi, ale nieco hakerskimi rozwiązaniami autorstwa Willa Hardy ' ego i Michaela Halla.

    Wszystko to opiera się na Django South hooks, które zgodnie z Will Hardy ' s talk at DjangoCon 2011 (uważaj!) {[15] } są jednak wytrzymałe i przetestowane w produkcji (odpowiednie źródło kod ).

    Pierwszy wdrożyć to był Michael Hall .

    Tak, to magia, dzięki tym podejściom możesz osiągnąć w pełni dynamiczne aplikacje, modele i pola Django {[15] } z dowolnym backendem relacyjnej bazy danych. Ale jakim kosztem? Czy przy intensywnym użytkowaniu ucierpi stabilność aplikacji? Są to pytania, które należy rozważyć. Aby umożliwić jednoczesną zmianę bazy danych, należy zachować odpowiednią blokadę prośby.

    Jeśli używasz Michael Halls lib, Twój kod będzie wyglądał tak:

    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',
                   )
    
 282
Author: Ivan Kharlamov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:41

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

Właściwie pola FK i M2M (patrz contrib.powiązane) również działają i możliwe jest nawet zdefiniowanie wrappera dla własnych pól niestandardowych.

Istnieje również wsparcie dla opcji modelu, takich jak unique_together i ordering plus Model bases, dzięki czemu możesz podklasować model proxy, abstract lub mixins.

I ' m właściwie pracuję nad mechanizmem blokady nie w pamięci, aby upewnić się, że definicje modelu mogą być współdzielone z wieloma uruchomionymi instancjami django, jednocześnie zapobiegając ich używaniu przestarzałej definicji.

Projekt jest nadal bardzo alpha, ale jest to kamień węgielny technologii dla jednego z moich projektów, więc będę musiał zabrać go do produkcji gotowy. Wielkim planem jest wsparcie django-nonrel, abyśmy mogli wykorzystać sterownik mongodb.

 13
Author: Simon Charette,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-29 17:44:34

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

Najpierw jest oryginalny Projekt EAV-django , który jest na PyPi.

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

 4
Author: GDorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-17 21:01:41