SqlAlchemy 2.0 中文文档(七十七)(2)https://developer.aliyun.com/article/1561176
行为变更 - ORM
当按属性查询时,现在会以它们的对象形式返回复合属性
现在,使用 Query 与复合属性一起,会返回该复合属性维护的对象类型,而不是拆分为各个列。使用在 复合列类型 中设置的映射:
>>> session.query(Vertex.start, Vertex.end).filter(Vertex.start == Point(3, 4)).all() [(Point(x=3, y=4), Point(x=5, y=6))]
此更改与期望将各个属性扩展为各个列的代码不兼容。要获得该行为,请使用 .clauses 访问器:
>>> session.query(Vertex.start.clauses, Vertex.end.clauses).filter( ... Vertex.start == Point(3, 4) ... ).all() [(3, 4, 5, 6)]
另请参阅
ORM 查询的列捆绑
#2824 ### Query.select_from() 不再将该子句应用于对应的实体
近期版本中,Query.select_from() 方法已被广泛使用,作为控制 Query 对象“选择的第一件事”的手段,通常是为了控制 JOIN 如何渲染。
与通常的 User 映射相比,请考虑以下示例:
select_stmt = select([User]).where(User.id == 7).alias() q = ( session.query(User) .join(select_stmt, User.id == select_stmt.c.id) .filter(User.name == "ed") )
上述声明可预期地渲染为以下 SQL:
SELECT "user".id AS user_id, "user".name AS user_name FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id WHERE "user".name = :name_1
如果我们想要颠倒 JOIN 的左右元素的顺序,文档会让我们相信可以使用Query.select_from()来实现:
q = ( session.query(User) .select_from(select_stmt) .join(User, User.id == select_stmt.c.id) .filter(User.name == "ed") )
然而,在 0.8 版本及更早版本中,上述对Query.select_from()的使用会将select_stmt应用于替换User实体,因为它选择了与User兼容的user表:
-- SQLAlchemy 0.8 and earlier... SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name FROM (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id WHERE anon_1.name = :name_1
上述语句很混乱,ON 子句引用了anon_1.id = anon_1.id,我们的 WHERE 子句也被anon_1替换了。
这种行为是完全有意的,但与已经流行的Query.select_from()的用例不同。上述行为现在可以通过一个名为Query.select_entity_from()的新方法实现。这是一个较少使用的行为,在现代的 SQLAlchemy 中大致相当于从自定义aliased()构造中选择:
select_stmt = select([User]).where(User.id == 7) user_from_stmt = aliased(User, select_stmt.alias()) q = session.query(user_from_stmt).filter(user_from_stmt.name == "ed")
因此,在 SQLAlchemy 0.9 中,我们从select_stmt选择的查询产生了我们期望的 SQL:
-- SQLAlchemy 0.9 SELECT "user".id AS user_id, "user".name AS user_name FROM (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id WHERE "user".name = :name_1
Query.select_entity_from() 方法将在 SQLAlchemy 0.8.2 中可用,因此依赖旧行为的应用程序可以首先过渡到该方法,确保所有测试继续正常运行,然后无问题地升级到 0.9。
#2736 ### viewonly=True 在 relationship() 上阻止历史记录生效
在relationship()上的viewonly标志被应用于防止对目标属性的更改在刷新过程中产生任何影响。这是通过在刷新过程中排除属性来实现的。然而,直到现在,对属性的更改仍然会将父对象标记为“脏”,并触发潜在的刷新。更改是viewonly标志现在还阻止为目标属性设置历史记录。像反向引用和用户定义事件之类的属性事件仍然正常运行。
更改如下所示:
from sqlalchemy import Column, Integer, ForeignKey, create_engine from sqlalchemy.orm import backref, relationship, Session from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import inspect Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) a = relationship("A", backref=backref("bs", viewonly=True)) e = create_engine("sqlite://") Base.metadata.create_all(e) a = A() b = B() sess = Session(e) sess.add_all([a, b]) sess.commit() b.a = a assert b in sess.dirty # before 0.9.0 # assert a in sess.dirty # assert inspect(a).attrs.bs.history.has_changes() # after 0.9.0 assert a not in sess.dirty assert not inspect(a).attrs.bs.history.has_changes()
#2833 ### 关联代理 SQL 表达式改进和修复
现在,通过关联代理实现的==和!=运算符,引用标量关系上的标量值,现在产生更完整的 SQL 表达式,旨在考虑“关联”行是否存在,当比较为None时。
考虑这个映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b_id = Column(Integer, ForeignKey("b.id"), primary_key=True) b = relationship("B") b_value = association_proxy("b", "value") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) value = Column(String)
直到 0.8 版本,像下面这样的查询:
s.query(A).filter(A.b_value == None).all()
会产生:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL)
在 0.9 中,现在产生:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE (EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL)) OR a.b_id IS NULL
不同之处在于,它不仅检查b.value,还检查a是否根本没有引用任何b行。对于使用这种类型比较的系统,一些父行没有关联行,这将与先前版本产生不同的结果。
更为关键的是,对于A.b_value != None,会发出正确的表达式。在 0.8 版本中,对于没有b的A行,这将返回True:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE NOT (EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL))
现在在 0.9 版本中,检查已经重新设计,以确保 A.b_id 行存在,另外B.value不为 NULL:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NOT NULL)
此外,has()操作符得到增强,使您可以只针对标量列值调用它,而不需要任何条件,它将生成检查关联行是否存在的条件:
s.query(A).filter(A.b_value.has()).all()
输出:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id)
这等同于A.b.has(),但允许直接针对b_value进行查询。
#2751 ### 关联代理缺失标量返回 None
从标量属性到标量的关联代理现在如果被代理的对象不存在将返回None。这与 SQLAlchemy 中缺失的一对多关联返回 None 的事实一致,因此代理值也应该如此。例如:
from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b = relationship("B", uselist=False) bname = association_proxy("b", "name") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) name = Column(String) a1 = A() # this is how m2o's always have worked assert a1.b is None # but prior to 0.9, this would raise AttributeError, # now returns None just like the proxied value. assert a1.bname is None
#2810 ### attributes.get_history()如果值不存在,默认情况下将从数据库查询
有关get_history()的修复 bug 允许基于列的属性查询到数据库中未加载的值,假设passive标志保持默认的PASSIVE_OFF。以前,此标志将不被尊重。此外,添加了一个新方法AttributeState.load_history()来补充AttributeState.history属性,它将为未加载的属性发出加载器可调用。
这是一个小改变的演示如下:
from sqlalchemy import Column, Integer, String, create_engine, inspect from sqlalchemy.orm import Session, attributes from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) data = Column(String) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) sess = Session(e) a1 = A(data="a1") sess.add(a1) sess.commit() # a1 is now expired # history doesn't emit loader callables assert inspect(a1).attrs.data.history == (None, None, None) # in 0.8, this would fail to load the unloaded state. assert attributes.get_history(a1, "data") == ( (), [ "a1", ], (), ) # load_history() is now equivalent to get_history() with # passive=PASSIVE_OFF ^ INIT_OK assert inspect(a1).attrs.data.load_history() == ( (), [ "a1", ], (), )
#2787 ### 当按属性基础查询时,复合属性现在以其对象形式返回
现在,与复合属性一起使用Query现在返回由该复合属性维护的对象类型,而不是分解为单独列。使用在复合列类型设置的映射:
>>> session.query(Vertex.start, Vertex.end).filter(Vertex.start == Point(3, 4)).all() [(Point(x=3, y=4), Point(x=5, y=6))]
这个改变与期望将单个属性扩展为单独列的代码不兼容。要获得该行为,请使用.clauses访问器:
>>> session.query(Vertex.start.clauses, Vertex.end.clauses).filter( ... Vertex.start == Point(3, 4) ... ).all() [(3, 4, 5, 6)]
另请参阅
ORM 查询的列捆绑
Query.select_from()不再将子句应用于相应的实体
近期版本中,Query.select_from()方法已经变得流行,作为控制Query对象“选择自”的一种方式,通常用于控制 JOIN 的渲染方式。
请考虑以下示例与通常的User映射相对比:
select_stmt = select([User]).where(User.id == 7).alias() q = ( session.query(User) .join(select_stmt, User.id == select_stmt.c.id) .filter(User.name == "ed") )
上述语句可预见地生成类似以下的 SQL:
SELECT "user".id AS user_id, "user".name AS user_name FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id WHERE "user".name = :name_1
如果我们想要颠倒 JOIN 的左右元素的顺序,文档会让我们相信可以使用Query.select_from()来实现:
q = ( session.query(User) .select_from(select_stmt) .join(User, User.id == select_stmt.c.id) .filter(User.name == "ed") )
然而,在 0.8 版本及之前,上述对Query.select_from()的使用会将select_stmt应用于替换User实体,因为它选择自与User兼容的user表:
-- SQLAlchemy 0.8 and earlier... SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name FROM (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id WHERE anon_1.name = :name_1
上述语句是一团糟,ON 子句引用了anon_1.id = anon_1.id,我们的 WHERE 子句也被替换为anon_1。
这种行为是完全有意的,但与Query.select_from()变得流行的用例不同。上述行为现在可以通过一个名为Query.select_entity_from()的新方法来实现。这是一个较少使用的行为,在现代的 SQLAlchemy 中大致相当于从自定义的aliased()构造中选择:
select_stmt = select([User]).where(User.id == 7) user_from_stmt = aliased(User, select_stmt.alias()) q = session.query(user_from_stmt).filter(user_from_stmt.name == "ed")
因此,在 SQLAlchemy 0.9 中,我们的从select_stmt选择的查询会产生我们期望的 SQL:
-- SQLAlchemy 0.9 SELECT "user".id AS user_id, "user".name AS user_name FROM (SELECT "user".id AS id, "user".name AS name FROM "user" WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id WHERE "user".name = :name_1
Query.select_entity_from()方法将在 SQLAlchemy 0.8.2中可用,因此依赖旧行为的应用程序可以首先过渡到这种方法,确保所有测试继续正常运行,然后无问题地升级到 0.9。
在relationship()上使用viewonly=True会阻止历史记录生效
在relationship()上的viewonly标志被应用以防止对目标属性的更改在刷新过程中产生任何影响。这是通过在刷新过程中不考虑该属性来实现的。然而,直到现在,对属性的更改仍会将父对象注册为“脏”,并触发潜在的刷新。改变是,viewonly标志现在也阻止为目标属性设置历史记录。像反向引用和用户定义事件这样的属性事件仍然正常运行。
改变如下所示:
from sqlalchemy import Column, Integer, ForeignKey, create_engine from sqlalchemy.orm import backref, relationship, Session from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import inspect Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) a = relationship("A", backref=backref("bs", viewonly=True)) e = create_engine("sqlite://") Base.metadata.create_all(e) a = A() b = B() sess = Session(e) sess.add_all([a, b]) sess.commit() b.a = a assert b in sess.dirty # before 0.9.0 # assert a in sess.dirty # assert inspect(a).attrs.bs.history.has_changes() # after 0.9.0 assert a not in sess.dirty assert not inspect(a).attrs.bs.history.has_changes()
关联代理 SQL 表达式改进和修复
通过一个关联代理实现的==和!=运算符,它引用标量关系上的标量值,现在会产生一个更完整的 SQL 表达式,旨在考虑“关联”行在与None比较时是否存在。
考虑以下映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b_id = Column(Integer, ForeignKey("b.id"), primary_key=True) b = relationship("B") b_value = association_proxy("b", "value") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) value = Column(String)
直到 0.8 版本,像下面这样的查询:
s.query(A).filter(A.b_value == None).all()
将产生:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL)
在 0.9 中,现在产生:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE (EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL)) OR a.b_id IS NULL
不同之处在于,它不仅检查b.value,还检查a是否根本没有指向任何b行。这将与先前版本产生不同的结果,对于使用这种类型比较的系统,其中一些父行没有关联行。
更为关键的是,对于A.b_value != None,现在会生成正确的表达式。在 0.8 中,对于没有b的A行,这将返回True:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE NOT (EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NULL))
现在在 0.9 版本中,检查已经重新设计,以确保A.b_id行存在,另外B.value不为 NULL:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id AND b.value IS NOT NULL)
此外,has()运算符得到增强,使您可以只针对标量列值调用它,而不需要任何条件,它将生成检查关联行是否存在的条件:
s.query(A).filter(A.b_value.has()).all()
输出:
SELECT a.id AS a_id, a.b_id AS a_b_id FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.b_id)
这等同于A.b.has(),但允许直接针对b_value进行查询。
关联代理缺失标量返回 None
从标量属性到标量的关联代理现在如果代理对象不存在将返回None。这与 SQLAlchemy 中缺少多对一关系返回 None 的事实一致,所以代理值也应该如此。例如:
from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b = relationship("B", uselist=False) bname = association_proxy("b", "name") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) name = Column(String) a1 = A() # this is how m2o's always have worked assert a1.b is None # but prior to 0.9, this would raise AttributeError, # now returns None just like the proxied value. assert a1.bname is None
attributes.get_history()如果值不存在将默认从数据库查询
有关get_history()的修复允许基于列的属性查询数据库中未加载的值,假设passive标志保持默认值PASSIVE_OFF。以前,这个标志不会被遵守。此外,新增了一个新方法AttributeState.load_history()来补充AttributeState.history属性,该属性将为未加载的属性发出加载器可调用。
这是一个小改变的示例:
from sqlalchemy import Column, Integer, String, create_engine, inspect from sqlalchemy.orm import Session, attributes from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) data = Column(String) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) sess = Session(e) a1 = A(data="a1") sess.add(a1) sess.commit() # a1 is now expired # history doesn't emit loader callables assert inspect(a1).attrs.data.history == (None, None, None) # in 0.8, this would fail to load the unloaded state. assert attributes.get_history(a1, "data") == ( (), [ "a1", ], (), ) # load_history() is now equivalent to get_history() with # passive=PASSIVE_OFF ^ INIT_OK assert inspect(a1).attrs.data.load_history() == ( (), [ "a1", ], (), )
行为变化 - 核心
类型对象不再接受被忽略的关键字参数
在 0.8 系列之前,大多数类型对象接受任意关键字参数,这些参数会被静默忽略:
from sqlalchemy import Date, Integer # storage_format argument here has no effect on any backend; # it needs to be on the SQLite-specific type d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d") # display_width argument here has no effect on any backend; # it needs to be on the MySQL-specific type i = Integer(display_width=5)
这是一个非常古老的 bug,为此在 0.8 系列中添加了一个弃用警告,但因为没有人会使用“-W”标志来运行 Python,所以几乎从未被看到:
$ python -W always::DeprecationWarning ~/dev/sqlalchemy/test.py /Users/classic/dev/sqlalchemy/test.py:5: SADeprecationWarning: Passing arguments to type object constructor <class 'sqlalchemy.types.Date'> is deprecated d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d") /Users/classic/dev/sqlalchemy/test.py:9: SADeprecationWarning: Passing arguments to type object constructor <class 'sqlalchemy.types.Integer'> is deprecated i = Integer(display_width=5)
从 0.9 系列开始,TypeEngine中的“catch all”构造函数被移除,这些无意义的参数不再被接受。
利用方言特定参数如storage_format和display_width的正确方式是使用适当的方言特定类型:
from sqlalchemy.dialects.sqlite import DATE from sqlalchemy.dialects.mysql import INTEGER d = DATE(storage_format="%(day)02d.%(month)02d.%(year)04d") i = INTEGER(display_width=5)
那么当我们也想要方言无关的类型时怎么办?我们使用TypeEngine.with_variant()方法:
from sqlalchemy import Date, Integer from sqlalchemy.dialects.sqlite import DATE from sqlalchemy.dialects.mysql import INTEGER d = Date().with_variant( DATE(storage_format="%(day)02d.%(month)02d.%(year)04d"), "sqlite" ) i = Integer().with_variant(INTEGER(display_width=5), "mysql")
TypeEngine.with_variant()并不是新功能,它是在 SQLAlchemy 0.7.2 中添加的。因此,在 0.8 系列上运行的代码可以校正为使用这种方法,并在升级到 0.9 之前进行测试。
None不再能被用作“部分 AND”构造函数
None不再能被用作“后备”来逐步形成 AND 条件。即使一些 SQLAlchemy 内部使用了这种模式,这种模式也没有被记录在案:
condition = None for cond in conditions: condition = condition & cond if condition is not None: stmt = stmt.where(condition)
当conditions不为空时,上述序列在 0.9 上会产生SELECT .. WHERE AND NULL。None不再被隐式忽略,而是与在其他上下文中解释None时保持一致。
0.8 和 0.9 的正确代码应该是:
from sqlalchemy.sql import and_ if conditions: stmt = stmt.where(and_(*conditions))
另一个在 0.9 上适用于所有后端的变体,在 0.8 上只适用于支持布尔常量的后端:
from sqlalchemy.sql import true condition = true() for cond in conditions: condition = cond & condition stmt = stmt.where(condition)
在 0.8 版本中,这将生成一个 SELECT 语句,其 WHERE 子句中始终包含AND true,这不被不支持布尔常量的后端所接受(MySQL、MSSQL)。在 0.9 版本中,true常量将在and_()连接中被删除。
另请参见
改进的布尔常量、NULL 常量、连接词的呈现方式
create_engine() 的“password”部分不再将+号视为编码空格。
由于某种原因,Python 函数unquote_plus()被应用于 URL 的password字段,这是对RFC 1738中描述的编码规则的错误应用,因为它将空格转义为加号。现在 URL 的字符串化仅编码“:”、“@”或“/”,不再应用于username和password字段(以前仅应用于密码)。在解析时,编码字符被转换,但加号和空格保持不变:
# password: "pass word + other:words" dbtype://user:pass word + other%3Awords@host/dbname # password: "apples/oranges" dbtype://username:apples%2Foranges@hostspec/database # password: "apples@oranges@@" dbtype://username:apples%40oranges%40%40@hostspec/database # password: '', username is "username@" dbtype://username%40:@hostspec/database
#2873 ### COLLATE 的优先规则已更改
以前,类似以下的表达式:
print((column("x") == "somevalue").collate("en_EN"))
将会产生如下表达式:
-- 0.8 behavior (x = :x_1) COLLATE en_EN
上述内容被 MSSQL 误解,通常不是任何数据库建议的语法。该表达式现在将产生大多数数据库文档所示的语法:
-- 0.9 behavior x = :x_1 COLLATE en_EN
如果ColumnOperators.collate() 操作符被应用于右侧列,则可能会出现潜在的不兼容更改:
print(column("x") == literal("somevalue").collate("en_EN"))
在 0.8 版本中,会产生:
x = :param_1 COLLATE en_EN
然而在 0.9 版本中,将会产生更准确的,但可能不是您想要的形式:
x = (:param_1 COLLATE en_EN)
ColumnOperators.collate() 操作符现在在ORDER BY表达式中更为适当地工作,因为ASC和DESC操作符已被赋予特定的优先级,这将再次确保不会生成括号:
>>> # 0.8 >>> print(column("x").collate("en_EN").desc()) (x COLLATE en_EN) DESC >>> # 0.9 >>> print(column("x").collate("en_EN").desc()) x COLLATE en_EN DESC
#2879 ### PostgreSQL CREATE TYPE AS ENUM 现在对值应用引号
ENUM 类型现在将对枚举值中的单引号符号进行转义:
>>> from sqlalchemy.dialects import postgresql >>> type = postgresql.ENUM("one", "two", "three's", name="myenum") >>> from sqlalchemy.dialects.postgresql import base >>> print(base.CreateEnumType(type).compile(dialect=postgresql.dialect())) CREATE TYPE myenum AS ENUM ('one','two','three''s')
已经转义单引号符号的现有解决方法需要进行修改,否则它们现在将会双重转义。
类型对象不再接受被忽略的关键字参数
直到 0.8 系列,大多数类型对象接受任意关键字参数,这些参数被静默忽略:
from sqlalchemy import Date, Integer # storage_format argument here has no effect on any backend; # it needs to be on the SQLite-specific type d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d") # display_width argument here has no effect on any backend; # it needs to be on the MySQL-specific type i = Integer(display_width=5)
这是一个非常古老的 bug,已经在 0.8 系列中添加了弃用警告,但因为几乎没有人使用“-W”标志来运行 Python,所以几乎从未被看到:
$ python -W always::DeprecationWarning ~/dev/sqlalchemy/test.py /Users/classic/dev/sqlalchemy/test.py:5: SADeprecationWarning: Passing arguments to type object constructor <class 'sqlalchemy.types.Date'> is deprecated d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d") /Users/classic/dev/sqlalchemy/test.py:9: SADeprecationWarning: Passing arguments to type object constructor <class 'sqlalchemy.types.Integer'> is deprecated i = Integer(display_width=5)
从 0.9 系列开始,TypeEngine中的“catch all”构造函数已被移除,这些无意义的参数不再被接受。
使用方言特定参数(如storage_format和display_width)的正确方法是使用适当的方言特定类型:
from sqlalchemy.dialects.sqlite import DATE from sqlalchemy.dialects.mysql import INTEGER d = DATE(storage_format="%(day)02d.%(month)02d.%(year)04d") i = INTEGER(display_width=5)
如果我们还想要方言不可知的类型怎么办?我们使用TypeEngine.with_variant()方法:
from sqlalchemy import Date, Integer from sqlalchemy.dialects.sqlite import DATE from sqlalchemy.dialects.mysql import INTEGER d = Date().with_variant( DATE(storage_format="%(day)02d.%(month)02d.%(year)04d"), "sqlite" ) i = Integer().with_variant(INTEGER(display_width=5), "mysql")
TypeEngine.with_variant()并不是新功能,它是在 SQLAlchemy 0.7.2 中添加的。因此,在 0.8 系列上运行的代码可以根据需要使用这种方法进行更正并在升级到 0.9 之前进行测试。
None不再能够被用作“部分 AND”构造函数
None不再能够被用作逐步形成 AND 条件的“后备”。尽管一些 SQLAlchemy 内部使用了这种模式,但这种模式并未被记录:
condition = None for cond in conditions: condition = condition & cond if condition is not None: stmt = stmt.where(condition)
上述序列在conditions非空时,将在 0.9 上产生SELECT .. WHERE AND NULL。None不再被隐式忽略,而是与在其他上下文中解释None时保持一致。
对于 0.8 和 0.9 的正确代码应该是:
from sqlalchemy.sql import and_ if conditions: stmt = stmt.where(and_(*conditions))
另一个变体在 0.9 上适用于所有后端,但在 0.8 上仅适用于支持布尔常量的后端:
from sqlalchemy.sql import true condition = true() for cond in conditions: condition = cond & condition stmt = stmt.where(condition)
在 0.8 上,这将产生一个 SELECT 语句,在 WHERE 子句中始终有AND true,这不被不支持布尔常量的后端(MySQL、MSSQL)接受。在 0.9 上,true常量将在and_()连接中被删除。
另请参阅
布尔常量、NULL 常量、连接的改进渲染
create_engine()的“password”部分不再将+号视为编码空格
由于某种原因,Python 函数unquote_plus()被应用于 URL 的password字段,这是对RFC 1738中描述的编码规则的错误应用,因为它将空格转义为加号。现在,URL 的字符串化仅编码“:”、“@”或“/”,而不再应用于username和password字段(以前仅应用于密码)。在解析时,编码字符被转换,但加号和空格保持不变:
# password: "pass word + other:words" dbtype://user:pass word + other%3Awords@host/dbname # password: "apples/oranges" dbtype://username:apples%2Foranges@hostspec/database # password: "apples@oranges@@" dbtype://username:apples%40oranges%40%40@hostspec/database # password: '', username is "username@" dbtype://username%40:@hostspec/database
COLLATE 的优先规则已更改
以前,类似以下的表达式:
print((column("x") == "somevalue").collate("en_EN"))
会产生如下表达式:
-- 0.8 behavior (x = :x_1) COLLATE en_EN
上述方法被 MSSQL 误解,通常不是任何数据库建议的语法。现在该表达式将产生大多数数据库文档所示的语法:
-- 0.9 behavior x = :x_1 COLLATE en_EN
如果 ColumnOperators.collate() 操作符应用于右列,则可能会出现潜在的不兼容变化,如下所示:
print(column("x") == literal("somevalue").collate("en_EN"))
在 0.8 中,这将产生:
x = :param_1 COLLATE en_EN
但在 0.9 中,现在将产生更准确的,但可能不是您想要的形式:
x = (:param_1 COLLATE en_EN)
ColumnOperators.collate() 操作符现在在 ORDER BY 表达式中更合适地工作,因为给了 ASC 和 DESC 操作符一个特定的优先级,这将再次确保不生成括号:
>>> # 0.8 >>> print(column("x").collate("en_EN").desc()) (x COLLATE en_EN) DESC >>> # 0.9 >>> print(column("x").collate("en_EN").desc()) x COLLATE en_EN DESC
PostgreSQL CREATE TYPE AS ENUM 现在对值应用引用
ENUM 类型现在将在枚举值中应用转义到单引号符:
>>> from sqlalchemy.dialects import postgresql >>> type = postgresql.ENUM("one", "two", "three's", name="myenum") >>> from sqlalchemy.dialects.postgresql import base >>> print(base.CreateEnumType(type).compile(dialect=postgresql.dialect())) CREATE TYPE myenum AS ENUM ('one','two','three''s')
已经转义单引号的现有解决方法将需要修改,否则它们将会双重转义。
SqlAlchemy 2.0 中文文档(七十七)(4)https://developer.aliyun.com/article/1561181