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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
人工智能 自然语言处理 监控
大数据&AI产品月刊【2023年10月】
大数据&AI产品技术月刊【2023年10月】,涵盖本月技术速递、产品和功能发布、市场和客户应用实践等内容,帮助您快速了解阿里云大数据&AI方面最新动态。
大数据&AI产品月刊【2023年10月】
|
数据采集 编解码 运维
PMU
PMU
816 1
|
网络协议 算法 Java
|
9月前
|
安全 网络安全 数据安全/隐私保护
为什么网站会出现不安全提示
**网站不安全提示:原因与应对** 访问网站时遇到“不安全”警告,通常由以下原因引起:1. 未使用 HTTPS 协议,数据易被窃取;2. SSL/TLS 证书问题,如过期或无效;3. 混合内容,HTTPS 网站加载 HTTP 资源;4. 恶意内容,存在钓鱼或恶意代码。用户应谨慎访问、避免输入敏感信息,确保浏览器和杀毒软件更新。网站管理员需及时排查并修复漏洞,保障网站安全。共同维护网络安全环境至关重要。
|
11月前
|
应用服务中间件 Linux 网络安全
nginx安装部署ssl证书,同时支持http与https方式访问
为了使HTTP服务支持HTTPS访问,需生成并安装SSL证书,并确保Nginx支持SSL模块。首先,在`/usr/local/nginx`目录下生成RSA密钥、证书申请文件及自签名证书。接着,确认Nginx已安装SSL模块,若未安装则重新编译Nginx加入该模块。最后,编辑`nginx.conf`配置文件,启用并配置HTTPS服务器部分,指定证书路径和监听端口(如20000),保存后重启Nginx完成部署。
3568 8
|
编解码 Dart 网络协议
"震撼揭秘!Flutter如何玩转超低延迟RTSP/RTMP播放,跨平台视频流体验大升级,让你的应用秒变直播神器!"
【8月更文挑战第15天】Flutter作为跨平台UI框架,以其高效性和丰富生态著称。本文详述如何利用flutter_vlc_player等插件在Flutter中实现低延迟RTSP/RTMP播放,并提供代码示例。通过优化播放器设置,如禁用缓冲、启用帧丢弃等,可进一步减少延迟,提升用户观看体验,展现了Flutter在视频流媒体应用中的强大潜力。
519 0
|
SQL 数据库连接 API
SqlAlchemy 2.0 中文文档(二十八)(5)
SqlAlchemy 2.0 中文文档(二十八)
500 0
|
网络协议 算法 网络性能优化
一文带你了解tcp协议
一文带你了解tcp协议
|
小程序
【微信小程序6】引入第三方UI的方法(ColorUi)
【微信小程序6】引入第三方UI的方法(ColorUi)
1203 0
|
开发框架 小程序 JavaScript
高颜值微信小程序 UI 组件库!
高颜值微信小程序 UI 组件库!
783 1