Python 在SQLAlchemy中处理插入时重复的主键(声明式样式)

Python 在SQLAlchemy中处理插入时重复的主键(声明式样式),python,mysql,sqlalchemy,celery,Python,Mysql,Sqlalchemy,Celery,我的应用程序使用范围会话和SQLALchemy的声明式风格。这是一个web应用程序,许多DB插入都是由任务调度器芹菜执行的 通常,在决定插入对象时,我的代码可能会执行以下操作: from schema import Session from schema.models import Bike pk = 123 # primary key bike = Session.query(Bike).filter_by(bike_id=pk).first() if not bike: # no bike

我的应用程序使用范围会话和SQLALchemy的声明式风格。这是一个web应用程序,许多DB插入都是由任务调度器芹菜执行的

通常,在决定插入对象时,我的代码可能会执行以下操作:

from schema import Session
from schema.models import Bike

pk = 123 # primary key
bike = Session.query(Bike).filter_by(bike_id=pk).first()
if not bike: # no bike in DB
    new_bike = Bike(pk, "shiny", "bike")
    Session.add(new_bike)
    Session.commit()
这里的问题是,因为很多工作都是由异步工作者完成的,所以有可能一个人在插入
id=123的
Bike
时半途而废,而另一个人则在检查它的存在。在这种情况下,第二个工作者将尝试插入具有相同主键的行,SQLAlchemy将引发一个
IntegrityError

我一辈子都找不到一个好办法来处理这个问题,除了把Session.commit()换成:

'''schema/__init__.py'''
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())

def commit(ignore=False):
    try:
        Session.commit()
    except IntegrityError as e:
        reason = e.message
        logger.warning(reason)

        if not ignore:
            raise e

        if "Duplicate entry" in reason:
            logger.info("%s already in table." % e.params[0])
            Session.rollback()
然后,无论我在哪里都有
Session.commit
我现在有
schema.commit(ignore=True)
在这里我不介意再次插入行

对我来说,这似乎非常脆弱,因为字符串检查。仅供参考,当引发
IntegrityError
时,它如下所示:

(IntegrityError) (1062, "Duplicate entry '123' for key 'PRIMARY'")
当然,我插入的主键是类似于重复输入是一件很酷的事情,然后我想我可能会错过IntegrityError,它实际上不是因为重复的主键

有没有更好的方法来维护我正在使用的干净的SQLAlchemy方法(而不是开始用字符串等写出语句)

Db是MySQL(尽管对于单元测试,我喜欢使用SQLite,并且不想用任何新方法阻碍这种能力)


干杯

您应该以相同的方式处理每个
IntegrityError
:回滚事务,然后可以选择重试。一些数据库甚至不允许您在执行
IntegrityError
之后再执行其他操作。您还可以在两个冲突事务开始时获取表上的锁,或者在数据库允许的情况下获取更细粒度的锁

使用
with
语句显式开始事务,并自动提交(或在任何异常时回滚):


我假设你的主键在某种程度上是自然的,这就是为什么你不能依赖于正常的自动增量技术。因此,假设问题是您需要插入的某个独特列中的一个,这是比较常见的

如果您想要“尝试插入,失败时部分回滚”,您可以使用保存点,它与SQLAlchemy一起使用的是begin_nested()。下一个rollback()或commit()只作用于该保存点,而不是更大范围的操作

然而,总的来说,这里的模式只是一个真正应该避免的模式。你真正想在这里做的是三件事之一。1.不要运行处理需要插入的相同密钥的并发作业。2.以某种方式在与和3一起使用的并发密钥上同步作业。使用一些公共服务生成此特定类型的新记录,这些记录由作业共享(或者确保在作业运行之前都已设置好)

如果你想一想#2在任何情况下都是在高度隔离的情况下发生的。开始两个研究生课程。第1场会议:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo (id) values (1);
第2次会议:

test=> begin;
BEGIN
test=> insert into foo(id) values(1);
您将看到,会话2阻塞,因为带有PK#1的行被锁定。我不确定MySQL是否足够聪明,可以做到这一点,但这是正确的行为。如果您尝试插入不同的PK:

^CCancel request sent
ERROR:  canceling statement due to user request
test=> rollback;
ROLLBACK
test=> begin;
BEGIN
test=> insert into foo(id) values(2);
INSERT 0 1
test=> \q
它进行得很好,没有阻塞


关键是,如果您正在进行这种PK/UQ争用,那么您的芹菜任务无论如何都会序列化它们自己,或者至少它们应该序列化

如果使用
session.merge(bike)
而不是
session.add(bike)
,则不会生成主键错误。将根据需要检索、更新或创建
自行车。

而不是
会话。添加(obj)
您需要使用下面提到的代码,这将更干净,并且您不需要像前面提到的那样使用自定义提交功能。然而,这将忽略冲突,不仅对于重复密钥,对于其他密钥也是如此

mysql:

 self.session.execute(insert(self.table, values=values, prefixes=['IGNORE']))
sqlite

self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE']))

为什么不考虑使用自动增量来生成主键呢?那你就不用担心这个问题了。或者有没有不这样做的具体原因?有一个具体的原因(对不起,这个例子有点琐碎)。嗨。我不是故意同时安排插入和检查。问题在于,该对象恰好是由两个单独的进程以特殊方式创建的。这没有什么不好的,只是应用程序的方式(实际上,对象不是自行车,而是时间)。但是,您关于运行单个工作人员的看法是正确的。我正在研究如何指定一个工人来管理所有与数据库相关的任务,这将提供我所需要的同步性。从应用程序执行插入操作不是一个选项。DB在一台远程机器上,我需要低于100ms的web应用响应。这种SQL问题几乎总是归咎于设计。例如,您确定不能使数据库的主键自动递增并处理偶尔出现的“主键列的两行”结果吗?[对不起,我应该补充,PK不是自动递增的原因很好]我不确定我是否同意。数据库由许多其他应用程序共享,包括使用有问题的表。为什么在你做了一些尽职调查后,DB可能会在我的另一个进程/应用程序/人中插入一行,这是一个糟糕的设计?关键是你必须在你的应用程序中处理这个问题。我的问题很简单,在SQLAlchemy中处理这个问题的唯一方法是通过字符串检查,它似乎不是特别健壮。哦,您只想处理完整性错误。你可能没有赵
self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE']))