Поля динамической модели Django
Я работаю над многопользовательским приложением, в котором некоторые пользователи могут определять свои собственные поля данных (через администратора), чтобы собирать дополнительные данные в формах и сообщать о них. Последний бит делает 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, как указано выше, не очень хорошо работает с запросами.
3 ответа
На сегодняшний день существует четыре доступных подхода, два из которых требуют определенного хранилища:
Django-eav (оригинальная упаковка больше не ухожена, но имеет несколько процветающих вилок)
Это решение основано на модели данных Entity Attribute Value, по сути, оно использует несколько таблиц для хранения динамических атрибутов объектов. Большая часть этого решения заключается в том, что оно:
- использует несколько чистых и простых моделей Django для представления динамических полей, что делает его простым для понимания и независимым от базы данных;
позволяет эффективно подключать / отключать хранилище динамических атрибутов к модели Django с помощью простых команд, таких как:
eav.unregister(Encounter) eav.register(Patient)
В то же время быть действительно мощным.
Недостатки:
- Не очень эффективно. Это скорее критика самого шаблона 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)
Поля 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')
Или другие адаптации 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')] )
Джанго-мутант: динамические модели, основанные на syncdb и South-hooks
Django-мутант реализует полностью динамические поля 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', )
Я работал над продвижением идеи Джанго-Динамо. Проект по-прежнему недокументирован, но вы можете прочитать код по адресу https://github.com/charettes/django-mutant.
На самом деле поля FK и M2M (см. Contrib.related) также работают, и даже можно определить оболочку для ваших собственных настраиваемых полей.
Также есть поддержка опций модели, таких как unique_together и ordering plus Base Model, так что вы можете создавать подклассы моделей прокси, абстрактных или миксинов.
На самом деле я работаю над механизмом блокировки не в памяти, чтобы убедиться, что определения моделей могут быть общими для нескольких запущенных экземпляров django, не позволяя им использовать устаревшее определение.
Проект все еще очень альфа, но это краеугольная технология для одного из моих проектов, поэтому мне придется довести его до готовности. Большой план также поддерживает django-nonrel, чтобы мы могли использовать драйвер mongodb.
Дальнейшие исследования показывают, что это несколько особый случай шаблона проектирования Entity Attribute Value, который был реализован для Django несколькими пакетами.
Во-первых, это оригинальный проект eav-django, который находится на PyPi.
Во-вторых, есть более поздняя версия первого проекта, django-eav, которая в первую очередь является рефакторингом, позволяющим использовать EAV с собственными моделями или моделями django в сторонних приложениях.