Поля динамической модели Django

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

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, как указано выше, поскольку он не будет хорошо работать с запросами.

Ответ 1

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

  • Django-eav (исходный пакет больше не поддерживается, но имеет несколько процветающие вилки)

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

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

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Красиво интегрируется с администратором Django;

    • В то же время он действительно мощный.

    Downsides:

    • Не очень эффективно. Это скорее критика самого шаблона 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')
    
  • Django MongoDB

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

    Библиотеки Django NoSQL отличные, но имейте в виду, что они не на 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')]
    )
    
  • Django-mutant: Динамические модели, основанные на syncdb и South-hooks

    Django-mutant реализует полностью динамические поля Foreign Key и m2m. И вдохновляется невероятными, но довольно хакерскими решениями Will Hardy и Michael Hall.

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

    Сначала реализовать это было Michael Hall.

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

    Если вы используете 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',
                   )
    

Ответ 2

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

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

Там также поддерживаются параметры модели, такие как unique_together и ordering плюс базы моделей, поэтому вы можете подклассифицировать прокси-объекты модели, абстрактные или mixins.

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

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

Ответ 3

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

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

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