小编典典

Django 动态模型字段

all

我正在开发一个 多租户 应用程序,其中一些用户可以定义自己的数据字段(通过管理员)以收集表单中的其他数据并报告数据。后一点使 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,如上所述,因为它不适用于查询。

阅读 57

收藏
2022-08-20

共1个答案

小编典典

截至今天,有四种可用的方法,其中两种需要特定的存储后端:

  1. Django-eav (原来的包不再托管,但有一些 蓬勃发展的分叉

该解决方案基于实体属性值数据模型,本质上是使用多个表来存储对象的动态属性。这个解决方案的重要之处在于:

* 使用几个纯粹简单的 Django 模型来表示动态字段,这使其易于理解且与数据库无关;
* 允许您使用以下简单命令有效地将动态属性存储附加/分离到 Django 模型:

            eav.unregister(Encounter)
    eav.register(Patient)


* **[与 Django admin 完美集成](https://github.com/mvpdev/django-eav/blob/master/eav/admin.py)** ;

* 同时真的很强大。

缺点:

* 效率不是很高。这更像是对 EAV 模式本身的批评,它需要手动将数据从列格式合并到模型中的一组键值对。
* 更难维护。维护数据完整性需要多列唯一键约束,这在某些数据库上可能效率低下。
* 您将需要选择[其中一个 fork](https://github.com/mvpdev/django-eav/network),因为不再维护官方包并且没有明确的领导者。

用法非常简单:

    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)
  1. PostgreSQL 中的 Hstore、JSON 或 JSONB 字段

PostgreSQL 支持几种更复杂的数据类型。大多数都通过第三方包支持,但近年来 Django 已将它们纳入
django.contrib.postgres.fields。

HStore 字段

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 的 shell 中,你可以像这样使用它:

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

JSON字段

JSON/JSONB 字段支持任何可 JSON 编码的数据类型,不仅是键/值对,而且往往比 Hstore 更快且(对于 JSONB)更紧凑。几个包实现了
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')
  1. Django MongoDB

或者其他 NoSQL Django 改编——有了它们,你可以拥有完全动态的模型。

NoSQL Django 库很棒,但请记住,它们不是 100% 与 Django 兼容的,例如,要从标准 Django 迁移到Django-
nonrel
,您需要将
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')]
)
  1. Django-mutant:基于 syncdb 和 South-hooks 的动态模型

Django-mutant实现了完全动态的外键和 m2m
字段。并且受到Will Hardy和 Michael Hall
令人难以置信但有些骇人听闻的解决方案的启发。

所有这些都基于 Django South 钩子,根据Will Hardy 在 DjangoCon 2011
上的演讲

(观看它!) ,它仍然是健壮的并且在生产中经过测试(相关源代码)。

首先实现这一点的是Michael
Hall

是的,这很神奇,通过这些方法,您可以使用任何关系数据库后端实现 完全动态的 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',
               )
2022-08-20