版本:1.1.0b2 |发布日期:2016年7月1日

SQLAlchemy 1.1文档

常见问题

项目版本

会话/查询

我正在使用会话重新加载数据,但没有看到我在别处提交的更改

关于这种行为的主要问题是,即使事务处于可序列化的隔离状态,即使事务不是(通常不是),事务也会像事务一样进行。实际上,这意味着会话不会更改已在事务范围内读取的任何数据。

如果术语“隔离级别”不熟悉,那么您首先需要阅读以下链接:

隔离级别

In short, serializable isolation level generally means that once you SELECT a series of rows in a transaction, you will get the identical data back each time you re-emit that SELECT. 如果您处于隔离级别较低的“可重复读取”,则会看到新添加的行(不再看到已删除的行),但是对于已经已经加载的行,您不会看到任何变化。只有在较低的隔离级别时,例如“读取提交”,是否有可能看到一行数据改变其价值。

有关使用SQLAlchemy ORM时控制隔离级别的信息,请参阅Setting Transaction Isolation Levels

为了大大简化事情,Session本身按照完全独立的事务工作,除非您告诉它,否则不会覆盖已经读取的任何映射属性。尝试重新读取正在进行的事务中加载的数据的用例是一种罕见的用例,在很多情况下这种用例没有效果,所以这被认为是例外,而不是规范;为了在这个异常中工作,提供了几种方法来允许在正在进行的事务的上下文中重新加载特定的数据。

当我们谈论Session时,为了理解“事务”是什么意思,你的Session只能用在一个事务中。这个概述是在Managing Transactions

一旦我们发现了隔离级别,我们认为我们的隔离级别被设置得足够低,所以如果我们重新选择一行,我们应该在Session中看到新的数据>,我们怎么看?

从最普通到最不重要的三种方式:

  1. We simply end our transaction and start a new one on next access with our Session by calling Session.commit() (note that if the Session is in the lesser-used “autocommit” mode, there would be a call to Session.begin() as well). 绝大多数的应用程序和用例没有任何问题,因为他们不能在其他事务中“看”数据,因为他们坚持这种模式,这是短期交易的最佳实践的核心。 T0>。请参阅When do I construct a Session, when do I commit it, and when do I close it?对此有一些想法。
  2. We tell our Session to re-read rows that it has already read, either when we next query for them using Session.expire_all() or Session.expire(), or immediately on an object using Session.refresh. 有关详细信息,请参阅Refreshing / Expiring
  3. 我们可以运行整个查询,同时将它们设置为在使用Query.populate_existing()读取行时明确覆盖已经加载的对象。

But remember, the ORM cannot see changes in rows if our isolation level is repeatable read or higher, unless we start a new transaction.

“由于之前在刷新过程中出现异常,此会话的事务已经回滚”(或类似的)

This is an error that occurs when a Session.flush() raises an exception, rolls back the transaction, but further commands upon the Session are called without an explicit call to Session.rollback() or Session.close().

它通常对应于捕获Session.flush()Session.commit()中的异常并且不能正确处理异常的应用程序。例如:

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base(create_engine('sqlite://'))

class Foo(Base):
    __tablename__ = 'foo'
    id = Column(Integer, primary_key=True)

Base.metadata.create_all()

session = sessionmaker()()

# constraint violation
session.add_all([Foo(id=1), Foo(id=1)])

try:
    session.commit()
except:
    # ignore error
    pass

# continue using session without rolling back
session.commit()

Session的使用应该符合类似于以下的结构:

try:
    <use session>
    session.commit()
except:
   session.rollback()
   raise
finally:
   session.close()  # optional, depends on use case

许多事情可能会导致尝试/除了冲刷除外失败。您应该总是对会话操作进行某种“构建”,以便连接和事务资源具有确定的边界,否则您的应用程序不会真正控制其资源的使用。这并不是说你需要在整个应用程序中放置try / except块,相反,这将是一个糟糕的主意。您应该构建您的应用程序,以便在会话操作周围有一个(或几个)“框架”点。

有关如何组织使用Session的详细讨论,请参阅When do I construct a Session, when do I commit it, and when do I close it?

但为什么flush()坚持发出ROLLBACK?

如果Session.flush()可以部分完成,然后不回滚将会很好,但是这超出了它当前的能力,因为它的内部簿记必须被修改,以便可以在任何时候时间和完全一致的是什么被冲刷到数据库。虽然这在理论上是可行的,但是由于许多数据库操作在任何情况下都需要ROLLBACK,所以增强的有效性大大降低了。特别是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 values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

SQLAlchemy提供的解决这两个问题的方法是通过Session.begin_nested()支持SAVEPOINT。使用Session.begin_nested(),您可以构建一个可能在事务中失败的操作,然后在保持封闭事务的同时“回退”到失败前的位置。

但是为什么不是自动调用ROLLBACK呢?为什么我必须再次ROLLBACK?

这也是Session提供了一个一致的接口,并拒绝猜测正在使用的上下文。例如,Session在多个级别内支持上面的“成帧”。比如,假设你有一个装饰器@with_session(),它是这样做的:

def with_session(fn):
   def go(*args, **kw):
       session.begin(subtransactions=True)
       try:
           ret = fn(*args, **kw)
           session.commit()
           return ret
       except:
           session.rollback()
           raise
   return go

上面的装饰器开始一个事务,如果一个事务不存在,然后提交它,如果它是创建者。The “subtransactions” flag means that if Session.begin() were already called by an enclosing function, nothing happens except a counter is incremented - this counter is decremented when Session.commit() is called and only when it goes back to zero does the actual COMMIT happen. 它允许这种使用模式:

@with_session
def one():
   # do stuff
   two()


@with_session
def two():
   # etc.

one()

two()

one()可以调用two(),或two()可以自行调用,@with_session正如你所看到的,如果two()调用抛出异常然后发出rollback()flush(),那么 always是装饰器执行的第二个rollback(),可能还有第三个对应于两个装饰器级别。如果flush()rollback()推到栈顶,然后我们说所有剩下的rollback()一个写得不好的封闭方法可能会抑制这个异常,然后在没有错误的情况下调用commit(),然后你有一个无声的失败条件。人们得到这个错误的主要原因实际上是因为他们没有编写干净的“框架”代码,他们将会遇到其他问题。

If you think the above use case is a little exotic, the same kind of thing comes into play if you want to SAVEPOINT- you might call begin_nested() several times, and the commit()/rollback() calls each resolve the most recent begin_nested(). The meaning of rollback() or commit() is dependent upon which enclosing block it is called, and you might have any sequence of rollback()/commit() in any order, and its the level of nesting that determines their behavior.

在上述两种情况下,如果flush()破坏了事务块的嵌套,则根据场景的不同,从“魔术”到静默失败到公然中断代码流的任何地方。

flush() makes its own “subtransaction”, so that a transaction is started up regardless of the external transactional state, and when complete it calls commit(), or rollback() upon failure - but that rollback() corresponds to its own subtransaction - it doesn’t want to guess how you’d like to handle the external “framing” of the transaction, which could be nested many levels with any combination of subtransactions and real SAVEPOINTs. 开始/结束“帧”的工作与flush()外部的代码保持一致,我们决定这是最一致的方法。

如何做一个查询,总是为每个查询添加一个特定的过滤器?

请参阅PreFilteredQuery中的配方。

我已经创建了一个映射外部联接,而查询返回行时,没有对象返回。为什么不呢?

由外部联接返回的行可能包含NULL作为主键的一部分,因为主键是两个表的组合。Query对象会忽略没有可接受主键的传入行。基于mapper()上的allow_partial_pks标志的设置,如果该值至少有一个非NULL值,或者如果该值具有没有NULL值。请参阅mapper()处的allow_partial_pks

我使用joinedload()lazy=False来创建一个JOIN / OUTER JOIN,而SQLAlchemy在我尝试添加一个WHERE时没有构造正确的查询,ORDER BY ,LIMIT等(依赖于(OUTER)JOIN)

由联合加载生成的连接仅用于完全加载相关集合,并且设计为不影响查询的主要结果。由于它们是匿名的,因此不能直接引用。

有关此行为的详细信息,请参阅The Zen of Eager Loading

查询没有__len__(),为什么不呢?

应用于对象的Python __len__()魔术方法允许使用len()内建值来确定集合的长度。It’s intuitive that a SQL query object would link __len__() to the Query.count() method, which emits a SELECT COUNT. 这是不可能的原因是因为评估查询作为列表将导致两个SQL调用,而不是一个:

class Iterates(object):
    def __len__(self):
        print("LEN!")
        return 5

    def __iter__(self):
        print("ITER!")
        return iter([1, 2, 3, 4, 5])

list(Iterates())

输出:

ITER!
LEN!

如何在ORM查询中使用文本SQL?

看到:

我正在调用Session.delete(myobject),并且不会从父集合中移除!

有关此行为的描述,请参见Deleting from Collections

当我加载对象时,为什么不调用__init__()

有关此行为的描述,请参见Constructors and Object Initialization

如何在SA的ORM中使用ON DELETE CASCADE?

SQLAlchemy将总是为当前加载在Session中的相关行发布UPDATE或DELETE语句。对于未加载的行,默认情况下会发出SELECT语句来加载这些行,并将其删除/删除。换句话说,它假定没有配置ON DELETE CASCADE。要将SQLAlchemy配置为与ON DELETE CASCADE配合使用,请参阅Using Passive Deletes

我把我的实例的“foo_id”属性设置为“7”,但是“foo”属性仍然是None - 它不应该使用id#7加载Foo吗? T2>

ORM的构造方式不是支持从外键属性更改驱动的直接人口关系 - 而是设计成反其道而行 - 外键属性由ORM在幕后处理,最终用户自然建立对象关系。因此,建议设置o.foo的方法就是这样做 - 设置它!:

foo = Session.query(Foo).get(7)
o.foo = foo
Session.commit()

外键属性的操作当然是完全合法的。但是,将外键属性设置为新值并不会触发它所涉及的relationship()的“过期”事件。这意味着对于以下顺序:

o = Session.query(SomeClass).first()
assert o.foo is None  # accessing an un-set attribute sets it to None
o.foo_id = 7

o.foo is initialized to None when we first accessed it. 设置o.foo_id = 7的值为“7” - 所以o.foo还是None

# attribute is already set to None, has not been
# reconciled with o.foo_id = 7 yet
assert o.foo is None

对于o.foo来加载基于外键的变异通常是在commit后自然实现的,这两个都会刷新新的外键值,并且过期所有的状态:

Session.commit()  # expires all attributes

foo_7 = Session.query(Foo).get(7)

assert o.foo is foo_7  # o.foo lazyloads on access

更简单的操作是单独使用属性 - 这可以使用Session.expire()对任何persistent对象执行:

o = Session.query(SomeClass).first()
o.foo_id = 7
Session.expire(o, ['foo'])  # object must be persistent for this

foo_7 = Session.query(Foo).get(7)

assert o.foo is foo_7  # o.foo lazyloads on access

请注意,如果对象不是持久的,而是出现在Session中,则称为pending这意味着该对象的行尚未被插入到数据库中。对于这样的对象,在插入行之前设置foo_id没有意义。否则还没有行:

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# accessing an un-set attribute sets it to None
assert new_obj.foo is None

Session.flush()  # emits INSERT

# expire this because we already set .foo to None
Session.expire(o, ['foo'])

assert new_obj.foo is foo_7  # now it loads

非持久对象的属性加载

上面“等待”行为的一个变种是,如果我们在relationship()上使用flag load_on_pending当这个标志被设置时,惰性加载器将在INSERT进行之前为new_obj.foo发出;这种方法的另一个变体是使用Session.enable_relationship_loading()方法,它可以将一个对象“附加”到一个Session,这样多对一的关系根据外键属性加载,而不管对象处于什么特定的状态。Both techniques are not recommended for general use; they were added to suit specific programming scenarios encountered by users which involve the repurposing of the ORM’s usual object states.

配方ExpireRelationshipOnFKChange具有一个使用SQLAlchemy事件的例子,以协调外键属性与多对一关系的设置。

有没有办法自动地只有唯一的关键字(或其他类型的对象),而不查询关键字和获取包含该关键字的行的引用?

当人们阅读文档中的多对多示例时,如果您创建两次相同的Keyword,它会被放入数据库两次。这有点不方便。

这个UniqueObject配方是为了解决这个问题而创建的。