在Django 1.8 / 1.9

时间:2016-03-07 09:13:00

标签: mysql django django-models

我发现了MySQL 5.7的新generated columns功能,并希望用这些列替换我模型的某些属性。以下是模型的示例:

class Ligne_commande(models.Model):
    Quantite = models.IntegerField()
    Prix = models.DecimalField(max_digits=8, decimal_places=3)
    Discount = models.DecimalField(max_digits=5, decimal_places=3, blank=True, null=True)

@property
def Prix_net(self):
    if self.Discount:
        return (1 - self.Discount) * self.Prix
    return self.Prix
@property
def Prix_total(self):
    return self.Quantite * self.Prix_net

我将生成的字段类定义为Django字段的子类(例如GeneratedDecimalField作为DecimalField的子类)。这在一个只读上下文中工作,Django迁移正确处理它,除了一个细节:生成的MySQL列不支持前向引用,django迁移不尊重字段在模型中定义的顺序,因此迁移文件必须是编辑以重新排序操作。

之后,尝试创建或修改实例返回了mysql错误:'error totally whack'。我想Django试图编写生成的字段,MySQL不喜欢这样。在看了django代码后,我意识到,在最低级别,django使用_meta.local_concrete_fields列表并将其发送到MySQL。从此列表中删除生成的字段可以解决问题。

我遇到了另一个问题:在修改实例期间,生成的字段不会反映对计算它们的字段所做的更改。如果在实例修改期间使用生成的字段,就像我的情况一样,这是有问题的。为了解决这个问题,我创建了一个“生成的字段描述符”。

这是所有这些的最终代码。

在模型中创建生成的字段,替换上面定义的属性:

Prix_net = mk_generated_field(models.DecimalField, max_digits=8, decimal_places=3,
    sql_expr='if(Discount is null, Prix, (1.0 - Discount) * Prix)',
    pyfunc=lambda x: x.Prix if not x.Discount else (1 - x.Discount) * x.Prix)
Prix_total = mk_generated_field(models.DecimalField, max_digits=10, decimal_places=2,
    sql_expr='Prix_net * Quantite',
    pyfunc=lambda x: x.Prix_net * x.Quantite)

创建生成字段的函数。为简单起见,动态创建类:

from django.db.models import fields
def mk_generated_field(field_klass, *args, sql_expr=None, pyfunc=None, **kwargs):
    assert issubclass(field_klass, fields.Field)
    assert sql_expr

    generated_name = 'Generated' + field_klass.__name__
    try:
        generated_klass = globals()[generated_name]
    except KeyError:
        globals()[generated_name] = generated_klass = type(generated_name, (field_klass,), {})

        def __init__(self, sql_expr, pyfunc=None, *args, **kwargs):
            self.sql_expr = sql_expr
            self.pyfunc = pyfunc
            self.is_generated = True # mark the field
            # null must be True otherwise migration will ask for a default value
            kwargs.update(null=True, editable=False)
            super(generated_klass, self).__init__(*args, **kwargs)

        def db_type(self, connection):
            assert connection.settings_dict['ENGINE'] == 'django.db.backends.mysql'
            result = super(generated_klass, self).db_type(connection)
            # double single '%' if any because it will clash with later Django format
            sql_expr = re.sub('(?<!%)%(?!%)', '%%', self.sql_expr)
            result += ' GENERATED ALWAYS AS (%s)' % sql_expr
            return result

        def deconstruct(self):
            name, path, args, kwargs = super(generated_klass, self).deconstruct()
            kwargs.update(sql_expr=self.sql_expr)
            return name, path, args, kwargs

        generated_klass.__init__ = __init__
        generated_klass.db_type = db_type
        generated_klass.deconstruct = deconstruct
    return generated_klass(sql_expr, pyfunc, *args, **kwargs)

在模型中注册生成的字段的功能。必须在django启动时调用它,例如在应用程序ready的{​​{1}}方法中。

AppConfig

描述符。请注意,仅在为字段定义pyfunc时才使用它。

from django.utils.datastructures import ImmutableList
def register_generated_fields(model):
    local_concrete_fields = list(model._meta.local_concrete_fields[:])
    generated_fields = []
    for field in model._meta.fields:
        if hasattr(field, 'is_generated'):
            local_concrete_fields.remove(field)
            generated_fields.append(field)
            if field.pyfunc:
                setattr(model, field.name, GeneratedFieldDescriptor(field.pyfunc))
    if generated_fields:
        model._meta.local_concrete_fields = ImmutableList(local_concrete_fields)

请注意必须告知实例是否正在被修改的class GeneratedFieldDescriptor(object): attr_prefix = '_GFD_' def __init__(self, pyfunc, name=None): self.pyfunc = pyfunc self.nickname = self.attr_prefix + (name or str(id(self))) def __get__(self, instance, owner): if instance is None: return self if hasattr(instance, self.nickname) and not instance.has_changed: return getattr(instance, self.nickname) return self.pyfunc(instance) def __set__(self, instance, value): setattr(instance, self.nickname, value) def __delete__(self, instance): delattr(instance, self.nickname) 。如果找到了此here的解决方案。 我已经对我的应用程序进行了大量测试,它工作正常,但我远没有使用所有的django功能。我的问题是:这个设置是否会与django的一些用例冲突?

0 个答案:

没有答案