关于python:Django动态模型字段

Django dynamic model fields

我正在开发一个多租户应用程序,其中一些用户可以定义自己的数据字段(通过管理员)以收集表单中的其他数据并报告数据。后一位使JSONField不是一个很好的选择,所以我有以下解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 to Site - 每个站点将具有一组不同的自定义数据字段,但使用相同的数据库。
然后,各种具体数据字段可以定义为:

1
2
3
4
5
6
7
8
9
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'),)

这导致以下用途:

1
2
3
4
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,如上所列,因为它不能很好地处理查询。


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

  • Django-eav(最初的包裹不再保留,但有一些蓬勃发展的叉子)

    此解决方案基于实体属性值数据模型,实质上,它使用多个表来存储对象的动态属性。关于这个解决方案的重要部分是它:

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

    1
    2
    eav.unregister(Encounter)
    eav.register(Patient)
  • 很好地与Django管理员集成;

  • 同时真的很强大。

  • 缺点:

  • 不是很有效率。这更多地是对EAV模式本身的批评,这需要手动将数据从列格式合并到模型中的一组键值对。
  • 更难维护。维护数据完整性需要多列唯一键约束,这在某些数据库上可能效率低下。
  • 您将需要选择其中一个分叉,因为不再保留官方包,并且没有明确的领导者。
  • 用法非常简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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)
  • PostgreSQL中的Hstore,JSON或JSONB字段

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

    HStoreField:

    Django-hstore最初是第三方软件包,但Django 1.8添加了HStoreField作为内置函数,以及其他几个PostgreSQL支持的字段类型。

    从某种意义上说,这种方法很好,它可以让您充分利用这两个领域:动态字段和关系数据库。但是,hstore在性能方面并不理想,特别是如果您最终要在一个字段中存储数千个项目。它也只支持值的字符串。

    1
    2
    3
    4
    5
    #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中你可以像这样使用它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> 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字段发出索引查询:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 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,并且对于大型词典可能表现更好。它还支持字符串以外的类型,例如整数,布尔值和嵌套字典。

    1
    2
    3
    4
    5
    #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)

    在shell中创建:

    1
    2
    3
    4
    >>> instance = Something.objects.create(
                     name='something',
                     data={'a': 1, 'b': 2, 'nested': {'c':3}}
               )

    索引查询几乎与HStoreField完全相同,但嵌套是可能的。复杂索引可能需要手动创建(或脚本化迁移)。

    1
    2
    3
    >>> Something.objects.filter(data__a=1)
    >>> Something.objects.filter(data__nested__c=3)
    >>> Something.objects.filter(data__has_key='a')
  • Django MongoDB

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

    NoSQL Django库很棒,但请记住,它们不是100%兼容Django的,例如,从标准Django迁移到Django-nonrel,你需要用ListField替换ManyToMany等等。

    查看这个Django MongoDB示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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模型的嵌入式列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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实现了完全动态的外键和m2m字段。威尔·哈迪和迈克尔·霍尔的灵感来自于令人难以置信但有些神秘的解决方案。

    所有这些都基于Django South钩子,根据Will Hardy在DjangoCon 2011上的讲话(观看它!),它们在生产中都是强大的并经过测试(相关的源代码)。

    首先实施这个是Michael Hall。

    是的,这很神奇,通过这些方法,您可以使用任何关系数据库后端实现完全动态的Django应用程序,模型和字段。但是以什么代价?应用的稳定性会在大量使用时受到影响吗这些是需要考虑的问题。您需要确保维护正确的锁定,以便同时允许数据库更改请求。

    如果您使用的是Michael Halls lib,您的代码将如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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',
                   )
  • 好。


    我一直在努力进一步推动django-dynamo的想法。该项目仍未记录,但您可以在https://github.com/charettes/django-mutant上阅读代码。

    实际上FK和M2M字段(参见contrib.related)也可以工作,甚至可以为您自己的自定义字段定义包装器。

    还支持模型选项,例如unique_together和排序以及模型库,因此您可以子类化模型代理,抽象或混合。

    我实际上正在研究一种非内存中的锁定机制,以确保模型定义可以在多个django运行实例中共享,同时防止它们使用过时的定义。

    该项目仍然是非常阿尔法,但它是我的一个项目的基石技术,所以我将不得不把它准备好生产。最大的计划是支持django-nonrel,这样我们就可以利用mongodb驱动程序了。


    进一步的研究表明,这是实体属性值设计模式的一个特例,它已经通过几个包为Django实现。

    首先,有一个原始的eav-django项目,它位于PyPi上。

    其次,第一个项目django-eav是一个更新的分支,它主要是一个允许在第三方应用程序中使用django自己的模型或模型的EAV的重构。