Django动态模型字段


161

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

Answers:


277

到目前为止,有四种可用的方法,其中两种需要特定的存储后端:

  1. Django-eav(不再提供原始软件包,但有一些繁荣的fork

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

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

      eav.unregister(Encounter)
      eav.register(Patient)
    • 与Django admin很好地集成 ;

    • 同时真正强大。

    缺点:

    • 不太有效。这更多地是对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进行存储的内置方法。 JSONFieldHStoreField相似,并且在使用大字典时可能会表现更好。它还支持字符串以外的类型,例如整数,布尔值和嵌套字典。

    #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 迁移到Django-nonrel,您将需要用ListField替换ManyToMany 。

    看看这个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实现了完全动态的外键和m2m字段。灵感来自于Will Hardy和Michael Hall 令人难以置信但有些骇人听闻的解决方案。

    所有这些都基于Django South hooks,根据Will Hardy在DjangoCon 2011上的演讲 (观看!)仍然很健壮并已在生产中进行了测试(相关源代码)。

    首先实现这一点的迈克尔·霍尔

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

3
最近在DjangoCon 2013欧洲大会上讨论了这个主题:slideshare.net/schacki/…youtube.com/watch?v=67wcGdk4aCc
Aleck Landgraf

还可能值得注意的是,在Postgres> = 9.2上使用django-pgjson可以直接使用postgresql的json字段。在Django> = 1.7上,用于查询的过滤器API相对健全。Postgres> = 9.4还允许jsonb字段具有更好的索引,以实现更快的查询。
GDorn

1
今天进行了更新,以说明Django在contrib中采用了HStoreField和JSONField。它包含了一些不错的表单小部件,但是如果您需要在管理员中调整数据,则可以使用。
GDorn 2015年

13

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

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

还支持模型选项,例如unique_together和ordering以及Model基类,因此您可以将模型代理,抽象或混合作为子类。

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

该项目仍处于Alpha状态,但这是我的一个项目的基础技术,因此我必须将其投入生产准备。大型计划还支持django-nonrel,因此我们可以利用mongodb驱动程序。


1
嗨,西蒙!在github上创建项目后,就在Wiki答案中包含了指向您项目的链接。:)))很高兴在stackoverflow上见到您!
伊万·哈拉莫夫

4

进一步的研究表明,这是实体属性值设计模式的一种特殊情况,该模式已通过几个软件包为Django实现。

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

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


我将其包含在Wiki中。
伊万·哈拉莫夫

1
我反过来说,EAV是动态建模的特例。如果它包含唯一ID,则在“语义网”社区中被大量使用,被称为“三重”或“四元组”。但是,它不可能像动态创建和修改SQL表的机制那样高效。
Cerin 2012年

@GDom是eav-django的首选吗?我的意思是您选择了以上哪个选项?
Moreno

1
@Moreno正确的选择将在很大程度上取决于您的特定用例。由于不同的原因,我同时使用了EAV和JsonFields。后者现在由Django直接支持,因此对于新项目,我将首先使用它,除非我特别需要能够在EAV表上进行查询。请注意,您也可以在JsonFields上查询。
GDorn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.