13

我有一个旧数据库,其中有一个表示文件系统中节点的表。节点类型很少,例如 A、B、C,不同类型的节点具有不同的属性。在当前的数据库设计中,有一个表保存有关节点的信息。如果节点属于 A 类型,则仅设置与 A 类型相关的字段。现在我想将类型 A、B、C 表示为模型。出现的问题是:

  1. 我想有这样的行为,所有三种类型都有一个 name 属性。我想通过 name 属性过滤文件系统中的所有节点并获取良好类型的对象列表。

  2. 每个节点作为父链接,在数据库中表示为外键,因此可能应该发生某种形式的继承。

在django中可以吗?

4

2 回答 2

8

对的,这是可能的。这是一个例子:

模型.py

from django.db import models

# Create your models here.
class NodeA(models.Model):

    name_a = models.CharField(max_length=75, blank=True, null=True)

    class Meta:
        db_table = 'Nodes'
        managed = False

class NodeB(models.Model):

    name_b = models.CharField(max_length=75, blank=True, null=True)

    class Meta:
        db_table = 'Nodes'
        managed = False

class NodeC(models.Model):

    name_c = models.CharField(max_length=75, blank=True, null=True)

    class Meta:
        db_table = 'Nodes'
        managed = False

数据库架构 (SQLITE)

 Nodes {
    id        integer   primary key
    name_a    TEXT
    name_b    TEXT
    name_c    TEXT }

概念证明

import NodeA, NodeB, NodeC

a = NodeA()
a.name_a = 'Node A'
a.save()

b = NodeB()
b.name_b = 'Node B'
b.save()

c = NodeC()
c.name_c = 'Node C'
c.save()

这会产生:

id        name_a        name_b        name_c
1         Node A
2                       Node B
3                                     Node C
于 2012-12-11T16:06:12.530 回答
6

我使用了一种稍微不同的方法,它通过创建一个视角与南方很好地配合。透视图是一个代理,它重命名模型中的某些字段,但保留列的名称。

对我来说,这是一个展示 django ORM 灵活性的例子。我不确定您是否想在生产代码中使用它。因此它没有经过足够的测试,但它会给你一些想法。

这个想法

透视图让用户为一个表创建不同的模型,该表可以有自己的方法和不同的字段名称,但共享底层模型和表。

它可以在同一个表中存储不同的类型,这对于日志记录或事件系统来说非常方便。每个透视图只能看到它自己的条目,因为它是根据字段名action_type过滤的。

这些模型是非托管的,但有一个自定义管理器,因此 south 不会为其创建新表。

用法

实现是一个类装饰器,它修改django模型的元数据。它接受一个“基本”模型和一个别名字段字典。

我们先来看一个例子:

class UserLog(models.Model):
"""
A user action log system, user is not in this class, because it clutters import
"""
date_created = models.DateTimeField(_("Date created"), auto_now_add=True)

# Action type is obligatory

action_type = models.CharField(_("Action Type"), max_length=255)
integer_field1 = models.IntegerField()
integer_field2 = models.IntegerField()
char_field1 = models.CharField(max_length=255)
char_field2 = models.CharField(max_length=255)


@ModelPerspective({
    'x': 'integer_field1',
    'y': 'integer_field2',
    'target': 'char_field1'
}, UserLog)
class UserClickLog(models.Model):
    pass

这将创建一个模型,该模型将属性 x 映射到 integer_field1,将 y 映射到 integer_field2,并将目标映射到 char_field1,其中基础表与作为 UserLog 的表相同。

用法与任何其他模型没有什么不同,而南只会创建 UserLog 表。

现在让我们看看如何实现它。

执行

它是如何工作的?

如果该类被评估,则装饰器接收该类。这将对类进行猴子修补,因此它的实例将反映您提供的基表。

添加别名

如果我们更深入地研究代码。读取别名字典,并为每个字段查找基本字段。如果我们在基表中找到该字段,则会更改名称。这有一个小的副作用,它也会改变列。所以我们必须从基础字段中检索字段列。然后使用contribute_to_class方法将该字段添加到类中,该方法负责所有簿记。

然后将所有未别名的属性添加到模型中。这本身不是必需的,但我选择添加它们。

设置属性

现在我们有了所有的字段,我们必须设置几个属性。托管属性会欺骗南方忽略表格,但它有副作用。班级将没有经理。(我们稍后会解决这个问题)。我们还从基本模型中复制表名(db_table)并将 action_type 字段默认为类名。

我们需要做的最后一件事是提供一个经理。必须小心,因为 django 声明只有一个 QuerySet 管理器。我们通过使用 deepcopy 复制管理器来解决这个问题,然后添加一个过滤器语句,该语句过滤类名。

deepcopy(QuerySet()).filter( action_type = cls.class.name )

这让我们的表只返回相关记录。现在把它包进一个装饰器就完成了。

这是代码:

from django.db import models
from django.db.models.query import QuerySet

def ModelPerspective(aliases, model):
  """
  This class decorator creates a perspective from a model, which is
  a proxy with aliased fields.

  First it will loop over all provided aliases
  these are pairs of new_field, old_field.
  Then it will copy the old_fields found in the
  class to the new fields and change their name,
  but keep their columnnames.

  After that it will copy all the fields, which are not aliased.

 Then it will copy all the properties of the model to the new model.

  Example:
    @ModelPerspective({
        'lusername': 'username',
        'phonenumber': 'field1'
    }, User)
    class Luser(models.Model):
        pass

  """
  from copy import deepcopy

  def copy_fields(cls):

    all_fields = set(map(lambda x: x.name, model._meta.fields))
    all_fields.remove('id')
    # Copy alias fields

    for alias_field in aliases:

        real_field = aliases[alias_field]

        # Get field from model
        old_field = model._meta.get_field(real_field)
        oldname, columnname = old_field.get_attname_column()
        new_field = deepcopy(old_field)

        # Setting field properties
        new_field.name = alias_field
        new_field.db_column = columnname
        new_field.verbose_name = alias_field

        new_field.contribute_to_class(cls, "_%s" % alias_field)
        all_fields.remove(real_field)

    for field in all_fields:
        new_field = deepcopy(model._meta.get_field(field))
        new_field.contribute_to_class(cls, "_%s" % new_field.name)

  def copy_properties(cls):
    # Copy db table
    cls._meta.db_table = model._meta.db_table


  def create_manager(cls):
    from copy import deepcopy
    field = cls._meta.get_field('action_type')
    field.default = cls.__name__
    # Only query on relevant records
    qs = deepcopy(cls.objects)
    cls.objects = qs.filter(action_type=cls.__name__)

  def wrapper(cls):

    # Set it unmanaged
    cls._meta.managed = False

    copy_properties(cls)
    copy_fields(cls)
    create_manager(cls)

    return cls
  return wrapper

这准备好生产了吗?

我不会在生产代码中使用它,对我来说这是一个展示 django 灵活性的练习,但是如果有足够的测试,它可以在代码中使用。

反对在生产中使用的另一个论点是代码使用了大量 django ORM 的内部工作。我不确定 api 是否足够稳定。

而且这个解决方案不是你能想到的最好的解决方案。有更多的可能性来解决在数据库中存储动态字段的问题。

于 2013-10-23T19:28:29.483 回答