Django - 反向查询名称冲突

19

我在Django中设置了一个循环的外键,导致出现了名称冲突。我不明白为什么会出现这种冲突,也不知道如何最好地解决它。(这是我在模型中第一次遇到循环引用,所以我不确定是否可能是问题的根本原因?)

对于下面的内容:

class Supplier(models.Model):
    unique_id = models.IntegerField(unique=True)
    name = models.CharField(max_length=255, unique=True)
    rating = models.FloatField(null=True)
    last_updated = models.DateTimeField(auto_now=True)
    default_tariff = models.ForeignKey('Tariff')

class Tariff(models.Model):
    name = models.CharField(max_length=255)
    supplier = models.ForeignKey(Supplier)
    end_date = models.DateField(null=True, blank=True)
    payment_method = models.ManyToManyField(PaymentMethod) 
    region = models.ManyToManyField(Region)

错误信息为:

'Supplier.default_tariff' 的反向查询名称与字段名 'Tariff.supplier' 冲突。 提示:重命名字段 'Tariff.supplier',或为字段 'Supplier.default_tariff' 的定义添加/更改 related_name 参数。

3个回答

17
为避免这种情况,您可以使用related_name属性:
class Supplier(models.Model):
    unique_id = models.IntegerField(unique=True)
    name = models.CharField(max_length=255, unique=True)
    rating = models.FloatField(null=True)
    last_updated = models.DateTimeField(auto_now=True)
    default_tariff = models.ForeignKey('Tariff', related_name='+')

class Tariff(models.Model):
    name = models.CharField(max_length=255)
    supplier = models.ForeignKey(Supplier)
    end_date = models.DateField(null=True, blank=True)
    payment_method = models.ManyToManyField(PaymentMethod) 
    region = models.ManyToManyField(Region)

从评论中编辑:

当您设置related_name="+"时,Django不会创建反向关系。在这种情况下,Tariff将没有与Supplier模型的反向关系。


3
related_name="+"是什么意思?除了修复错误之外,会发生什么?哪些功能会停止工作? - Toskan
3
这个回答很糟糕,没有解释为什么会出现错误,也没有说明如何解决它。 - Silidrone
1
@Silidrone 你好,避免恶劣的评论,并用缺失的信息补充答案。干杯! - Gocht
是的,你说得对,当时我很沮丧,抱歉。 - Silidrone
简短解释:当您设置related_name="+"时,Django不会创建反向关系。在这种情况下,Tariff将不会与Supplier模型有反向关系。 - maczos
显示剩余2条评论

5

这个错误信息本身已经相当正确了(你忽略了输出中给出的错误代码,我想它是'E303'),但我们需要理解它实际意味着什么。

我不会问为什么在这种情况下不使用多对多关系,让我们关注“反向”的对象关系如何工作:

例如:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    def __str__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=100)
    def __str__(self):
        return self.headline

向“反向”追踪关系

一对多的关系不存在“正向”方式,因为它是持有键的“多方”,但Django为关系定义模型到相关模型的“另一方”创建了API访问器。例如,Blog对象b可以通过entry_set属性访问所有相关Entry对象的列表:b.entry_set.all()

...

如果一个模型具有ForeignKey,则外键模型的实例将具有返回第一个模型的所有实例的Manager 的访问权限。默认情况下,此Manager的名称为FOO_set,其中FOO是源模型名称的小写形式。此Manager返回QuerySets,可以按照“检索对象”部分中所述进行过滤和操作。

单击文档链接获取更多信息/示例。

正是由于这些API访问器,'related_name'(与抽象类中的related_name 配合使用)被实现,以避免字段名称与

  1. 访问器名称(这将是Manager的名称)之间的名称冲突, 和/或
  2. 查询名称(即模型名称)

让我们查看Django代码以更好地理解:

这里是您发生错误的位置:

def _check_clashes(self):
    """Check accessor and reverse query name clashes."""
    from django.db.models.base import ModelBase

    errors = []
    opts = self.model._meta

    # `f.remote_field.model` may be a string instead of a model. Skip if model name is
    # not resolved.
    if not isinstance(self.remote_field.model, ModelBase):
        return []

    # Consider that we are checking field `Model.foreign` and the models
    # are:
    #
    #     class Target(models.Model):
    #         model = models.IntegerField()
    #         model_set = models.IntegerField()
    #
    #     class Model(models.Model):
    #         foreign = models.ForeignKey(Target)
    #         m2m = models.ManyToManyField(Target)

    # rel_opts.object_name == "Target"
    rel_opts = self.remote_field.model._meta
    # If the field doesn't install a backward relation on the target model
    # (so `is_hidden` returns True), then there are no clashes to check
    # and we can skip these fields.
    rel_is_hidden = self.remote_field.is_hidden()
    rel_name = self.remote_field.get_accessor_name()  # i. e. "model_set"
    rel_query_name = self.related_query_name()  # i. e. "model"
    # i.e. "app_label.Model.field".
    field_name = '%s.%s' % (opts.label, self.name)

    # Check clashes between accessor or reverse query name of `field`
    # and any other field name -- i.e. accessor for Model.foreign is
    # model_set and it clashes with Target.model_set.
    potential_clashes = rel_opts.fields + rel_opts.many_to_many
    for clash_field in potential_clashes:
        # i.e. "app_label.Target.model_set".
        clash_name = '%s.%s' % (rel_opts.label, clash_field.name)
        if not rel_is_hidden and clash_field.name == rel_name:
            errors.append(
                checks.Error(
                    "Reverse accessor for '%s' clashes with field name '%s'." % (field_name, clash_name),
                    hint=("Rename field '%s', or add/change a related_name "
                          "argument to the definition for field '%s'.") % (clash_name, field_name),
                    obj=self,
                    id='fields.E302',
                )
            )

        if clash_field.name == rel_query_name:
            errors.append(
                checks.Error(
                    "Reverse query name for '%s' clashes with field name '%s'." % (field_name, clash_name),
                    hint=("Rename field '%s', or add/change a related_name "
                          "argument to the definition for field '%s'.") % (clash_name, field_name),
                    obj=self,
                    id='fields.E303',
                )
            )

    # Check clashes between accessors/reverse query names of `field` and
    # any other field accessor -- i. e. Model.foreign accessor clashes with
    # Model.m2m accessor.
    potential_clashes = (r for r in rel_opts.related_objects if r.field is not self)
    for clash_field in potential_clashes:
        # i.e. "app_label.Model.m2m".
        clash_name = '%s.%s' % (
            clash_field.related_model._meta.label,
            clash_field.field.name,
        )
        if not rel_is_hidden and clash_field.get_accessor_name() == rel_name:
            errors.append(
                checks.Error(
                    "Reverse accessor for '%s' clashes with reverse accessor for '%s'." % (field_name, clash_name),
                    hint=("Add or change a related_name argument "
                          "to the definition for '%s' or '%s'.") % (field_name, clash_name),
                    obj=self,
                    id='fields.E304',
                )
            )

        if clash_field.get_accessor_name() == rel_query_name:
            errors.append(
                checks.Error(
                    "Reverse query name for '%s' clashes with reverse query name for '%s'."
                    % (field_name, clash_name),
                    hint=("Add or change a related_name argument "
                          "to the definition for '%s' or '%s'.") % (field_name, clash_name),
                    obj=self,
                    id='fields.E305',
                )
            )

    return errors

这里是生成accessor_name的方法。

def get_accessor_name(self, model=None):
    # This method encapsulates the logic that decides what name to give an
    # accessor descriptor that retrieves related many-to-one or
    # many-to-many objects. It uses the lowercased object_name + "_set",
    # but this can be overridden with the "related_name" option. Due to
    # backwards compatibility ModelForms need to be able to provide an
    # alternate model. See BaseInlineFormSet.get_default_prefix().
    opts = model._meta if model else self.related_model._meta
    model = model or self.related_model
    if self.multiple:
        # If this is a symmetrical m2m relation on self, there is no reverse accessor.
        if self.symmetrical and model == self.model:
            return None
    if self.related_name:
        return self.related_name
    return opts.model_name + ('_set' if self.multiple else '')

而这是查询名称(也在related.py中)

def related_query_name(self):
    """
    Define the name that can be used to identify this related object in a
    table-spanning query.
    """
    return self.remote_field.related_query_name or self.remote_field.related_name or self.opts.model_name

现在,您已经了解了命名方式以及反向关系是什么,您将理解为什么会出现以下问题:

'Supplier.default_tariff'的反向查询名称与字段名'Tariff.supplier'冲突。

以及如何解决:

提示:重命名'Tariff.supplier'字段,或添加/更改related_name参数以用于'Supplier.default_tariff'字段定义。

希望这有所帮助。我刚处理完一个相关的问题))


1

尝试使用以下解决方案。它适用于django==3。

class Supplier(models.Model):
    unique_id = models.IntegerField(unique=True)
    name = models.CharField(max_length=255, unique=True)
    rating = models.FloatField(null=True)
    last_updated = models.DateTimeField(auto_now=True)
    default_tariff = models.ForeignKey('Tariff', related_name="%(class)s_tarriff")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接