Поля динамічної моделі Джанго


161

Я працюю над багатозастосованою програмою, в якій деякі користувачі можуть визначати власні поля даних (через адміністратора) для збору додаткових даних у формах та звітування про дані. Останній біт робить JSONField не чудовим варіантом, тому натомість у мене є таке рішення:

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

Зверніть увагу, як у CustomDataField є сайт ForeignKey для кожного сайту - кожен Сайт матиме різний набір спеціальних полів даних, але використовує ту саму базу даних. Тоді різні конкретні поля даних можна визначити як:

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

Це призводить до наступного використання:

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?

Але це дуже непохитно, особливо з необхідністю вручну створювати відповідні дані та пов'язувати їх із конкретною моделлю. Чи є кращий підхід?

Варіанти, які попередньо відкинули:

  • Спеціальний SQL для зміни таблиць на ходу. Почасти тому, що це не буде масштабуватися, а частково тому, що це занадто багато хак.
  • Без схемних рішень, як NoSQL. Я нічого не маю проти них, але вони все ще не дуже підходять. В кінцевому рахунку ці дані є набрані, і існує можливість використання програми звітності третьою стороною.
  • Як зазначено вище, JSONField, оскільки він не буде добре працювати з запитами.

6
Превентивно, це не якийсь - небудь з цих питань: stackoverflow.com/questions/7801729 / ... stackoverflow.com/questions/2854656 / ...
GDorn

Відповіді:


278

На сьогоднішній день існує чотири доступні підходи, два з яких потребують певного резервного копіювання:

  1. Django-eav (оригінальна упаковка більше не доглядається, але має процвітаючі виделки )

    Це рішення засноване на моделі даних вартості атрибутів сутності , по суті, воно використовує кілька таблиць для зберігання динамічних атрибутів об'єктів. Відмінна частина цього рішення полягає в тому, що воно:

    • використовує кілька чистих і простих моделей Django для представлення динамічних полів, що робить його простим для розуміння та агностики даних;
    • дозволяє ефективно приєднувати / від'єднувати динамічне зберігання атрибутів до моделі Django за допомогою простих команд, таких як:

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Чудово інтегрується з адміністратором Django ;

    • У той же час бути справді потужним.

    Недоліки:

    • Не дуже ефективно. Це скоріше критика самого шаблону EAV, який вимагає вручну об'єднати дані з формату стовпця до набору пар ключових значень у моделі.
    • Важче у обслуговуванні. Підтримка цілісності даних вимагає унікального обмеження ключа у стовпцях, яке може бути неефективним для деяких баз даних.
    • Вам потрібно буде вибрати один з виделок , оскільки офіційний пакет більше не підтримується і чіткого лідера немає.

    Використання досить просто:

    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 або JSONB в PostgreSQL

    PostgreSQL підтримує кілька більш складних типів даних. Більшість підтримується через сторонні пакети, але останніми роками Django прийняв їх у django.contrib.postgres.fields.

    HStoreField :

    Спочатку Django-hstore був стороннім пакетом, але Django 1.8 додав HStoreField як вбудований разом із кількома іншими типами полів, підтримуваних PostgreSQL.

    Цей підхід хороший тим, що він дозволяє вам мати найкраще з обох світів: динамічні поля та реляційні бази даних. Однак hstore не є ідеальним для продуктивності , особливо якщо ви збираєтеся зберігати тисячі предметів в одному полі. Він також підтримує лише рядки для значень.

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

    У оболонці Джанго ви можете використовувати його так:

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

    Ви можете задавати індексовані запити щодо полів 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 :

    Поля JSON / JSONB підтримують будь-який тип даних, що кодується JSON, не тільки пари ключів / значень, але також мають тенденцію бути швидшими та (для JSONB) більш компактними, ніж Hstore. Кілька пакетів реалізують поля JSON / JSONB, включаючи django-pgfields , але, як і Django 1.9, JSONField є вбудованим, використовуючи JSONB для зберігання. JSONField схожий на HStoreField, і може краще працювати з великими словниками. Він також підтримує типи, крім рядків, такі як цілі числа, булеві та вкладені словники.

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

    Створення в оболонці:

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

    Індексовані запити майже ідентичні HStoreField, за винятком можливого введення. Складні індекси можуть зажадати створення вручну (або сценарії міграції).

    >>> Something.objects.filter(data__a=1)
    >>> Something.objects.filter(data__nested__c=3)
    >>> Something.objects.filter(data__has_key='a')
    
  3. Джанго МонгоДБ

    Або інші адаптації Django NoSQL - з ними ви можете мати повністю динамічні моделі.

    Бібліотеки NoSQL Django чудові, але майте на увазі, що вони не на 100% сумісні з Django, наприклад, щоб перейти на Django-nonrel зі стандартного Django, вам потрібно буде замінити ManyToMany на ListField серед іншого.

    Оформити цей приклад Джанго МонгоДБ:

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

    Ви навіть можете створити вбудовані списки будь-яких моделей 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-мутант: Динамічні моделі, засновані на syncdb та South-гаках

    Django-мутант реалізує повністю динамічні поля Foreign Foreign Key та m2m. І його надихають неймовірні, але дещо хакерські рішення Вілла Харді та Майкла Холла.

    Все це базується на гачках Джанго Саута , які, згідно з розмовами Вілла Харді на DjangoCon 2011 (дивіться це!) , Все-таки надійні і перевірені у виробництві ( відповідний вихідний код ).

    Першим здійснити це був Майкл Холл .

    Так, це магія, завдяки цим підходам ви можете досягти повністю динамічних додатків, моделей та полів Django за допомогою будь-якого резервного інтерфейсу бази даних. Але якою ціною? Чи погіршиться стабільність застосування при сильному використанні? Це питання, які слід розглянути. Ви повинні бути впевнені, щоб підтримувати належний замок , щоб дозволити одночасну зміну запитів бази даних.

    Якщо ви використовуєте lib Michael Halls, ваш код буде виглядати приблизно так:

    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
про цю тему нещодавно говорили на DjangoCon 2013 Europe: slideshare.net/schacki/… та youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf

Можливо, варто також зазначити, що використання django-pgjson на Postgres> = 9.2 дозволяє безпосередньо використовувати поле json postgresql. На Django> = 1.7, API фільтра для запитів відносно здоровий. Postgres> = 9.4 також дозволяє поля jsonb з кращими індексами для швидших запитів.
GDorn

1
Оновлено сьогодні, щоб відзначити прийняття Джанго HStoreField та JSONField у внесок. Він включає деякі віджети форми, які не є приголомшливими, але працюють, якщо вам потрібно налаштувати дані в адміністраторі.
GDorn

13

Я працював над просуванням ідеї джанго-динамо далі. Проект ще не зареєстрований, але ви можете прочитати код на веб- сайті https://github.com/charettes/django-mutant .

Насправді поля FK та M2M (див. Пов'язані з contrib.related) також працюють, і навіть можна визначити обгортку для власних спеціальних полів.

Існує також підтримка варіантів моделі, таких як unique_together та замовлення плюс бази моделей, щоб ви могли підкласити проксі-модель, реферат або міксин.

Я фактично працюю над механізмом блокування не в пам'яті, щоб переконатися, що визначення моделей можна ділити через декілька запущених екземплярів django, не дозволяючи їм використовувати застаріле визначення.

Проект ще дуже альфа, але це основна технологія для одного з моїх проектів, тому мені доведеться взяти його до виробництва готовим. Великий план також підтримує django-nonrel, щоб ми могли використовувати водія mongodb.


1
Привіт, Саймоне! Я включив посилання на ваш проект у свою вікі-відповідь відразу після того, як ви створили його на github. :))) Приємно бачити вас на stackoverflow!
Іван Харламов

4

Подальше дослідження виявляє, що це дещо особливий випадок дизайну вартості значення атрибутів суб'єктів господарювання , який був реалізований для Django за допомогою декількох пакетів.

По-перше, є оригінальний проект eav-django , який знаходиться на PyPi.

По-друге, є нещодавніший вил першого проекту, django-eav, який в першу чергу є рефактором, що дозволяє використовувати EAV з власними моделями або моделями django в сторонніх додатках.


Я включу його у вікі.
Іван Харламов

1
Я б заперечував навпаки, що EAV - це особливий випадок динамічного моделювання. Він широко використовується в спільноті "семантична павутина", де його називають "потрійним" або "чотирьох", якщо він включає унікальний ідентифікатор. Однак навряд чи коли-небудь буде настільки ефективним, як механізм, здатний динамічно створювати та змінювати таблиці SQL.
Серін

@GDom є ев-джанго ваш перший вибір? Я маю на увазі, який варіант ви обрали вище?
Морено

1
@Moreno Правильний вибір дуже сильно залежатиме від конкретного випадку використання. Я використовував і EAV, і JsonFields з різних причин. Останній зараз безпосередньо підтримується Django, тому для нового проекту я б скористався цим першим, якщо тільки у мене не виникне конкретної потреби мати можливість запиту в таблиці EAV. Зауважте, що ви також можете здійснювати запит на JsonFields.
GDorn
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.