我的应用程序使用作用域会话和SQLALchemy的声明式样式。它是一个Web应用程序,许多数据库插入由任务调度程序Celery
执行。
通常,在决定插入对象时,我的代码可能会执行以下操作:
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()
这里的问题是因为很多这是由异步工作者完成的,但是有一个工作可能会在Bike
插入id=123
而中途,而另一个正在检查它是否存在。在这种情况下,第二个worker将尝试插入具有相同主键的行,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)
,我不介意再插入这行。
对我而言,由于字符串检查,这似乎非常脆弱。就像一个FYI,当IntegrityError
被提升时,它看起来像这样:
(IntegrityError) (1062, "Duplicate entry '123' for key 'PRIMARY'")
当然,我插入的主键是Duplicate entry is a cool thing
,然后我想我可能会错过IntegrityError
,这实际上并不是因为重复的主键。
有没有更好的方法,它们维护我正在使用的干净的SQLAlchemy方法(而不是开始在字符串中写出语句等。)
Db是MySQL(虽然对于单元测试我喜欢使用SQLite,并且不希望用任何新方法阻碍这种能力。)
干杯!
答案 0 :(得分:26)
如果您使用session.merge(bike)
而不是session.add(bike)
,那么您将不会生成主键错误。将根据需要检索并更新或创建bike
。
答案 1 :(得分:8)
您应该以相同的方式处理每个IntegrityError
:回滚事务,并可选择重试。有些数据库甚至不会让你在IntegrityError
之后做更多的事情。如果数据库允许,您还可以在两个冲突的事务开始时获取表上的锁,或者更精细的锁。
使用with
语句显式开始事务,并自动提交(或回滚任何异常):
from schema import Session
from schema.models import Bike
session = Session()
with session.begin():
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)
答案 2 :(得分:4)
我假设你的主键在某种程度上是自然的,这就是为什么你不能依赖正常的自动增量技术。因此,假设问题实际上是您需要插入的一个独特列中的一个,这更常见。
如果你想要“尝试插入,部分失败时回滚”,你可以使用SAVEPOINT,其中SQLAlchemy是begin_nested()。下一个rollback()或commit()只对SAVEPOINT起作用,而不是更大的事情。
然而,总体而言,这里的模式只是应该避免的模式。你真正想做的是三件事之一。 1.不要运行处理需要插入的相同密钥的并发作业。 2.以某种方式在正在使用的并发密钥上同步作业,并使用一些常用服务生成此特定类型的新记录,由作业共享(或确保它们在作业运行之前全部设置)。
如果你考虑一下,#2在任何情况下都会发生高度隔离。开始两个postgres会议。第一节:
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是否足够智能,但这是正确的行为。如果OTOH你试图插入不同的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争用,你的芹菜任务将自己序列化无论如何,或者至少,它们应该是。
答案 3 :(得分:2)
而不是session.add(obj)
你需要使用下面提到的代码,这将更加清晰,你不需要像你提到的那样使用自定义提交功能。但是,这将忽略冲突,不仅是重复密钥,也是其他密钥。
<强> MySQL的:强>
self.session.execute(insert(self.table, values=values, prefixes=['IGNORE']))
<强>源码强>
self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE']))
答案 4 :(得分:0)
有了下面的代码,你应该可以为所欲为,不仅仅是为了解决这个问题。
class SessionWrapper(Session):
def commit(self, ignore=True):
try:
super(SessionWrapper, self).commit()
except IntegrityError as e:
if not ignore:
raise e
message = e.args[0]
if "Duplicate entry" in message:
logging.info("Error while executing %s.\n%s.", e.statement, message)
finally:
super(SessionWrapper, self).close()
def session(self, auto_commit=False):
session_factory = sessionmaker(class_=SessionWrapper, bind=self.engine, autocommit=auto_commit)
return scoped_session(session_factory)
Session = session()
s1 = Session()
p = Test(test="xxx", id=1)
s1.add(p)
s1.commit()
s1.close()
答案 5 :(得分:0)
只需回滚并一一重试,就这么简单:
try:
self._session.bulk_insert_mappings(mapper, items)
self._session.commit()
except IntegrityError:
self._session.rollback()
logger.info("bulk inserting rows failed, fallback to insert one-by-one")
for item in items:
try:
self._session.execute(insert(mapper).values(**item))
self._session.commit()
except SQLAlchemyError as e:
logger.error("Error inserting item: %s for %s", item, e)