SqlAlchemy 2.0 中文文档(七十七)(3)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB PostgreSQL 版,企业版 4核16GB
推荐场景:
HTAP混合负载
简介: SqlAlchemy 2.0 中文文档(七十七)

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=Truerelationship() 上阻止历史记录生效

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 版本中,对于没有bA行,这将返回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 查询的列捆绑

#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

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()

#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 中,对于没有bA行,这将返回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()的修复允许基于列的属性查询数据库中未加载的值,假设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

行为变化 - 核心

类型对象不再接受被忽略的关键字参数

在 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_formatdisplay_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 NULLNone不再被隐式忽略,而是与在其他上下文中解释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 的字符串化仅编码“:”、“@”或“/”,不再应用于usernamepassword字段(以前仅应用于密码)。在解析时,编码字符被转换,但加号和空格保持不变:

# 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表达式中更为适当地工作,因为ASCDESC操作符已被赋予特定的优先级,这将再次确保不会生成括号:

>>> # 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') 

已经转义单引号符号的现有解决方法需要进行修改,否则它们现在将会双重转义。

#2878

类型对象不再接受被忽略的关键字参数

直到 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_formatdisplay_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 NULLNone不再被隐式忽略,而是与在其他上下文中解释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 的字符串化仅编码“:”、“@”或“/”,而不再应用于usernamepassword字段(以前仅应用于密码)。在解析时,编码字符被转换,但加号和空格保持不变:

# 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 表达式中更合适地工作,因为给了 ASCDESC 操作符一个特定的优先级,这将再次确保不生成括号:

>>> # 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') 

已经转义单引号的现有解决方法将需要修改,否则它们将会双重转义。

#2878


SqlAlchemy 2.0 中文文档(七十七)(4)https://developer.aliyun.com/article/1561181

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
SQL JSON 测试技术
SqlAlchemy 2.0 中文文档(七十五)(2)
SqlAlchemy 2.0 中文文档(七十五)
27 3
|
1月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(七十一)(4)
SqlAlchemy 2.0 中文文档(七十一)
24 1
|
1月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(4)
SqlAlchemy 2.0 中文文档(七十五)
27 1
|
1月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(四十一)(5)
SqlAlchemy 2.0 中文文档(四十一)
33 6
|
1月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(1)
SqlAlchemy 2.0 中文文档(七十五)
40 4
|
1月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(四十一)(4)
SqlAlchemy 2.0 中文文档(四十一)
28 4
|
1月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(四十一)(3)
SqlAlchemy 2.0 中文文档(四十一)
33 4
|
1月前
|
Oracle 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十一)(1)
SqlAlchemy 2.0 中文文档(七十一)
20 1
|
1月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十七)(4)
SqlAlchemy 2.0 中文文档(七十七)
23 0
|
1月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十七)(5)
SqlAlchemy 2.0 中文文档(七十七)
15 0