Поля динамической модели Джанго

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, как указано выше, не очень хорошо работает с запросами.
GDorn
источник
6
Преимущественно, это не любой из этих вопросов: stackoverflow.com/questions/7801729/… stackoverflow.com/questions/2854656/…
GDorn

Ответы:

278

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

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

    Это решение основано на модели данных Entity Attribute Value , по сути, оно использует несколько таблиц для хранения динамических атрибутов объектов. Отличительной чертой этого решения является то, что оно:

    • использует несколько чистых и простых моделей 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)
    

    В оболочке Django вы можете использовать это так:

    >>> 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. Джанго МонгоДБ

    Или другие адаптации NoSQL Django - с ними вы можете иметь полностью динамические модели.

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

    Проверьте этот пример 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, ...}
    

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

    Джанго-мутант реализует полностью динамические поля Foreign Key и m2m. И вдохновлен невероятными, но несколько хакерскими решениями Уиллом Харди и Майклом Холлом.

    Все они основаны на хуках Django South, которые, согласно докладу Уилла Харди на DjangoCon 2011 (смотрите!) , Тем не менее, надежны и протестированы в производстве ( соответствующий исходный код ).

    Первым для реализации этого был Майкл Холл .

    Да, это волшебство, с помощью этих подходов вы можете создавать полностью динамические приложения, модели и поля Django с любым бэкэндом реляционной базы данных. Но какой ценой? Будет ли стабильность приложения ухудшаться при интенсивном использовании? Это вопросы для рассмотрения. Вы должны быть уверены, что сохраняете правильную блокировку , чтобы позволить одновременные запросы на изменение базы данных.

    Если вы используете Michael Halls lib, ваш код будет выглядеть так:

    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
Алек Ландграф
Также стоит отметить, что использование django-pgjson в Postgres> = 9.2 позволяет напрямую использовать поле json в postgresql. При Django> = 1.7 API-фильтр для запросов является относительно нормальным. Postgres> = 9.4 также позволяет использовать поля jsonb с лучшими индексами для более быстрых запросов.
GDorn
1
Обновлено сегодня, чтобы отметить принятие Django HStoreField и JSONField в contrib. Он включает в себя некоторые виджеты форм, которые не удивительны, но работают, если вам нужно настроить данные в админке.
GDorn
13

Я работал над продвижением идеи Джанго-Динамо. Проект по-прежнему недокументирован, но вы можете прочитать код по адресу https://github.com/charettes/django-mutant .

На самом деле поля FK и M2M (см. Contrib.related) также работают, и даже можно определить оболочку для ваших собственных настраиваемых полей.

Также есть поддержка опций модели, таких как unique_together и ordering plus Основы модели, так что вы можете создавать подклассы моделей прокси, абстракций или миксинов.

На самом деле я работаю над механизмом блокировки не в памяти, чтобы убедиться, что определения моделей могут быть общими для нескольких запущенных экземпляров django, не позволяя им использовать устаревшее определение.

Проект все еще очень альфа, но это краеугольная технология для одного из моих проектов, поэтому мне придется довести его до готовности. Большой план также поддерживает django-nonrel, чтобы мы могли использовать драйвер mongodb.

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

Дальнейшие исследования показывают, что это несколько особый случай шаблона проектирования Entity Attribute Value , который был реализован для Django несколькими пакетами.

Во-первых, это оригинальный проект eav-django , который находится на PyPi.

Во-вторых, есть более поздняя версия первого проекта, django-eav, которая в первую очередь является рефакторингом, позволяющим использовать EAV с собственными моделями или моделями django в сторонних приложениях.

GDorn
источник
Я включу это в вики.
Иван Харламов
1
С другой стороны, я бы сказал, что EAV - это особый случай динамического моделирования. Он широко используется в сообществе «семантическая сеть», где его называют «тройным» или «четырехугольным», если он включает уникальный идентификатор. Однако вряд ли он когда-либо будет столь же эффективным, как механизм, который может динамически создавать и изменять таблицы SQL.
Cerin
@GDom - это твой первый выбор? Я имею в виду, какой вариант выше вы выбрали?
Морено
1
@ Морено Правильный выбор будет сильно зависеть от вашего конкретного случая использования. Я использовал EAV и JsonFields по разным причинам. Последнее теперь напрямую поддерживается Django, поэтому для нового проекта я бы использовал его первым, если у меня не было особой необходимости иметь возможность запрашивать таблицу EAV. Обратите внимание, что вы можете делать запросы и в JsonFields.
GDorn