SQLAlchemy:尝试保存非唯一值后重新保存模型的唯一字段

时间:2011-09-25 13:01:53

标签: python database sqlalchemy

在我的SQLAlchemy应用程序中,我有以下模型:

from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

class MyModel(declarative_base()):
    # ...
    label = Column(String(20), unique=True)

    def save(self, force=False):
        DBSession.add(self)
        if force:
            DBSession.flush()

稍后我会想要随机生成MyModel的每个新label个对象的代码,如果生成的值已经存在于DB中,则只重新生成它。
我正在尝试执行以下操作:

# my_model is an object of MyModel
while True:
    my_model.label = generate_label()
    try:
        my_model.save(force=True)
    except IntegrityError:
        # label is not unique - will do one more iteration
        # (*)
        pass
    else:
        # my_model saved successfully - exit the loop
        break

但是如果第一次生成的label不唯一而且第二次(或稍后)迭代调用了save(),则会收到此错误:

 InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (IntegrityError) column url_label is not unique... 

当我在位置(*)中添加DBSession.rollback()时,我得到了这个:

 ResourceClosedError: The transaction is closed

我该怎么做才能正确处理这种情况? 感谢

3 个答案:

答案 0 :(得分:5)

如果您的session对象基本上回滚,则必须先创建一个新会话并刷新模型,然后才能重新开始。如果您使用zope.sqlalchemy,则应使用transaction.commit()transaction.abort()来控制事物。所以你的循环看起来像这样:

# you'll also need this import after your zope.sqlalchemy import statement
import transaction

while True:
    my_model.label = generate_label()
    try:
        transaction.commit()
    except IntegrityError:
        # need to use zope.sqlalchemy to clean things up
        transaction.abort()
        # recreate the session and re-add your object
        session = DBSession()
        session.add(my_model)
    else:
        break

我已经从对象的save方法中删除了会话对象的使用。我不完全确定ScopedSession如何在课堂上使用时如何刷新自己。就个人而言,我认为在模型中嵌入SqlAlchemy内容并不能很好地与SqlAlchemy的unit of work方法很好地协调。

如果您的标签对象确实是生成且唯一的值,那么我同意TokenMacGuy并只使用uuid值。

希望有所帮助。

答案 1 :(得分:2)

数据库没有一致的方式告诉您为什么事务失败,以自动化可访问的形式。您通常无法尝试该事务,然后重试,因为因某些特定原因而失败

如果您知道要解决的条件(如唯一约束),那么您需要自己检查约束。在sqlalchemy中,这看起来像这样:

# Find a unique label
label = generate_label()
while DBsession.query(
        sqlalchemy.exists(sqlalchemy.orm.Query(Model)
                  .filter(Model.lable == label)
                  .statement)).scalar():
    label = generate_label()

# add that label to the model
my_model.label = label
DBSession.add(my_model)
DBSession.flush()

编辑:另一种回答这个问题的方法是你不应该自动重试交易;您可以改为返回HTTP状态代码307 Temporary Redirect(在重定向的URL中有一些盐),以便事务真正重新启动。

答案 2 :(得分:2)

我在使用Pyramid框架编写的webapp中遇到了类似的问题。我发现这个问题有点不同。

while True:
    try:
        my_model.label = generate_label()
        DBSession.flush()
        break
    except IntegrityError:
        # Rollback will recreate session:
        DBSession.rollback()
        # if my_model was in db it must be merged:
        my_model = DBSession.merge(my_model)

如果之前存储了my_model,则合并部分至关重要。没有合并会话将是空的,因此flush不会采取任何行动。