Поля динамической модели Django - PullRequest
155 голосов
/ 28 октября 2011

Я работаю над мультитенантным приложением, в котором некоторые пользователи могут определять свои собственные поля данных (через администратора), чтобы собирать дополнительные данные в формах и сообщать о них. Последний бит делает 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 ]

264 голосов
/ 29 октября 2011

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

  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)
    

    В оболочке 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. Django MongoDB

    или другоеАдаптации 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 и южных хуках

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

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

    Сначала реализуйте это был Майкл Холл .

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

    Если вы используете 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',
                   )
    
13 голосов
/ 29 января 2012

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

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

Существует также поддержка опций модели, таких как unique_together и ordering plus Base, чтобы вы могли создавать подклассы моделей прокси, абстрактных или смешанных.

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

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

4 голосов
/ 18 ноября 2011

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

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

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

...