我发现了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的一些用例冲突?