【问题标题】:SQLAlchemy: Re-saving model's unique field after trying to save non-unique valueSQLAlchemy:尝试保存非唯一值后重新保存模型的唯一字段
【发布时间】:2011-11-24 15:06:37
【问题描述】:

在我的 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

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

【问题讨论】:

  • 您应该将declarative_base() 的返回值分配给一个变量。否则在创建多个模型时会遇到问题,因为它们可能有不同的基类。

标签: python database sqlalchemy


【解决方案1】:

如果您的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 值。

希望对您有所帮助。

【讨论】:

  • ScopedSession 使用线程本地存储模型;会话显式无效(通过ScopedSession.reset()),但这通常由为您提供会话的框架在您将请求的控制权返回给框架时处理。当框架提供帮助时它很方便,但如果你不能使用这种线程模型,那就真的很头疼了。除非你正在设计一个多线程框架,否则 scopedesssion 不是你想要的。
  • @TokenMacGuy - 我认为你得到的是 ScopedSession 本质上是线程上的一个全局对象,所以在请求周期结束时明确地清理它(顺便说一句它是@987654333 @ in 0.6/7) 成为一个额外的问题。
  • 对;如果每个工作单元有一个线程,那么 ScopedSession 可能会简化无法以任何其他方式轻松耦合的组件的事情;但在许多情况下,要么通过全局 TLS 容器以外的方式注入会话,要么每个 uow 的线程是不可能的,而且 ScopedSession 根本不会帮助你。对于初学者来说,这似乎是一个常见的困惑点;作用域会话使 UOW 有点神奇,当框架不以符合开发人员期望的方式管理会话时,应用程序变得难以调试
【解决方案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()

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

【讨论】:

  • 是的,我想过自己检查约束,但问题是不能保证我刚刚生成并将存储在 DB 中的相同值不会出现在该 DB 之间生成和存储的时刻。我没有问,如何知道交易失败的原因,我问的是如何以正确的方式“修复”会话。
  • 您应该考虑使用原子序列或全局唯一键;大多数数据库都支持某种序列(例如,MySQL 有 AUTOINCREMENT)。如果这对您来说不是一个明智的选择,您可以使用由uuid 模块生成的 ID,以获得唯一 ID 的高概率。
  • 谢谢,去uuid模块看看
【解决方案3】:

我在用 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,则合并部分至关重要。没有合并会话将是空的,因此刷新不会采取任何行动。

【讨论】:

  • 请注意:如果您有一个使用 pyramid_tm 的 Pyramid 应用程序,最好使用 transaction.abort() 而不是 DBSession.rollback()
猜你喜欢
  • 2014-11-13
  • 2023-03-23
  • 2014-05-23
  • 2020-06-22
  • 1970-01-01
  • 2021-12-21
  • 2015-03-06
  • 2020-11-15
  • 1970-01-01
相关资源
最近更新 更多