请教大家一个 Django 的 `get_or_create`

254 views
Skip to first unread message

bwangel

unread,
Dec 14, 2017, 5:46:36 AM12/14/17
to python-cn(华蟒用户组,CPyUG 邮件列表)
大家好,最近在看 Django的 get_or_create 的文档的时候,发现了这样一句话:

If you are using MySQL, be sure to use the READ COMMITTED isolation level rather than REPEATABLE READ (the default), otherwise you may see cases where get_or_create will raise an IntegrityError but the object won’t appear in a subsequent get() call.





我对于上面这句话的理解就是,如果使用 MySQL的话,需要将数据库的隔离级别从默认的可重复读改成提交读。否则的话,你可能会看到 get_or_create 方法抛出了一个 IntegrityError 异常,但是随后在数据库中查询的时候却找不到你要插入的记录。

我知道 get_or_create 是依赖于唯一约束的,通过唯一约束抛出的 IntegrityError 来判断数据库中是不是已经存在相同的记录,但我对于文档上这句话很困惑,为什么 MySQL 的可重复读隔离级别会导致 get_or_create 抛出 IntegrityError 异常,而且数据也写入不到数据库里面。我查了一下 get_or_create 的源码:它创建数据库记录的代码是这样的:

    def _create_object_from_params(self, lookup, params):
        """
        Tries to create an object using passed params.
        Used by get_or_create and update_or_create
        """
        try:
            with transaction.atomic(using=self.db):
                params = {k: v() if callable(v) else v for k, v in params.items()}
                obj = self.create(**params)
            return obj, True
        except IntegrityError:
            exc_info = sys.exc_info()
            try:
                return self.get(**lookup), False
            except self.model.DoesNotExist:
                pass
            six.reraise(*exc_info)

_create_object_from_params 做的事情就是开启一个事务,然后通过 insert 语句插入数据。就算是在并发高的时候开启了两个事务,同时插入了相同的数据。那么至少也应该有一个事务执行成功啊,另外一个事务抛出 IntegrityError,为什么会一条数据也插入不进去。

这个问题想了好久,感觉脑袋想破了也没想明白,MySQL的事务无论在什么隔离级别下,应该保证至少有一条数据能够插入成功啊,Django 文档中描述的这种一条记录也没有的情况,会在什么情况出现啊?

哪位大佬能不能解答一下小弟的困惑啊,这个问题我想了好久也没想明白,感觉太难受了。。。。

piglei

unread,
Dec 14, 2017, 6:46:55 AM12/14/17
to pyth...@googlegroups.com
but the object won’t appear in a subsequent get() call.

这句话的意思是说:新插入的对象在之后的 get() 调用中不会出现。这句话的语境仅仅是指抛出了 IntergrityError 的那条事务,不是说数据真的没有插入进去。以上是我的推测,楼主可以具体试试看。

--
邮件来自: `CPyUG`华蟒用户组(中文Python技术邮件列表)
规则: http://code.google.com/p/cpyug/wiki/PythonCn
详情: http://code.google.com/p/cpyug/wiki/CpyUg
严正: 理解列表! 智慧提问! http://wiki.woodpecker.org.cn/moin/AskForHelp
---
您收到此邮件是因为您订阅了Google网上论坛上的“python-cn(华蟒用户组,CPyUG 邮件列表)”群组。
要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到python-cn+unsubscribe@googlegroups.com
要发帖到此群组,请发送电子邮件至python-cn@googlegroups.com
要查看更多选项,请访问https://groups.google.com/d/optout

Leo Jay

unread,
Dec 14, 2017, 4:41:58 PM12/14/17
to python-cn:CPyUG
简单结论:
repeatable read的问题是,如果你在第一次查找这笔数据的时候这笔数据不存在,
哪怕后来其它进程把这笔数据插进去了,在你的事务里也是看不到这笔数据的。
*但是*,看不到归看不到,你也不能把这个数据插进去。
所以,这笔数据在你的事务里就会处在一个很奇怪的状态:想取取不到,想创建创建不了。


详细分析:
手上没有mysql,我拿postgresql为例:
比方说我有这样一个空表:
create table t(id int primary key, data text);

时刻1:
我的进程1:
leo=> begin;
BEGIN
leo=> set transaction isolation level repeatable read;
SET
leo=> select * from t where id=1;
id | data
----+------
(0 rows)

leo=>

在这里,我开一个事务,把transaction isolation level设成repeatable read,然后查id为1的数据。
结果没有找到。

时刻2:
我的进程2:
leo=> insert into t values(1, 'data 1');
INSERT 0 1
leo=>

我在另一个进程里插入了这条id为1的数据。

时刻3:
我的进程1:
leo=> select * from t where id=1;
id | data
----+------
(0 rows)

leo=>
你会发现,由于repeatable read的要求,这笔由进程2新插入的数据在你的事务中仍然是找不到的。

那你如果在进程1里试着插入这笔数据会发生什么呢?
leo=> savepoint sp1;
SAVEPOINT
leo=> insert into t values(1, 'data 1 by process 1');
ERROR: duplicate key value violates unique constraint "t_pkey"
DETAIL: Key (id)=(1) already exists.
leo=> rollback to sp1;
ROLLBACK
leo=>

你会发现你插不进去。说是主键已经存在了。
那你再查这笔数据呢:
leo=> select * from t where id=1;
id | data
----+------
(0 rows)

leo=>
还是什么都没有。

所以 django 的 get_or_create 这个方法想取取不到,想创建创建不了。结果就只能 raise an IntegrityError。
只要你的当前事务没有结束,你是读不到这笔数据的,所以the object won’t appear in a subsequent get() call.

bwangel

unread,
Dec 15, 2017, 2:06:31 AM12/15/17
to python-cn(华蟒用户组,CPyUG 邮件列表)
@猪之哀伤,@LeoJay。谢谢你们的回复。

感觉@LeoJay这么一解释,我就懂了。文档中这句话针对的应该是这种情况:

from django.db import transaction
with transaction.atomic():
     User.objects.get_or_create(nickname='xxx')

如果将 get_or_create语句包裹在一个事务中,且MySQL的隔离级别设置成可重复读的话,上述语句确实会抛出IntergrityError的异常。

我看了一下get_or_create的实现,它的执行过程应该是这样的:

由于get_or_create实际上执行了三个事务,如果我们没有手动地把它放在一个事务中的话,它应该是不会抛出IntegrityError异常的。如果我们手动地把它放在一个事务中的话,而数据库的隔离级别又是可重复读的话,那么就很容易出现 @LeoJay 所说的情况,数据取又取不出来,存又存不进去。


以上就是我们对于get_or_create的理解,由于我水平有限,不对的地方还请大家指正。



在 2017年12月14日星期四 UTC+8下午6:46:36,bwangel写道:

Leo Jay

unread,
Dec 15, 2017, 3:09:09 AM12/15/17
to python-cn:CPyUG
你的理解是对的。但是,我想提两点:
1. 在 GET 里,你不应该使用 get_or_create,GET应该是无副作用的。
在 POST 里,你一般总是会希望要修改的数据要么提交全进数据库,要么回滚,什么也不改。
所以一般 POST 的代码总应该包在一个事务里。从而,不应该出现你所说的
如果我们没有手动地把它放在一个事务中的话,它应该是不会抛出IntegrityError异常的
情况。

2. 我知道你是随手这么一写,get_or_create(nickname='xxx'),但我还是想说一下,
除非你的 nickname 上有 unique constraint,否则,这样写有可能有竞态条件创建出两条数据nickname都是 'xxx'。
get_or_create的过滤参数列表里的参数,一定要有 unique constraint,否则在你的事务2里是不会触发回滚的。
文档有说:
This method is atomic assuming correct usage, correct database configuration, and correct behavior of the underlying database.
However, if uniqueness is not enforced at the database level for the kwargs used in a get_or_create call (see unique or unique_together),
 this method is prone to a race-condition which can result in multiple rows with the same parameters being inserted simultaneously.

bwangel

unread,
Dec 15, 2017, 4:20:12 AM12/15/17
to python-cn(华蟒用户组,CPyUG 邮件列表)
谢谢提醒。

关于第二点,这个确实。get_or_create 是依赖于唯一约束的,如果没有唯一约束,可能会插入多条相同的记录。

对于第一点,有时候我们的需求比较复杂,确实做不到把每个POST请求全都包裹在一个事务中。

比如通过手机号和验证码注册/登录,服务端判断这个手机号注册过没有,注册过直接登录,没注册过的话,返回不同的相应,提示客户端跳转到完善信息页面。

这里 POST请求其实做的是两件事,不能放在同一个事务中。


在 2017年12月15日星期五 UTC+8下午4:09:09,Leo Jay写道:
Reply all
Reply to author
Forward
0 new messages