Django 动态模型字段

186 投票
3 回答
91365 浏览
提问于 2025-04-17 05:12

我正在开发一个多租户的应用程序,允许一些用户通过管理员界面定义自己的数据字段,以便在表单中收集额外的数据并进行报告。后面这部分让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有一个指向Site的外键 - 每个Site会有一组不同的自定义数据字段,但它们会使用同一个数据库。然后可以定义各种具体的数据字段,如下所示:

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 个回答

4

进一步研究发现,这其实是一个比较特殊的设计模式,叫做实体-属性-值模式。这个模式已经有一些包为Django实现了。

首先,有一个叫做eav-django的项目,它在PyPi上可以找到。

其次,还有一个比较新的版本,叫做django-eav,这个项目主要是对第一个项目的改进,让它可以和Django自己的模型或者第三方应用中的模型一起使用。

13

我一直在推动django-dynamo这个想法。这个项目还没有文档,但你可以在这里查看代码:https://github.com/charettes/django-mutant

其实,外键(FK)和多对多(M2M)字段(可以在contrib.related找到)也能正常工作,甚至可以为你自己定义的自定义字段创建一个包装器。

这个项目还支持一些模型选项,比如唯一组合(unique_together)和排序(ordering),还有模型基础类,这样你就可以继承模型代理、抽象类或混合类。

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

这个项目还处于非常早期的阶段,但它是我一个项目的基础技术,所以我必须把它准备好投入生产。大计划是也支持django-nonrel,这样我们就可以利用mongodb的驱动。

300

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

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

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

    JSONField

    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)
    

    在shell中创建:

    >>> 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迁移到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')]
    )
    
  4. Django-mutant:基于syncdb和South-hooks的动态模型

    Django-mutant实现了完全动态的外键和多对多字段。它的灵感来自于Will Hardy和Michael Hall的一些非常棒但有点黑科技的解决方案。

    所有这些都基于Django South hooks,正如Will Hardy在DjangoCon 2011的演讲中提到的(一定要看!),这些方法在生产环境中经过了严格测试(相关源代码)。

    首先实现这个的是Michael Hall

    没错,这简直是魔法,使用这些方法你可以实现完全动态的Django应用、模型和字段,并且可以使用任何关系型数据库后端。但代价是什么呢?在高负载下,应用的稳定性会受到影响吗?这些都是需要考虑的问题。你需要确保维护一个合适的,以允许同时进行数据库修改请求。

    如果你使用Michael Hall的库,你的代码看起来会像这样:

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

撰写回答