【问题标题】:Django QuerySet update_or_create creating duplicate entriesDjango QuerySet update_or_create 创建重复条目
【发布时间】:2018-08-15 13:46:06
【问题描述】:

最近我在update_or_create 方法中遇到问题。让我先做一个完整的解释。

型号:

class TransactionPageVisits(models.Model):
    transactionid = models.ForeignKey(
        Transaction,
        on_delete=models.CASCADE,
        db_column='transactionid',
    )
    sessionid = models.CharField(max_length=40, db_index=True)
    ip_address = models.CharField(max_length=39, editable=False)
    user_agent = models.TextField(null=True, editable=False)
    page = models.CharField(max_length=100, null=True, db_index=True)
    method = models.CharField(max_length=20, null=True)
    url = models.TextField(null=False, editable=False)
    created_dtm = models.DateTimeField(auto_now_add=True)

    class Meta(object):
        ordering = ('created_dtm',)

功能:

def _tracking(self, request, response, **kwargs):
    txn_details = kwargs.get('txn_details')
    data = {
        'sessionid': request.session.session_key,
        'ip_address': get_ip_address(request),
        'user_agent': get_user_agent(request),
        'method': request.method,
        'url': request.build_absolute_uri(),
        'transactionid': txn_details.txn_object,
        'page': kwargs.get('page')
    }

    # Keep updating/creating tracking data to model
    obj, created = TransactionPageVisits.objects.update_or_create(**data)

注意事项:

我知道我没有将任何 defaults 参数传递给 update_or_create(),因为在编写代码时它不是必需的(仅当所有列都为每个数据都是唯一的)。 _tracking() 也在中间件中,在每个请求和响应中都会被调用。

一切都很顺利,直到今天我遇到了以下异常:

File "trackit.py", line 65, in _tracking
    obj, created = TransactionPageVisits.objects.update_or_create(**data)
  File "/usr/local/lib/python2.7/dist-packages/Django-1.10.4-py2.7.egg/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/Django-1.10.4-py2.7.egg/django/db/models/query.py", line 488, in update_or_create
    obj = self.get(**lookup)
  File "/usr/local/lib/python2.7/dist-packages/Django-1.10.4-py2.7.egg/django/db/models/query.py", line 389, in get
    (self.model._meta.object_name, num)
MultipleObjectsReturned: get() returned more than one TransactionPageVisits -- it returned 2!

我注意到在表中创建了两个具有完全相同值的条目(created_dtm 除外,因为它具有 auto_add_now=True):

| id    | sessionid                        | ip_address     | user_agent                                                                     | page | method | url                                                                                                    | created_dtm                | transactionid |
| 32858 | nrq2vwxbtsjp8yoibotpsur0zit5jhoq | xx.xxx.xxx.xxx | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0 |      | GET    | https://www.example.com/example_url/?jobid=5a9f2acb4cedfd00011c7d5d&transactionid=XXXXXXXXXXXX | 2018-03-06 23:57:00.061280 | XXXXXXXXXXXX  |
| 32859 | nrq2vwxbtsjp8yoibotpsur0zit5jhoq | xx.xxx.xxx.xxx | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0 |      | GET    | https://www.example.com/example_url/?jobid=5a9f2acb4cedfd00011c7d5d&transactionid=XXXXXXXXXXXX | 2018-03-06 23:57:00.062121 | XXXXXXXXXXXX  |

为什么首先在表格中创建了重复条目?

【问题讨论】:

    标签: django django-queryset


    【解决方案1】:

    update_or_create 容易出现竞争条件,如documentation 中所述:

    正如上面在 get_or_create() 中描述的那样,这种方法容易产生 可能导致插入多行的竞争条件 如果在数据库级别未强制执行唯一性,则同时进行。

    您可以在模型中使用unique_together,如另一个答案中所建议的那样。我从来没有测试过这个,但显然Django catches the IntegrityError caused by these race conditions

    【讨论】:

    • IMO 文档中的这句话是错误的,因为 Django 1.11 修复了底层的竞争条件。我要求在 IRC 上进行确认。然而,这仍然与 Django 1.10 相关,后者是 OP 使用的(已弃用)版本,并且在 db 引擎不支持事务的情况下。
    • 是的,我认为你是对的,有一个 commit 修复。
    • @AntoinePinsard 实际上,我认为竞争条件总是存在的。 select_for_update 锁定行,如果存在。但是如果两个不同的线程尝试get 并且对象不存在,它们都会创建它(整个表没有被锁定)。该修复程序只是防止对已存在的对象进行并发更新。
    • 确实,你又是对的。这降低了竞争条件的风险,但它仍然存在。这也是我在 IRC 上被告知的内容。
    • @PauloAlmeida:感谢您指出提交。它修复了 django 1.11 中的问题,使用transaction.atomic() 很好地实现了锁定。大声笑必须与我的经理确认我们是否可以升级 django 或者只是添加一个 try-except 到现在! :D
    【解决方案2】:

    我无法完全诊断此问题,因为如果没有默认设置,它仍然有可能出现意外行为(在我看来)。但是,我建议可能会查看 unique_together 以强制数据库内的唯一性,这可能会在未来强制字段的唯一性。

    【讨论】:

      【解决方案3】:

      源代码继续查找具有给定参数的唯一元素。

      def get(self, *args, **kwargs):
          """
          Perform the query and return a single object matching the given
          keyword arguments.
          """
          clone = self.filter(*args, **kwargs)
          if self.query.can_filter() and not self.query.distinct_fields:
              clone = clone.order_by()
          num = len(clone)
          if num == 1:
              return clone._result_cache[0]
          if not num:
              raise self.model.DoesNotExist(
                  "%s matching query does not exist." %
                  self.model._meta.object_name
              )
          raise self.model.MultipleObjectsReturned(
              "get() returned more than one %s -- it returned %s!" %
              (self.model._meta.object_name, num)
          )
      

      您给出的参数肯定类似于两个对象。

      data = {
          'sessionid': request.session.session_key,
          'ip_address': get_ip_address(request),
          'user_agent': get_user_agent(request),
          'method': request.method,
          'url': request.build_absolute_uri(),
          'transactionid': txn_details.txn_object,
          'page': kwargs.get('page')
      }
      

      你想同时更新它们吗?您想保持条目唯一吗?

      【讨论】:

      • 我看到了 get() 函数,并认为这是由于多个对象造成的。但我的问题是 为什么首先要创建多行? 这行如果所有字段的现有条目已经存在,代码将简单地忽略并且不会执行任何操作:obj, created = TransactionPageVisits.objects.update_or_create(**data)
      猜你喜欢
      • 2015-08-09
      • 2017-08-27
      • 2018-10-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-20
      • 2017-06-22
      • 2019-05-20
      相关资源
      最近更新 更多