Django 动态模型字段

发布于 2024-12-12 19:40:48 字数 1661 浏览 0 评论 0 原文

我正在开发一个多租户应用程序,其中一些用户可以定义自己的数据字段(通过管理员)以收集表单中的其他数据并报告数据。后一点使得 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 的外键 - 每个站点将有一组不同的自定义数据字段,但使用相同的数据库。 然后,各种具体数据字段可以定义为:

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

I'm working on a multi-tenanted application in which some users can define their own data fields (via the admin) to collect additional data in forms and report on the data. The latter bit makes JSONField not a great option, so instead I have the following solution:

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

Note how CustomDataField has a ForeignKey to Site - each Site will have a different set of custom data fields, but use the same database.
Then the various concrete data fields can be defined as:

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

This leads to the following use:

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?

But this feels very clunky, particularly with the need to manually create the related data and associate it with the concrete model. Is there a better approach?

Options that have been pre-emptively discarded:

  • Custom SQL to modify tables on-the-fly. Partly because this won't scale and partly because it's too much of a hack.
  • Schema-less solutions like NoSQL. I have nothing against them, but they're still not a good fit. Ultimately this data is typed, and the possibility exists of using a third-party reporting application.
  • JSONField, as listed above, as it's not going to work well with queries.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

孤蝉 2024-12-19 19:40:48

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

  1. Django-eav (原始包不再维护,但有一些蓬勃发展的分叉

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

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

      eav.unregister(遭遇)
      eav.register(患者)
      
    • 与 Django admin 很好地集成< /a>;

    • 同时又非常强大。

    缺点:

    用法非常简单:

    <前><代码>导入eav
    从 app.models 导入患者、遭遇

    eav.register(遭遇)
    eav.register(患者)
    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='国家', 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='是/否/未知')
    ynu.enums.add(self.yes)
    ynu.enums.add(self.no)
    ynu.enums.add(self.unkown)

    Attribute.objects.create(name='发烧', datatype=Attribute.TYPE_ENUM,\
    枚举组=ynu)

    # 当您在 EAV 中注册模型时,
    # 您可以访问所有 EAV 属性:

    Patient.objects.create(name='Bob', eav__age=12,
    eav__fever=否, eav__city='纽约',
    eav__country='美国')
    # 您可以根据 EAV 字段过滤查询:

    query1 = Patient.objects.filter(Q(eav__city__contains='Y'))
    query2 = Q(eav__city__contains='Y') | Q(eav__发烧=否)

  2. PostgreSQL 中的 Hstore、JSON 或 JSONB 字段

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

    HStoreField

    Django-hstore原本是第三方包,但是Django 1.8添加了 HStoreField 作为内置的,以及其他几种 PostgreSQL 支持的字段类型。

    从某种意义上说,这种方法很好,它可以让您两全其美:动态字段和关系数据库。然而,hstore 在性能方面并不理想,特别是如果您最终将在一个字段中存储数千个项目。它还仅支持值的字符串。

    <前><代码>#app/models.py
    从 django.contrib.postgres.fields 导入 HStoreField
    类 Something(models.Model):
    名称 = models.CharField(max_length=32)
    数据 = models.HStoreField(db_index=True)

    在 Django 的 shell 中你可以像这样使用它:

    <前><代码>>>>实例 = Something.objects.create(
    名称='某事',
    数据={'a':'1','b':'2'}

    >>>>>实例.data['a']
    ‘1’
    >>>>>空 = Something.objects.create(name='空')
    >>>>>空数据
    {}
    >>>>>空.data['a'] = '1'
    >>>>>清空.save()
    >>>>> Something.objects.get(name='something').data['a']
    ‘1’

    您可以对 hstore 字段发出索引查询:

    # 等价
    Something.objects.filter(data={'a': '1', 'b': '2'})
    
    # 按键/值映射的子集
    Something.objects.filter(data__a='1')
    
    # 按键列表的子集
    Something.objects.filter(data__has_keys=['a', 'b'])
    
    # 按单个键的子集
    Something.objects.filter(data__has_key='a')    
    

    JSONField

    JSON/JSONB 字段支持任何 JSON 可编码的数据类型,不仅仅是键/值对,而且往往比 Hstore 更快且(对于 JSONB)更紧凑。
    几个包实现了 JSON/JSONB 字段,包括 django-pgfields< /strong>,但从 Django 1.9 开始,JSONField是内置的使用JSONB进行存储。
    JSONField 与 HStoreField 类似,并且对于大型字典可能表现更好。它还支持字符串以外的类型,例如整数、布尔值和嵌套字典。

    <前><代码>#app/models.py
    从 django.contrib.postgres.fields 导入 JSONField
    类 Something(models.Model):
    名称 = models.CharField(max_length=32)
    数据 = JSONField(db_index=True)

    在 shell 中创建:

    <前><代码>>>>实例 = Something.objects.create(
    名称='某事',
    数据={'a': 1, 'b': 2, '嵌套': {'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-nonrel 来自标准 Django,您需要将 ManyToMany 替换为 ListField 等等。

    查看这个 Django MongoDB 示例:

    from djangotoolbox.fields import DictField
    
    图像类(模型.模型):
        exif = 字典字段()
    ...
    
    >>>>>图像 = Image.objects.create(exif=get_exif_data(...))
    >>>>>图像.exif
    {u'camera_model':'Spamcams 4242','exposure_time':0.3,...}
    

    您甚至可以创建任何 Django 模型的嵌入列表

    类容器(models.Model):
        东西 = ListField(EmbeddedModelField())
    
    类 FooModel(models.Model):
        foo = models.IntegerField()
    
    类 BarModel(models.Model):
        栏 = models.CharField()
    ...
    
    >>>>> Container.objects.create(
        stuff=[FooModel(foo=42), BarModel(bar='垃圾邮件')]
    )
    
  4. Django-mutant:基于syncdb和的动态模型南钩

    Django-mutant 实现完全动态的外键和 m2m 字段。其灵感来自 Will Hardy 和 Michael Hall 的令人难以置信但有些黑客的解决方案.

    所有这些都基于 Django South hooks,根据 威尔·哈迪 (Will Hardy) 在 DjangoCon 2011 上的演讲 (观看!) 仍然很强大,并且在生产中经过了测试(相关源代码)。

    首先实现此的是迈克尔·霍尔

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

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

    从发电机导入模型
    
    test_app,创建= models.DynamicApp.objects.get_or_create(
                          名称='发电机'
                        )
    测试,创建= models.DynamicModel.objects.get_or_create(
                      name='测试',
                      verbose_name='测试模型',
                      应用=测试应用
                   )
    foo, 创建 = models.DynamicModelField.objects.get_or_create(
                      名称 = 'foo',
                      verbose_name = 'Foo 字段',
                      模型=测试,
                      field_type = '动态字符字段',
                      空=真,
                      空白=真,
                      唯一=假,
                      help_text = 'Foo 的测试字段',
                   )
    栏,创建= models.DynamicModelField.objects.get_or_create(
                      名称 = '酒吧',
                      verbose_name = '栏字段',
                      模型=测试,
                      field_type = '动态整数字段',
                      空=真,
                      空白=真,
                      唯一=假,
                      help_text = '酒吧测试字段',
                   )
    

As of today, there are four available approaches, two of them requiring a certain storage backend:

  1. Django-eav (the original package is no longer mantained but has some thriving forks)

    This solution is based on Entity Attribute Value data model, essentially, it uses several tables to store dynamic attributes of objects. Great parts about this solution is that it:

    • uses several pure and simple Django models to represent dynamic fields, which makes it simple to understand and database-agnostic;
    • allows you to effectively attach/detach dynamic attribute storage to Django model with simple commands like:

      eav.unregister(Encounter)
      eav.register(Patient)
      
    • Nicely integrates with Django admin;

    • At the same time being really powerful.

    Downsides:

    • Not very efficient. This is more of a criticism of the EAV pattern itself, which requires manually merging the data from a column format to a set of key-value pairs in the model.
    • Harder to maintain. Maintaining data integrity requires a multi-column unique key constraint, which may be inefficient on some databases.
    • You will need to select one of the forks, since the official package is no longer maintained and there is no clear leader.

    The usage is pretty straightforward:

    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. Hstore, JSON or JSONB fields in PostgreSQL

    PostgreSQL supports several more complex data types. Most are supported via third-party packages, but in recent years Django has adopted them into django.contrib.postgres.fields.

    HStoreField:

    Django-hstore was originally a third-party package, but Django 1.8 added HStoreField as a built-in, along with several other PostgreSQL-supported field types.

    This approach is good in a sense that it lets you have the best of both worlds: dynamic fields and relational database. However, hstore is not ideal performance-wise, especially if you are going to end up storing thousands of items in one field. It also only supports strings for values.

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

    In Django's shell you can use it like this:

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

    You can issue indexed queries against hstore fields:

    # 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 fields support any JSON-encodable data type, not just key/value pairs, but also tend to be faster and (for JSONB) more compact than Hstore.
    Several packages implement JSON/JSONB fields including django-pgfields, but as of Django 1.9, JSONField is a built-in using JSONB for storage.
    JSONField is similar to HStoreField, and may perform better with large dictionaries. It also supports types other than strings, such as integers, booleans and nested dictionaries.

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

    Creating in the shell:

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

    Indexed queries are nearly identical to HStoreField, except nesting is possible. Complex indexes may require manually creation (or a scripted migration).

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

    Or other NoSQL Django adaptations -- with them you can have fully dynamic models.

    NoSQL Django libraries are great, but keep in mind that they are not 100% the Django-compatible, for example, to migrate to Django-nonrel from standard Django you will need to replace ManyToMany with ListField among other things.

    Checkout this Django MongoDB example:

    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, ...}
    

    You can even create embedded lists of any Django models:

    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: Dynamic models based on syncdb and South-hooks

    Django-mutant implements fully dynamic Foreign Key and m2m fields. And is inspired by incredible but somewhat hackish solutions by Will Hardy and Michael Hall.

    All of these are based on Django South hooks, which, according to Will Hardy's talk at DjangoCon 2011 (watch it!) are nevertheless robust and tested in production (relevant source code).

    First to implement this was Michael Hall.

    Yes, this is magic, with these approaches you can achieve fully dynamic Django apps, models and fields with any relational database backend. But at what cost? Will stability of application suffer upon heavy use? These are the questions to be considered. You need to be sure to maintain a proper lock in order to allow simultaneous database altering requests.

    If you are using Michael Halls lib, your code will look like this:

    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',
                   )
    
千纸鹤带着心事 2024-12-19 19:40:48

我一直致力于进一步推动 django-dynamo 的想法。该项目仍然没有文档记录,但您可以在 https://github.com/charettes/django-mutant 阅读代码。

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

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

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

该项目仍处于 alpha 阶段,但它是我的一个项目的基石技术,因此我必须将其投入生产。最大的计划是支持 django-nonrel,这样我们就可以利用 mongodb 驱动程序。

I've been working on pushing the django-dynamo idea further. The project is still undocumented but you can read the code at https://github.com/charettes/django-mutant.

Actually FK and M2M fields (see contrib.related) also work and it's even possible to define wrapper for your own custom fields.

There's also support for model options such as unique_together and ordering plus Model bases so you can subclass model proxy, abstract or mixins.

I'm actually working on a not in-memory lock mechanism to make sure model definitions can be shared accross multiple django running instances while preventing them using obsolete definition.

The project is still very alpha but it's a cornerstone technology for one of my project so I'll have to take it to production ready. The big plan is supporting django-nonrel also so we can leverage the mongodb driver.

像你 2024-12-19 19:40:48

进一步的研究表明,这是 Entity Attribute Value 的特殊情况 设计模式,已通过几个包为 Django 实现。

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

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

Further research reveals that this is a somewhat special case of Entity Attribute Value design pattern, which has been implemented for Django by a couple of packages.

First, there's the original eav-django project, which is on PyPi.

Second, there's a more recent fork of the first project, django-eav which is primarily a refactor to allow use of EAV with django's own models or models in third-party apps.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文