使用 get_or_create 返回的 MultipleObjects

Posted

技术标签:

【中文标题】使用 get_or_create 返回的 MultipleObjects【英文标题】:MultipleObjectsReturned with get_or_create 【发布时间】:2013-07-31 09:37:12 【问题描述】:

我正在编写一个小的 django 命令来将数据从 json API 端点复制到 Django 数据库中。在我实际创建对象时,使用obj, created = model.objects.get_or_create(**filters),我收到MultipleObjectsReturned 错误。这让我很惊讶,因为我对get_or_create 的理解是,如果我尝试创建一个已经存在的对象,它只会“获取”它。

我不确定我正在克隆的数据库的完整性,但即使其中有多个相同的对象,当我将它们加载到我的本地 Django 数据库中时,get_or_create 也不应该这样做,这样我就永远不会获得多个副本?

谁能解释一下?我很乐意提供更多细节,我只是不想让读者陷入困境。

【问题讨论】:

get 搜索unique 元素如果有更多elements 会引发错误MultipleObjectsReturned,过滤搜索大量elements 并返回其中的list . 比较在这里***.com/questions/1541249/… 【参考方案1】:

示例代码

假设您有以下模型:

class DictionaryEntry(models.Model):
    name = models.CharField(max_length=255, null=False, blank=False)
    definition = models.TextField(null=True, blank=False)

以及以下代码:

obj, created = DictionaryEntry.objects.get_or_create(
    name='apple', definition='some kind of fruit')

get_or_create

如果你还没有看到code for get_or_create:

 # simplified
 def get_or_create(cls, **kwargs):
     try:
         instance, created = cls.get(**kwargs), False
     except cls.DoesNotExist:
         instance, created = cls.create(**kwargs), True
     return instance, created

关于网络服务器...

现在假设您有一个带有2 工作进程的网络服务器,它们都有自己的并发访问数据库。

 # simplified
 def get_or_create(cls, **kwargs):
     try:
         instance, created = cls.get(**kwargs), False # <===== nope not there...
     except cls.DoesNotExist:
         instance, created = cls.create(**kwargs), True
     return instance, created

如果时机正确(或错误,取决于您要如何表达),两个进程都可以进行查找但找不到该项目。他们都可以创建项目。一切都很好……

MultipleObjectsReturned: get() returned more than one KeyValue -- it returned 2!

一切都很好......直到您第三次致电get_or_create,他们说“第三次是一种魅力”。

 # simplified
 def get_or_create(cls, **kwargs):
     try:
         instance, created = cls.get(**kwargs), False # <==== kaboom, 2 objects.
     except cls.DoesNotExist:
         instance, created = cls.create(**kwargs), True
     return instance, created

unique_together

你怎么能解决这个问题?也许在数据库级别强制执行约束:

class DictionaryEntry(models.Model):
    name = models.CharField(max_length=255, null=False, blank=False)
    definition = models.TextField(null=True, blank=False)
    class Meta:
        unique_together = (('name', 'definition'),)

回到函数:

 # simplified
 def get_or_create(cls, **kwargs):
     try:
         instance, created = cls.get(**kwargs), False
     except cls.DoesNotExist:
         instance, created = cls.create(**kwargs), True # <==== this handles IntegrityError
     return instance, created

假设你和之前的比赛一样,他们都没有找到该项目并继续插入;这样做他们将开始交易,其中一个将赢得比赛,而另一个将看到IntegrityError

mysql ?

该示例使用TextField,对于mysql,它转换为LONGTEXT(在我的例子中)。添加unique_together 约束会使syncdb 失败。

django.db.utils.InternalError: (1170, u"BLOB/TEXT column 'definition' used in key specification without a key length")

所以,运气不好,您可能不得不手动处理MultipleObjectsReturned

https://code.djangoproject.com/ticket/2495 https://code.djangoproject.com/ticket/12579 http://django.readthedocs.org/en/latest/topics/db/transactions.html#using-a-high-isolation-level https://docs.djangoproject.com/en/dev/topics/db/transactions/#django.db.transaction.atomic

可能的解决方案

可以将TextField 替换为CharField。 可以添加一个CharField,它可能是TextField 的强哈希,您可以在pre_save 中计算并在unique_together 中使用。

【讨论】:

【参考方案2】:

顾名思义,get_or_createmodel.objects.get()s 或model.objects.create()s。

它在概念上等同于:

try:
   model.objects.get(pk=1)
except model.DoesNotExist:
   model.objects.create(pk=1)

来源是您找到这些类型问题的明确答案的地方。提示:搜索def get_or_create。 可以看到,这个函数只在try/except中捕获DoesNotExist

def get_or_create(self, **kwargs):
    """
    Looks up an object with the given kwargs, creating one if necessary.
    Returns a tuple of (object, created), where created is a boolean
    specifying whether an object was created.
    """
    assert kwargs, \
            'get_or_create() must be passed at least one keyword argument'
    defaults = kwargs.pop('defaults', )
    lookup = kwargs.copy()
    for f in self.model._meta.fields:
        if f.attname in lookup:
            lookup[f.name] = lookup.pop(f.attname)
    try:
        self._for_write = True
        return self.get(**lookup), False
    except self.model.DoesNotExist:

【讨论】:

对,我的问题实际上更像是一种深思熟虑的类型。我看到get_or_create 只捕获DoesNotExist。但是如果它确实存在,它不应该是self.get(**lookup) 吗?如果是这样,我的脚本怎么会创建第二个对象——因此我怎么能得到MultipleObjectsReturned 异常? @bepetersn 你可能会得到一个副本,具体取决于你如何填充查询 kwargs(这实际上发生在我身上):get_or_create(name=foo), get_or_create(name=foo, bar=bar), get_or_create(name=foo) # multiple 啊,我明白了!所以我用create() 创建了两个不同的对象(b/c 的参数数量不同),然后当我尝试get() 只使用足够具体的参数来缩小对这两个对象的搜索范围时,我得到了一个错误。因此,从本质上讲,get(**params) 实际上只是过滤数据库。有点像select-where SQL 语句。 再次感谢您一直支持我并帮助我解决这个问题,@Yuji @Bepetersn,很好的发现 - 完全正确。使用更多参数获取或创建将导致单个参数重复。 NP人!【参考方案3】:

另一种可能导致 get_or_create() API 出现 MultipleObjectsReturned 错误的情况是,如果有多个线程同时使用同一组查询参数调用此 API。

仅依靠 try...catch... 在 Python 中创建唯一行是行不通的。如果您尝试使用此 API,我认为您应该对数据库中的相应列具有匹配的唯一性约束。

见:https://code.djangoproject.com/ticket/12579

【讨论】:

【参考方案4】:

警告

假设数据库强制执行关键字参数的唯一性,此方法是原子的(请参阅 unique 或 unique_together)。如果关键字参数中使用的字段没有唯一性约束,则对该方法的并发调用可能会导致插入具有相同参数的多行。

https://docs.djangoproject.com/en/3.2/ref/models/querysets/#django.db.models.query.QuerySet.get_or_create

【讨论】:

以上是关于使用 get_or_create 返回的 MultipleObjects的主要内容,如果未能解决你的问题,请参考以下文章

正确使用 get_or_create?

与 iexact 一起使用时,Django get_or_create 无法设置字段

使用 User.objects.get_or_create() 在 django 中给出无效的密码格式?

django - 使用 get_or_create 自动创建用户时设置用户权限

Django - SQL 批量 get_or_create 可能吗?

get_or_create() 问题