Django 模型字段默认基于同一模型中的另一个字段

Posted

技术标签:

【中文标题】Django 模型字段默认基于同一模型中的另一个字段【英文标题】:Django Model Field Default Based Off Another Field in Same Model 【发布时间】:2011-05-21 19:41:31 【问题描述】:

我有一个模型,我想包含一个主题名称和他们的首字母缩写(他的数据有些匿名并通过首字母缩写进行跟踪)。

现在,我写了

class Subject(models.Model):

    name = models.CharField("Name", max_length=30)
    def subject_initials(self):
        return ''.join(map(lambda x: '' if len(x)==0 else x[0],
                           self.name.split(' ')))
    # Next line is what I want to do (or something equivalent), but doesn't work with
    # NameError: name 'self' is not defined
    subject_init = models.CharField("Subject Initials", max_length=5, default=self.subject_initials)

如最后一行所示,我希望能够将首字母作为字段(独立于名称)实际存储在数据库中,但它是使用基于名称字段的默认值初始化的。但是,我遇到了问题,因为 django 模型似乎没有“自我”。

如果我将行改为subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials),我可以做syncdb,但不能创建新的主题。

这在 Django 中是否可能,让一个可调用函数根据另一个字段的值为某个字段提供默认值?

(出于好奇,我想单独分开我的商店首字母的原因是在极少数情况下奇怪的姓氏可能与我正在跟踪的姓氏不同。例如,其他人决定主题 1 命名为“John O'Mallory " 首字母是 "JM" 而不是 "JO" 并且想修复以管理员身份编辑它。)

【问题讨论】:

【参考方案1】:

模特当然有“自我”!只是您试图将模型类的属性定义为依赖于模型实例;这是不可能的,因为在您定义类及其属性之前,该实例不(也不能)存在。

为了得到你想要的效果,重写模型类的save()方法。对实例进行必要的更改,然后调用超类的方法进行实际保存。这是一个简单的例子。

def save(self, *args, **kwargs):
    if not self.subject_init:
        self.subject_init = self.subject_initials()
    super(Subject, self).save(*args, **kwargs)

这在文档中的Overriding Model Methods 中有介绍。

【讨论】:

请注意,在 Python 3 中,您可以像参考文档中的示例一样简单地调用 super().save(*args, **kwargs)(不带 Subject, self 参数)。 太糟糕了,这不允许基于另一个定义默认模型属性值,这在管理方面特别有用(例如,对于自动增量字段),因为它在保存时处理它它。还是我理解错了? 如果您要将字段添加到具有现有数据的模型,则可能需要使用数据迁移更新现有记录。【参考方案2】:

不知道有没有更好的办法,不过你可以use a signal handler换the pre_save signal:

from django.db.models.signals import pre_save

def default_subject(sender, instance, using):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

pre_save.connect(default_subject, sender=Subject)

【讨论】:

您导入了 post_save 而不是 pre_save @arkocal:感谢您的提醒,刚刚修复了它。在这种情况下,您可以自己提出修改建议,它有助于解决这样的问题 :) 这个实现似乎缺少**kwargs 参数,根据docs.djangoproject.com/en/2.0/topics/signals/…,所有接收器函数都应该具有该参数? 文档推荐against signals for code paths you control。覆盖save() 的公认答案可能更改为覆盖__init__,因为它更明确。【参考方案3】:

使用Django signals,这可以通过从模型接收the post_init signal 来完成。

from django.db import models
import django.dispatch

class LoremIpsum(models.Model):
    name = models.CharField(
        "Name",
        max_length=30,
    )
    subject_initials = models.CharField(
        "Subject Initials",
        max_length=5,
    )

@django.dispatch.receiver(models.signals.post_init, sender=LoremIpsum)
def set_default_loremipsum_initials(sender, instance, *args, **kwargs):
    """
    Set the default value for `subject_initials` on the `instance`.

    :param sender: The `LoremIpsum` class that sent the signal.
    :param instance: The `LoremIpsum` instance that is being
        initialised.
    :return: None.
    """
    if not instance.subject_initials:
        instance.subject_initials = "".join(map(
                (lambda x: x[0] if x else ""),
                instance.name.split(" ")))

post_init 信号由类在实例上完成初始化后发送。这样,实例会在测试其不可为空的字段是否已设置之前获取 name 的值。

【讨论】:

文档推荐against signals for code paths you control。覆盖save() 的公认答案可能更改为覆盖__init__,因为它更明确。【参考方案4】:

作为Gabi Purcaru 答案的替代实现,您还可以使用receiver decorator 连接到pre_save 信号:

from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=Subject)
def default_subject(sender, instance, **kwargs):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

此接收器函数还采用**kwargs 通配符关键字参数,所有信号处理程序都必须根据https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions 采用。

【讨论】:

以上是关于Django 模型字段默认基于同一模型中的另一个字段的主要内容,如果未能解决你的问题,请参考以下文章

确保字段对于 Django 模型中的另一个字段是唯一的

根据同一模型中的另一个外键动态限制 Django 模型中外键的​​选择

有条件地只需要 Django 模型表单中的一个字段

如何在Django中基于枚举为模型字段设置默认值?

如何根据具有外键关系的另一个模型更新 Django 模型的字段

Django数据模型——通用字段选项