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

时间:2012-04-25 19:34:03

标签: python mysql sqlalchemy celery

我的应用程序使用作用域会话和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,并且不希望用任何新方法阻碍这种能力。)

干杯!

6 个答案:

答案 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)