SQLAlchemy 1.3有什么新功能?
关于此文档
本文档描述了SQLAlchemy版本1.2和SQLAlchemy版本1.3之间的更改。
介绍
本指南介绍了SQLAlchemy 1.3版的新增功能,并记录了影响用户将其应用程序从1.2系列SQLAlchemy迁移到1.3的更改。
请仔细查看有关行为变化的章节,了解行为中潜在的向后不兼容的变化。
一般
对所有已弃用的元素发出弃用警告;添加了新的弃用
1.3版确保所有被否决的行为和API,包括那些多年来一直被列为“遗留”的行为和API,都在发出 DeprecationWarning
警告。这包括使用诸如 Session.weak_identity_map
以及诸如 MapperExtension
. 虽然在文档中已经注意到了所有的拒绝,但它们通常没有使用适当的重新构造的文本指令,或者没有将它们包含在不推荐使用的版本中。某个特定的API功能是否实际发出了拒绝警告,这是不一致的。一般的看法是,大多数或所有这些不推荐使用的功能都被视为长期遗留功能,而不打算删除它们。
变更包括所有记录在案的弃用现在在文档中使用一个具有版本号的适当的重组文本指令,在未来版本中将删除该特性或用例的措辞是明确的(例如,不再有遗留的永远的用例),并且任何此类特性或用例的使用肯定会发出一个 DeprecationWarning
在python 3中,以及在使用现代测试工具(如pytest)时,标准错误流中的这种方法现在变得更加明确了。其目标是,这些长期不推荐使用的功能,可以追溯到0.7或0.6版,应该开始完全删除,而不是将它们保留为“遗留”功能。此外,从1.3版起,一些主要的新折旧正在添加中。由于SQLAlchemy有14年的实际使用经验,成千上万的开发人员,它有可能指向一个单一的用例流,很好地融合在一起,并修剪掉针对这种单一工作方式工作的特性和模式。
更大的上下文是,sqlAlchemy试图适应即将到来的纯python 3的世界,以及一个类型注释的世界,朝着这个目标 实验性的 计划对SQLAlchemy进行重大的重做,希望这将大大减少API的认知负载,并对核心和ORM在实现和使用方面的巨大差异进行重大的传递。当这两个系统在sqlacalchemy第一次发布后发生了显著的变化时,尤其是ORM仍然保留了许多“栓接”的行为,这些行为使核心和ORM之间的隔离墙过高。通过提前将API集中在每个支持的用例的单个模式上,最终迁移到显著改变的API的工作将变得更简单。
有关1.3中添加的最主要折旧,请参见下面的链接部分。
参见
新功能和改进-ORM
与别名类的关系取代了对非主映射器的需要
“非主映射器”是 mapper()
创建于 命令(又称经典)映射 样式,它作为一个附加的映射器,针对已映射的类,针对不同类型的可选对象。非主映射器的根位于0.1、0.2系列的sqlAlchemy中,在该系列中,预期 mapper()
对象将作为主查询构造接口,在 Query
对象存在。
随着 Query
后来 AliasedClass
构造,非主映射器的大多数用例都消失了。这是一件好事,因为sqlacalchemy也远离了0.5系列周围的“经典”映射,转而支持声明性系统。
当意识到一些非常难以定义的 relationship()
当具有可选选项的非主映射器作为映射目标而不是试图构造一个 relationship.primaryjoin
它包含了特定对象间关系的所有复杂性。
随着这个用例越来越流行,它的限制变得明显,包括非主映射器很难针对添加新列的可选对象进行配置,映射器不继承原始映射的关系,非主映射器上显式配置的关系不起作用。对于加载器选项,非主映射器也不提供基于列的属性的完整功能命名空间,这些属性可以在查询中使用(同样,在以前的0.1-0.4天中,可以使用 Table
直接使用ORM的对象)。
缺失的部分是允许 relationship()
直接指 AliasedClass
. 这个 AliasedClass
已经完成了我们希望非主映射器执行的所有操作;它允许从可选的加载项加载现有的映射类,它继承现有映射器的所有属性和关系,它与加载程序选项一起工作得非常好,并且它提供了一个类状的对象,可以像它的类一样混合到查询中。自我。通过此更改,以前用于非主映射器的配方位于 配置关系联接方式 更改为别名类。
AT 与别名类的关系 ,原始的非主映射器如下所示:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = mapper( B, j, non_primary=True, primary_key=[j.c.b_id], properties={ "id": j.c.b_id, # so that 'id' looks the same as before "c_id": j.c.c_id, # needed for disambiguation "d_c_id": j.c.d_c_id, # needed for disambiguation "b_id": [j.c.b_id, j.c.d_b_id], "d_id": j.c.d_id, } ) A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
属性是必需的,以便重新映射其他列,以便它们不会与映射到的现有列冲突 B
还需要定义一个新的主键。
使用新的方法,所有这些冗长的内容都会消失,并且在建立关系时直接引用附加的列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = aliased(B, j, flat=True) A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
现在不推荐使用非主映射器,其最终目标是将经典映射作为一个特性完全去掉。声明性API将成为映射的单一方法,有望允许内部改进和简化,以及更清晰的文档故事。
selectin加载不再使用join实现简单的一对多
1.2中添加的“select in”加载功能引入了一种非常有效的新方法,可以快速加载集合,在许多情况下,这比“subquery”快速加载快得多,因为它不依赖于重述原始的select查询,而是使用简单的in子句。但是,“selectin”加载仍然依赖于在父表和相关表之间呈现联接,因为它需要行中的父主键值来匹配行。在1.3中,添加了一个新的优化,它将在最常见的一对多加载情况下省略此联接,其中相关行已经包含父行的主键,该主键在其外键列中表示。这再次提供了显著的性能改进,因为ORM现在可以在一个查询中加载大量集合,而完全不使用联接或子查询。
给定映射:
class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) bs = relationship("B", lazy="selectin") class B(Base): __tablename__ = 'b' id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
在1.2版本的“selectin”加载中,A到B的加载如下:
SELECT a.id AS a_id FROM a SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id FROM a AS a_1 JOIN b ON a_1.id = b.a_id WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
对于新的行为,负载看起来像:
SELECT a.id AS a_id FROM a SELECT b.a_id AS b_a_id, b.id AS b_id FROM b WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为是自动释放的,使用类似于lazy-loading使用的启发式方法来确定是否可以直接从标识映射中提取相关实体。然而,与大多数查询特性一样,由于与多态加载相关的高级场景,特性的实现变得更加复杂。如果遇到问题,用户应该报告一个bug,但是更改还包括一个标志 relationship.omit_join
可以设置为 False
上 relationship()
禁用优化。
多对一查询表达式行为的改进
在构建将多对一关系与对象值进行比较的查询时,例如:
u1 = session.query(User).get(5) query = session.query(Address).filter(Address.user == u1)
上面的表达式 Address.user == u1
,它最终编译为SQL表达式,通常基于 User
类物体 "address.user_id = 5"
,使用延迟的可调用项来检索值 5
在绑定表达式中尽可能晚。这两个用例都适合 Address.user == u1
表达式可能反对 User
对象尚未刷新,它依赖于服务器生成的主键值以及表达式始终返回正确的结果,即使主键值为 u1
自创建表达式以来已更改。
然而,这种行为的副作用是 u1
最后在计算表达式时过期,将生成一个附加的select语句,如果 u1
也脱离了 Session
,将引发错误::
u1 = session.query(User).get(5) query = session.query(Address).filter(Address.user == u1) session.expire(u1) session.expunge(u1) query.all() # <-- would raise DetachedInstanceError
当 Session
承诺和 u1
实例超出范围,因为 Address.user == u1
表达式不强引用对象本身,只引用其 InstanceState
.
解决方法是允许 Address.user == u1
用于计算值的表达式 5
基于尝试像现在一样在表达式编译时检索或加载值,但如果对象已分离并且已过期,则将根据 InstanceState
当该属性过期时,它将记忆该状态下特定属性的最后一个已知值。此机制仅对特定属性启用/ InstanceState
当表达式功能需要时,可以节省性能/内存开销。
最初,尝试了一些更简单的方法,例如使用各种安排立即计算表达式,以便在以后尝试加载值(如果不存在的话),但是困难的边缘情况是要更改的列属性(通常是自然主键)的值。为了确保 Address.user == u1
始终返回当前状态的正确答案 u1
,它将返回持久化对象的当前数据库持久化值,必要时通过select query取消激发;对于分离的对象,它将返回最新的已知值,无论何时使用 InstanceState
它跟踪列属性的最后一个已知值,无论何时该属性将过期。
现代属性API功能用于在无法计算值时指示特定的错误消息,其中两种情况是,列属性从未设置,以及在进行第一次计算并分离对象时对象已过期。在所有情况下, DetachedInstanceError
不再提升。
多对一替换不会为“raiseload”提升,也不会为“old”对象分离。
如果关系没有指定 relationship.active_history
标志,将不会为分离的对象引发断言::
a1 = session.query(Address).filter_by(id=5).one() session.expunge(a1) a1.user = some_user
以上,当 .user
已在分离的上替换属性 a1
对象,A DetachedInstanceError
将在属性尝试检索的前一个值时引发 .user
从身份地图。更改是操作现在在没有加载旧值的情况下继续进行。
同样的变化也发生在 lazy="raise"
装载机策略:
class Address(Base): # ... user = relationship("User", ..., lazy="raise")
以前,协会 a1.user
将调用“raiseload”异常,因为属性试图检索上一个值。在加载“old”值的情况下,现在将跳过此断言。
为ORM属性实现“del”
Python del
操作不能真正用于映射的属性(标量列或对象引用)。已添加支持以使其正常工作,其中 del
操作大致相当于将属性设置为 None
价值:
some_object = session.query(SomeObject).get(5) del some_object.some_attribute # from a SQL perspective, works like "= None"
添加到InstanceState的信息字典
增加了 .info
字典到 InstanceState
类,来自调用的对象 inspect()
在映射的对象上。这允许自定义配方添加有关对象的附加信息,该对象将与该对象在内存中的整个生命周期一起携带:
from sqlalchemy import inspect u1 = User(id=7, name='ed') inspect(u1).info['user_info'] = '7|ed'
水平切分扩展支持批量更新和删除方法
这个 ShardedQuery
扩展对象支持 Query.update()
和 Query.delete()
批量更新/删除方法。这个 query_chooser
调用Callable时会参考它们,以便根据给定的条件跨多个碎片运行更新/删除。
关联代理改进
虽然不是因为任何特定的原因,但是关联代理扩展在这个循环中有很多改进。
关联代理具有新的cascade_scalar_deletes标志
给定映射为:
class A(Base): __tablename__ = 'test_a' id = Column(Integer, primary_key=True) ab = relationship( 'AB', backref='a', uselist=False) b = association_proxy( 'ab', 'b', creator=lambda b: AB(b=b), cascade_scalar_deletes=True) class B(Base): __tablename__ = 'test_b' id = Column(Integer, primary_key=True) ab = relationship('AB', backref='b', cascade='all, delete-orphan') class AB(Base): __tablename__ = 'test_ab' a_id = Column(Integer, ForeignKey(A.id), primary_key=True) b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
分配给 A.b
将生成一个 AB
对象:
a.b = B()
这个 A.b
关联是标量的,并且包含一个新标志 AssociationProxy.cascade_scalar_deletes
. 设置时,设置 A.b
到 None
将移除 A.ab
也。默认行为保持为离开 a.ab
到位:
a.b = None assert a.ab is None
虽然一开始看起来很直观,这个逻辑应该只关注现有关系的“级联”属性,但仅从这个角度来看,如果应该删除代理对象,就不清楚了,因此行为可以作为一个显式选项使用。
此外, del
现在对scalars的工作方式与设置为 None
::
del a.b assert a.ab is None
AssociationProxy按类存储特定于类的状态
这个 AssociationProxy
对象根据与之关联的父映射类做出许多决策。而 AssociationProxy
历史上,它开始时是一个相对简单的“getter”,很明显,它在早期还需要决定它所指的属性类型,例如标量或集合、映射对象或简单值,以及类似的属性。为了实现这一点,它需要检查映射的属性或它所引用的其他描述符或属性(从其父类引用)。然而,在Python描述符机制中,只有在类上下文中访问描述符时,描述符才会了解其“父”类,例如调用 MyClass.some_descriptor
,它调用 __get__()
在类中传递的方法。这个 AssociationProxy
因此,对象将存储特定于该类的状态,但仅当调用此方法时;尝试在不首先访问 AssociationProxy
因为描述符会引起错误。此外,它还假定 __get__()
将是它需要知道的唯一父类。尽管如此,如果一个特定的类有继承子类,那么关联代理实际上是代表多个父类工作的,即使它没有被显式地重新使用。尽管有了这个缺点,关联代理仍然会与当前的行为保持相当的距离,但在某些情况下,它仍然会留下缺点,以及确定最佳“所有者”类的复杂问题。
这些问题现在都解决了 AssociationProxy
当 __get__()
调用;相反,每个类生成一个新对象,称为 AssociationProxyInstance
它处理特定映射父类的所有特定状态(当父类未映射时,否 AssociationProxyInstance
是生成的)。尽管在1.1中有所改进,但关联代理的单一“拥有类”概念基本上已被一种方法所取代,即AP现在可以平等地对待任意数量的“拥有”类。
以适应希望检查此状态的应用程序 AssociationProxy
不必打电话 __get__()
一种新方法 AssociationProxy.for_class()
提供对特定类的直接访问的 AssociationProxyInstance
,演示为:
class User(Base): # ... keywords = association_proxy('kws', 'keyword') proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了 AssociationProxyInstance
对象,在上面的示例中,存储在 proxy_state
变量,我们可以查看特定于 User.keywords
代理,如 target_class
::
>>> proxy_state.target_class Keyword
AssociationProxy现在为面向列的目标提供标准列运算符
给定一个 AssociationProxy
其中目标是数据库列,并且 not 对象引用或其他关联代理:
class User(Base): # ... elements = relationship("Element") # column-based association proxy values = association_proxy("elements", "value") class Element(Base): # ... value = Column(String)
这个 User.values
关联代理是指 Element.value
列。现在可以使用标准列操作,例如 like
::
>>> print(s.query(User).filter(User.values.like('%foo%'))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
::
>>> print(s.query(User).filter(User.values == 'foo')) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value = :value_1)
当与比较时 None
, the IS NULL
表达式被一个相关行根本不存在的测试所扩充;这与以前的行为相同::
>>> print(s.query(User).filter(User.values == None)) SELECT "user".id AS user_id FROM "user" WHERE (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id))
请注意 ColumnOperators.contains()
运算符实际上是字符串比较运算符; 这是行为的改变 在此之前,关联代理使用 .contains
仅作为列表包含运算符。通过面向列的比较,它现在的行为类似于“like”::
>>> print(s.query(User).filter(User.values.contains('foo'))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试 User.values
值的简单成员身份集合 "foo"
,equals运算符(例如 User.values == 'foo'
)应该使用;这也适用于以前的版本。
将基于对象的关联代理与集合一起使用时,行为与之前一样,即测试集合成员身份的行为,例如给定的映射:
class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) user_elements = relationship("UserElement") # object-based association proxy elements = association_proxy("user_elements", "element") class UserElement(Base): __tablename__ = 'user_element' id = Column(Integer, primary_key=True) user_id = Column(ForeignKey("user.id")) element_id = Column(ForeignKey("element.id")) element = relationship("Element") class Element(Base): __tablename__ = 'element' id = Column(Integer, primary_key=True) value = Column(String)
这个 .contains()
方法生成与以前相同的表达式,测试 User.elements
为了一个 Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1)))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM user_element WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,更改是基于作为 AssociationProxy按类存储特定于类的状态 ;由于代理现在在生成表达式时剥离附加状态,因此对象目标和列目标版本的 AssociationProxyInstance
班级。
关联代理现在强引用父对象
只保留对父对象弱引用的关联代理集合的长期行为将被还原;只要代理集合本身也在内存中,代理现在将保持对父对象的强引用,从而消除“过时的关联代理”错误。这种改变是在实验的基础上进行的,目的是看看是否有任何用例在产生副作用的地方出现。
例如,给定一个带有关联代理的映射:
class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) bs = relationship("B") b_data = association_proxy('bs', 'data') class B(Base): __tablename__ = 'b' id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) data = Column(String) a1 = A(bs=[B(data='b1'), B(data='b2')]) b_data = a1.b_data
以前,如果 a1
已从范围外删除::
del a1
尝试迭代 b_data
之后收集 a1
从作用域中删除将引发错误 "stale association proxy, parent object has gone out of scope"
. 这是因为关联代理需要访问 a1.bs
集合以生成视图,在进行此更改之前,它只保留弱引用 a1
. 特别是,用户在执行内联操作时经常会遇到此错误,例如:
collection = session.query(A).filter_by(id=1).first().b_data
上面,因为 A
对象将在 b_data
实际上使用了集合。
变化是 b_data
集合现在维护对 a1
对象,使其保持存在:
assert b_data == ['b1', 'b2']
此更改引入的副作用是,如果应用程序如上所述在集合周围传递, 父对象不会被垃圾收集 直到集合也被丢弃。一如既往,如果 a1
在特定的 Session
,它将保持该会话状态的一部分,直到被垃圾收集为止。
请注意,如果此更改导致问题,则可以对其进行修订。
对集合、带有AssociationProxy的dict执行大容量替换
将集合或字典分配给关联代理集合现在应该可以正常工作,而在为现有键重新创建关联代理成员之前,它将导致由于删除+插入同一对象而可能出现的刷新失败问题,现在应该只在适当的情况下创建新的关联对象:
class A(Base): __tablename__ = "test_a" id = Column(Integer, primary_key=True) b_rel = relationship( "B", collection_class=set, cascade="all, delete-orphan", ) b = association_proxy("b_rel", "value", creator=lambda x: B(value=x)) class B(Base): __tablename__ = "test_b" __table_args__ = (UniqueConstraint("a_id", "value"),) id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False) value = Column(String) # ... s = Session(e) a = A(b={"x", "y", "z"}) s.add(a) s.commit() # re-assign where one B should be deleted, one B added, two # B's maintained a.b = {"x", "z", "q"} # only 'q' was added, so only one new B object. previously # all three would have been re-created leading to flush conflicts # against the deleted ones. assert len(s.new) == 1
在删除操作期间,对集合重复项进行多对一的backref检查
当ORM映射的集合作为一个Python序列存在时,通常是一个Python list
默认为 relationship()
,包含重复项,对象从其一个位置而不是另一个位置移除,多对一backref将其属性设置为 None
即使一对多的一面仍然代表着物体的存在。即使一对多集合在关系模型中不能有重复的集合,也会映射一个ORM relationship()
如果使用序列集合,则在内存中可以有重复项,但有一个限制,即此重复状态既不能持久化,也不能从数据库中检索。特别是,列表中临时存在一个重复项是Python“swap”操作的固有特性。给定标准一对多/多对一设置:
class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) bs = relationship("B", backref="a") class B(Base): __tablename__ = 'b' id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
如果我们有 A
两个对象 B
成员,并执行交换:
a1 = A(bs=[B(), B()]) a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作过程中,拦截标准python __setitem__
__delitem__
方法传递一个临时状态,其中 B()
对象在集合中出现两次。当 B()
对象从其中一个位置移除,即 B.a
backref会将引用设置为 None
,导致 A
和 B
冲洗过程中要移除的对象。同样的问题可以用简单的重复来演示:
>>> a1 = A() >>> b1 = B() >>> a1.bs.append(b1) >>> a1.bs.append(b1) # append the same b1 object twice >>> del a1.bs[1] >>> a1.bs # collection is unaffected so far... [<__main__.B object at 0x7f047af5fb70>] >>> b1.a # however b1.a is None >>> >>> session.add(a1) >>> session.commit() # so upon flush + expire.... >>> a1.bs # the value is gone []
修复程序确保当backref触发时(即在集合发生变化之前),在将many设置为one side之前,使用线性搜索(目前使用的是 list.search
和 list.__contains__
.
最初,人们认为需要在集合内部使用基于事件的引用计数方案,以便在集合的整个生命周期中跟踪所有重复实例,这将对所有集合操作(包括非常频繁的操作)产生性能/内存/复杂性影响。加载和附加。相反,所采用的方法将额外费用限制在收集移除和批量替换的较不常见操作上,并且观察到的线性扫描开销可以忽略不计;关系绑定收集的线性扫描已在工作单元内以及在批量替换收集时使用。
关键行为变化-ORM
join()在更明确地确定“左”端时处理模糊性
历史上,给出如下查询:
u_alias = aliased(User) session.query(User, u_alias).join(Address)
给定标准教程映射,查询将生成一个FROM子句,如下所示:
SELECT ... FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id
也就是说,联接将隐式地针对第一个匹配的实体。新的行为是异常请求解决这种模糊性::
sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to join from, there are multiple FROMS which can join to this entity. Try adding an explicit ON clause to help resolve the ambiguity.
解决方案是提供一个ON子句,作为表达式::
# join to User session.query(User, u_alias).join(Address, Address.user_id == User.id) # join to u_alias session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)
或者使用关系属性(如果可用)::
# join to User session.query(User, u_alias).join(Address, User.addresses) # join to u_alias session.query(User, u_alias).join(Address, u_alias.addresses)
更改包括,如果联接不含糊,则联接现在可以正确链接到不是列表中第一个元素的FROM子句::
session.query(func.current_timestamp(), User).join(Address)
在此增强之前,上述查询将引发:
sqlalchemy.exc.InvalidRequestError: Don't know how to join from CURRENT_TIMESTAMP; please use select_from() to establish the left entity/selectable of this join
现在查询工作正常:
SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password FROM users JOIN addresses ON users.id = addresses.user_id
总的来说,这一变化直接指向了Python的“显式优于隐式”哲学。
FOR UPDATE子句在联接的EAGER LOAD子查询内以及外部呈现
此更改特别适用于 joinedload()
与行限制查询结合使用的加载策略,例如使用 Query.first()
或 Query.limit()
以及使用 Query.with_for_update()
方法。
给出的查询为:
session.query(A).options(joinedload(A.b)).limit(5)
这个 Query
对象在联接的“热切加载”与“限制”组合时呈现以下所选表单:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM ( SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 ) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
这样一来,主实体的行数限制就可以实现,而不会影响相关项的连接的热切负载。当上述查询与“select..for update”组合时,行为如下:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM ( SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 ) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
但是,mysql是因为https://bugs.mysql.com/bug.php?id=90693不锁定子查询中的行,这与PostgreSQL和其他数据库不同。所以上面的查询现在呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM ( SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE ) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
在Oracle方言中,由于Oracle不支持此语法,并且方言跳过了针对子查询的任何“for update”,因此不会呈现内部“for update”;在任何情况下都不需要这样做,因为Oracle(如PostgreSQL)正确锁定了返回行的所有元素。
当使用 Query.with_for_update.of
修饰符,通常在postgresql上,省略了外部的“for update”,现在的of在内部呈现;以前,of target不会被转换以正确地适应子查询。因此给出:
session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)
查询现在呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM ( SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a ) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
上面的表单对PostgreSQL还有帮助,因为PostgreSQL不允许在左外部连接目标之后呈现for update子句。
总的来说,for-update仍然高度特定于正在使用的目标数据库,并且不容易针对更复杂的查询进行通用化。
对于从集合中删除的对象,被动删除将保留fk不变。
这个 relationship.passive_deletes
选项接受值 "all"
指示刷新对象时不应修改任何外键属性,即使关系的集合/引用已被删除。以前,在以下情况下,一对多或一对一关系不会发生这种情况:
class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) addresses = relationship( "Address", passive_deletes="all") class Address(Base): __tablename__ = 'addresses' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('users.id')) user = relationship("User") u1 = session.query(User).first() address = u1.addresses[0] u1.addresses.remove(address) session.commit() # would fail and be set to None assert address.user_id == u1.id
现在修复包括 address.user_id
保持不变 passive_deletes="all"
. 这种方法对于构建自定义的“版本表”方案非常有用,例如,将行存档而不是删除。
新功能和改进-核心
新的多列命名约定令牌,长名称截断
以适应以下情况: MetaData
命名约定需要在多个列约束之间消除歧义,并且希望使用生成的约束名称中的所有列,将添加一系列新的命名约定标记,包括 column_0N_name
, column_0_N_name
, column_0N_key
, column_0_N_key
, referred_column_0N_name
, referred_column_0_N_name
,等等,呈现约束中所有列的列名(或键或标签),这些列没有分隔符或使用下划线分隔符联接在一起。下面我们定义一个将命名为 UniqueConstraint
具有将所有列的名称联接在一起的名称的约束::
metadata_obj = MetaData(naming_convention={ "uq": "uq_%(table_name)s_%(column_0_N_name)s" }) table = Table( 'info', metadata_obj, Column('a', Integer), Column('b', Integer), Column('c', Integer), UniqueConstraint('a', 'b', 'c') )
上面表的创建表将呈现为:
CREATE TABLE info ( a INTEGER, b INTEGER, c INTEGER, CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c) )
此外,长名称截断逻辑现在应用于由命名约定生成的名称,特别是为了适应可以生成非常长名称的多列标签。此逻辑与在select语句中截断长标签名称时使用的逻辑相同,它用一个确定生成的4字符哈希替换超出目标数据库标识符长度限制的多余字符。例如,在标识符不能超过63个字符的PostgreSQL上,通常会从下面的表定义生成一个长约束名:
long_names = Table( 'long_names', metadata_obj, Column('information_channel_code', Integer, key='a'), Column('billing_convention_name', Integer, key='b'), Column('product_identifier', Integer, key='c'), UniqueConstraint('a', 'b', 'c') )
截断逻辑将确保不会为唯一约束生成过长的名称::
CREATE TABLE long_names ( information_channel_code INTEGER, billing_convention_name INTEGER, product_identifier INTEGER, CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e UNIQUE (information_channel_code, billing_convention_name, product_identifier) )
以上后缀 a79e
基于长名称的MD5哈希,每次都将生成相同的值,以为给定架构生成一致的名称。
注意,截断逻辑也会引发 IdentifierError
当约束名对于给定方言显式太大时。这是 Index
对象很长一段时间,但现在也应用于其他类型的约束::
from sqlalchemy import Column from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import Table from sqlalchemy import UniqueConstraint from sqlalchemy.dialects import postgresql from sqlalchemy.schema import AddConstraint m = MetaData() t = Table("t", m, Column("x", Integer)) uq = UniqueConstraint( t.c.x, name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql", ) print(AddConstraint(uq).compile(dialect=postgresql.dialect()))
输出:
sqlalchemy.exc.IdentifierError: Identifier 'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql' exceeds maximum length of 63 characters
异常引发会阻止产生被数据库后端截断的不确定约束名,这些约束名随后与数据库迁移不兼容。
要将SQLAlchemy端截断规则应用于上述标识符,请使用 conv()
结构:
uq = UniqueConstraint( t.c.x, name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"), )
这将再次输出确定性截断的SQL,如下所示:
ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)
目前没有一个选项可以让名称通过以允许数据库端截断。这已经是事实了 Index
有一段时间没有提到名字和问题。
这一变化还修复了另外两个问题。一个是 column_0_key
令牌不可用,即使记录了此令牌,另一个是 referred_column_0_name
令牌将不经意地呈现 .key
而不是 .name
如果这两个值不同,则返回该列。
参见
SQL函数的二进制比较解释
这种增强是在核心级别实现的,但是主要适用于ORM。
比较两个元素的SQL函数现在可以用作“比较”对象,适合在ORM中使用。 relationship()
,首先使用 func
工厂,然后当函数完成时调用 FunctionElement.as_comparison()
要生成的修饰符 BinaryExpression
有一个“左”和一个“右”边:
class Venue(Base): __tablename__ = 'venue' id = Column(Integer, primary_key=True) name = Column(String) descendants = relationship( "Venue", primaryjoin=func.instr( remote(foreign(name)), name + "/" ).as_comparison(1, 2) == 1, viewonly=True, order_by=name )
上面, relationship.primaryjoin
“后代”关系将根据传递给的第一个和第二个参数生成“左”和“右”表达式。 instr()
. 这允许像ORM Lazyload这样的功能生成类似SQL的:
SELECT venue.id AS venue_id, venue.name AS venue_name FROM venue WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name ('parent1', '/', 1)
以及连接负载,例如:
v1 = s.query(Venue).filter_by(name="parent1").options( joinedload(Venue.descendants)).one()
工作如下:
SELECT venue.id AS venue_id, venue.name AS venue_name, venue_1.id AS venue_1_id, venue_1.name AS venue_1_name FROM venue LEFT OUTER JOIN venue AS venue_1 ON instr(venue_1.name, (venue.name || ?)) = ? WHERE venue.name = ? ORDER BY venue_1.name ('/', 1, 'parent1')
此功能将有助于处理一些情况,例如在关系联接条件中使用几何函数,或者在任何情况下,SQL联接的ON子句都是用SQL函数表示的。
扩展功能现在支持空列表
1.2版中介绍的“扩展”功能 参数集后期扩展允许在具有缓存语句的表达式中使用 现在支持传递给 ColumnOperators.in_()
操作员。空列表的实现将生成一个特定于目标后端的“空集”表达式,例如“select cast(null as integer)”,其中1!=1“对于PostgreSQL,从(选择1)中选择1作为empty_set where 1!=“1”用于MySQL::
>>> from sqlalchemy import create_engine >>> from sqlalchemy import select, literal_column, bindparam >>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True) >>> with e.connect() as conn: ... conn.execute( ... select([literal_column('1')]). ... where(literal_column('1').in_(bindparam('q', expanding=True))), ... q=[] ... ) ... SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)
该特性也适用于面向元组的语句,其中“empty in”表达式将被扩展以支持在元组内给定的元素,如postgresql::
>>> from sqlalchemy import create_engine >>> from sqlalchemy import select, literal_column, tuple_, bindparam >>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True) >>> with e.connect() as conn: ... conn.execute( ... select([literal_column('1')]). ... where(tuple_(50, "somestring").in_(bindparam('q', expanding=True))), ... q=[] ... ) ... SELECT 1 WHERE (%(param_1)s, %(param_2)s) IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)
类型引擎方法绑定表达式,列表达式与变量、类型特定的类型一起使用
这个 TypeEngine.bind_expression()
和 TypeEngine.column_expression()
方法现在可以在特定数据类型的“impl”上使用,从而允许方言以及 TypeDecorator
和 Variant
用例。
以下示例说明了 TypeDecorator
将SQL时间转换函数应用于 LargeBinary
. 为了使此类型在 Variant
编译器需要钻取变量表达式的“impl”以定位这些方法:
from sqlalchemy import TypeDecorator, LargeBinary, func class CompressedLargeBinary(TypeDecorator): impl = LargeBinary def bind_expression(self, bindvalue): return func.compress(bindvalue, type_=self) def column_expression(self, col): return func.uncompress(col, type_=self) MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")
以上表达式将在仅用于sqlite时呈现SQL中的函数::
from sqlalchemy import select, column from sqlalchemy.dialects import sqlite print(select([column('x', CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))
将呈现:
SELECT uncompress(x) AS x
这种变化还包括方言可以实现 TypeEngine.bind_expression()
和 TypeEngine.column_expression()
在方言级别的实现类型上,它们现在将被使用;特别是,这将用于MySQL新的“二进制前缀”要求,以及为MySQL强制转换十进制绑定值。
队列池的新后进先出策略
连接池通常由 create_engine()
被称为 QueuePool
. 这个池使用的对象相当于Python的内置 Queue
类以存储等待使用的数据库连接。这个 Queue
特性先进先出行为,旨在提供对池中持久存在的数据库连接的循环使用。但是,这一点的一个潜在缺点是,当池的利用率较低时,对每个串联连接的重复使用意味着服务器端的超时策略(尝试减少未使用的连接)无法关闭这些连接。为了适应这个用例,一个新的标志 create_engine.pool_use_lifo
是添加的,它使 .get()
方法 Queue
从队列的开始而不是结束拉取连接,实质上是将“队列”转换为“堆栈”(添加一个称为 StackPool
不过,这太冗长了)。
参见
关键变更-核心
将字符串SQL片段强制为text()已完全删除
在1.0版中首次添加的警告,如中所述 将完整的SQL片段强制转换为文本()时发出警告 ,现在已转换为异常。对于自动强制传递给类似 Query.filter()
和 Select.order_by()
正在转换为 text()
构造,即使这已发出警告。在情况下 Select.order_by()
, Query.order_by()
, Select.group_by()
和 Query.group_by()
,字符串标签或列名仍然解析为相应的表达式构造,但是如果解析失败,则 CompileError
从而阻止直接呈现原始SQL文本。
“threadlocal”引擎策略已弃用
在sqlalchemy0.2的周围添加了“threadlocal引擎策略”,以解决sqlalchemy0.1中的标准操作方式(可以概括为“threadlocal everything”)缺乏的问题。回想起来,似乎相当荒谬的是,在SQLAlchemy的第一个版本中,在各方面都是“alpha”的时候,人们担心太多的用户已经决定使用现有的API而不能简单地更改它。
SQLAlchemy的原始使用模型如下所示:
engine.begin() table.insert().execute(<params>) result = table.select().execute() table.update().execute(<params>) engine.commit()
在现实世界中使用了几个月之后,很明显,试图假装“连接”或“事务”是一个隐藏的实现细节是一个坏主意,特别是当某人需要一次处理多个数据库连接时。因此,我们今天看到的使用范例被引入了,去掉了上下文管理器,因为它们在Python中还不存在:
conn = engine.connect() try: trans = conn.begin() conn.execute(table.insert(), <params>) result = conn.execute(table.select()) conn.execute(table.update(), <params>) trans.commit() except: trans.rollback() raise finally: conn.close()
上面的范例是人们需要的,但是由于它仍然有点冗长(因为没有上下文管理器),旧的工作方式也保留了下来,它成为了线程本地引擎策略。
今天,由于上下文管理器的存在,使用core比原始模式更简洁,甚至更简洁:
with engine.begin() as conn: conn.execute(table.insert(), <params>) result = conn.execute(table.select()) conn.execute(table.update(), <params>)
此时,仍然依赖“threadlocal”风格的任何剩余代码都将通过这种反预测来实现现代化——该特性应该被下一个主要的sqlacalchemy系列(例如1.4)完全删除。连接池参数 Pool.use_threadlocal
也不推荐使用,因为它在大多数情况下实际上没有任何效果,正如 Engine.contextual_connect()
方法,通常与 Engine.connect()
方法,但在使用ThreadLocal引擎的情况下除外。
转换unicode参数已弃用
参数 String.convert_unicode
和 create_engine.convert_unicode
已弃用。这些参数的目的是指示sqlAlchemy确保在传递到数据库之前,python 2下传入的python unicode对象编码为bytestrings,并期望数据库中的bytestrings转换回python unicode对象。在Python3之前的时代,这是一个巨大的考验,因为几乎所有的PythonDBAPI都没有默认启用的Unicode支持,而且大多数DBAPI提供的Unicode扩展都存在重大问题。最后,sqlacalchemy添加了C扩展,这些扩展的主要目的之一是加速结果集中的Unicode解码过程。
python3引入后,DBAPIs开始更加全面地支持Unicode,更重要的是,默认情况下。但是,特定的DBAPI将或不从结果返回Unicode数据,以及接受Python Unicode值作为参数的条件仍然非常复杂。这是“convert_unicode”标志过时的开始,因为它们不再足以确保编码/解码只在需要的地方进行,而不是在不需要的地方进行。相反,“convert_unicode”开始被方言自动检测到。部分原因可以在引擎第一次连接时发出的“SELECT‘test plain returns’”和“SELECT‘test_unicode_returns’”SQL中看到;方言正在测试当前DBAPI及其当前设置和后端数据库连接是否默认返回unicode。
最终结果是,在任何情况下都不需要最终用户使用“convert_unicode”标志,如果需要,则sqlachemy项目需要知道这些情况是什么以及原因。目前,数百个Unicode往返测试通过所有主要数据库,而不使用此标志,因此有相当高的信心,它们不再需要,除非在可论证的非使用情况下,例如从旧数据库访问错误编码的数据,这将更适合使用自定义类型。
方言改进与变化-PostgreSQL
添加了对PostgreSQL分区表的基本反射支持
sqlAlchemy可以使用标志在postgresql create table语句中呈现“partition by”序列。 postgresql_partition_by
,在版本1.2.6中添加。然而, 'p'
直到现在,类型还不是所使用的反射查询的一部分。
给定一个模式,例如:
dv = Table( 'data_values', metadata_obj, Column('modulus', Integer, nullable=False), Column('data', String(30)), postgresql_partition_by='range(modulus)') sa.event.listen( dv, "after_create", sa.DDL( "CREATE TABLE data_values_4_10 PARTITION OF data_values " "FOR VALUES FROM (4) TO (10)") )
两个表名 'data_values'
和 'data_values_4_10'
会回来的 Inspector.get_table_names()
此外,这些列还将从 Inspector.get_columns('data_values')
以及 Inspector.get_columns('data_values_4_10')
. 这也延伸到 Table(..., autoload=True)
带着这些桌子。
方言改进和变化-MySQL
协议级ping现在用于预ping
mysql方言包括mysqlclient、python mysql、pymysql和mysql connector python现在使用 connection.ping()
池预Ping功能的方法,如所述 断开操作-悲观 . 这比以前在连接上发出“选择1”的方法要轻得多。
在重复的密钥更新时控制内部的参数排序
更新参数的顺序 ON DUPLICATE KEY UPDATE
现在可以通过传递两个元组的列表来显式排序子句::
from sqlalchemy.dialects.mysql import insert insert_stmt = insert(my_table).values( id='some_existing_id', data='inserted value') on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update( [ ("data", "some data"), ("updated_at", func.current_timestamp()), ], )
参见
方言改进和变化-sqlite
添加了对sqlite json的支持
一种新的数据类型 JSON
它代表 JSON
基本数据类型。SQLite JSON_EXTRACT
和 JSON_QUOTE
实现使用函数来提供基本的JSON支持。
请注意,在数据库中呈现的数据类型本身的名称是名称“json”。这将创建一个具有“numeric”关联性的sqlite数据类型,这通常不应该是一个问题,除非JSON值包含单个整数值。不过,下面是sqlite自己的文档(https://www.sqlite.org/json1.html)中的一个示例,json这个名称用于熟悉它。
添加约束冲突时支持sqlite
sqlite支持非标准的on conflict子句,该子句可以为独立约束以及一些列内联约束(如not null)指定。已通过添加对这些条款的支持 sqlite_on_conflict
关键字添加到如下对象 UniqueConstraint
以及一些 Column
-特定变体:
some_table = Table( 'some_table', metadata_obj, Column('id', Integer, primary_key=True, sqlite_on_conflict_primary_key='FAIL'), Column('data', Integer), UniqueConstraint('id', 'data', sqlite_on_conflict='IGNORE') )
上面的表将在create table语句中呈现为:
CREATE TABLE some_table ( id INTEGER NOT NULL, data INTEGER, PRIMARY KEY (id) ON CONFLICT FAIL, UNIQUE (id, data) ON CONFLICT IGNORE )
参见
方言改进和变化-Oracle
通用Unicode不强调的国家字符数据类型,使用选项重新启用
这个 Unicode
和 UnicodeText
默认情况下,数据类型现在对应于 VARCHAR2
和 CLOB
Oracle上的数据类型,而不是 NVARCHAR2
和 NCLOB
(也称为“国家”字符集类型)。这将在行为中看到,例如它们如何呈现 CREATE TABLE
语句,以及不会将任何类型对象传递给 setinputsizes()
绑定参数时使用 Unicode
或 UnicodeText
使用;cx_oracle本机处理字符串值。此更改基于cx_Oracle维护人员的建议,即Oracle中的“National”数据类型大部分已过时且未执行。在某些情况下,它们也会发生干扰,例如当应用于类似函数的格式说明符时 trunc()
.
只有一个案例 NVARCHAR2
对于不使用符合Unicode的字符集的数据库,可能需要相关的类型。在这种情况下,标志 use_nchar_for_unicode
可以传递给 create_engine()
重新启用旧行为。
一如既往,使用 NVARCHAR2
和 NCLOB
数据类型将继续显式使用 NVARCHAR2
和 NCLOB
,包括在DDL中以及使用cx-oracle处理绑定参数时 setinputsizes()
.
在读取端,python 2下的自动unicode转换被添加到char/varchar/clob结果行中,以匹配python 3下的cx_Oracle行为。为了减轻cx-oracle方言以前在python 2下的这种行为对性能的影响,在python 2下使用了sqlachemy的非常高性能(在构建C扩展时)本机Unicode处理程序。可以通过设置 coerce_to_unicode
标记为false。此标志现在默认为true,并应用于结果集中未显式位于 Unicode
或Oracle的nvarchar2/nchar/nclob数据类型。
cx_Oracle Connect参数已现代化,已删除不推荐使用的参数
cx-oracle方言接受的参数以及url字符串的一系列现代化:
不推荐使用的参数
auto_setinputsizes
,allow_twophase
,exclude_setinputsizes
被移除。的值
threaded
默认情况下,不再生成参数,对于sqlAlchemy方言,该参数始终默认为true。圣卢西亚Connection
对象本身不被认为是线程安全的,因此不需要传递此标志。不赞成通过
threaded
到create_engine()
本身。设置threaded
到True
,传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
.在URL查询字符串上传递的所有参数,如果不是特别使用的,现在将传递给cx_oracle.connect()函数。其中的一部分还被强制转换成cx-oracle常量或booleans,包括
mode
,purity
,events
和threaded
.如前所述,所有的cx-oracle
.connect()
通过create_engine.connect_args
字典,关于这方面的文档是不准确的。
方言改进和更改-SQL Server
支持PYODBC Fast_ExecuteMany
pyodbc最近添加的“fast_executemany”模式在使用Microsoft ODBC驱动程序时可用,现在是pyodbc/mssql方言的一个选项。通过它 create_engine()
::
engine = create_engine( "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server", fast_executemany=True)
参见
影响标识开始和增量的新参数,不推荐使用序列
从SQL Server 2012起,SQL Server现在支持序列 CREATE SEQUENCE
语法。在 #4235 ,SQLAlchemy将使用 Sequence
和其他方言一样。然而,目前的情况是 Sequence
已在SQL Server上重新调整用途,以影响 IDENTITY
主键列的规范。为了使向正态序列的转换也可用,使用 Sequence
将在整个1.3系列中发出一个弃用警告。为了影响“开始”和“增量”,使用新的 mssql_identity_start
和 mssql_identity_increment
参数开 Column
::
test = Table( 'test', metadata_obj, Column( 'id', Integer, primary_key=True, mssql_identity_start=100, mssql_identity_increment=10 ), Column('name', String(20)) )
为了发射 IDENTITY
在使用很少但有效的SQL Server用例的非主键列上,使用 Column.autoincrement
标志,设置为 True
在目标列上, False
在任何整数主键列上::
test = Table( 'test', metadata_obj, Column('id', Integer, primary_key=True, autoincrement=False), Column('number', Integer, autoincrement=True) )
参见
已更改语句错误格式(换行符和%s)
对字符串表示法引入了两个更改 StatementError
. 字符串表示形式的“detail”和“sql”部分现在用换行符分隔,并维护原始SQL语句中的换行符。其目的是提高可读性,同时仍将原始错误消息保持在一行上,以便进行日志记录。
这意味着以前看起来像这样的错误消息:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?'] (Background on this error at: https://sqlalche.me/e/cd3x)
现在看起来像这样:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id' [SQL: select * from reviews where id = ?] (Background on this error at: https://sqlalche.me/e/cd3x)
这一变化的主要影响是,消费者不能再假设一个完整的异常消息在一行上,但是从DBAPI驱动程序或SQLAlchemy内部生成的原始“错误”部分仍然在第一行上。