SQLAlchemy 1.2 中的新内容是什么?
关于本文档
本文描述了 SQLAlchemy 1.1 版本与 SQLAlchemy 1.2 版本之间的更改。
简介
本指南介绍了 SQLAlchemy 版本 1.2 中的新功能,并记录了影响用户将其应用程序从 SQLAlchemy 1.1 系列迁移到 1.2 系列的更改。
请仔细查看行为更改部分,可能会出现不兼容的行为更改。
平台支持
针对 Python 2.7 及更高版本
SQLAlchemy 1.2 现在将最低 Python 版本提高到 2.7,不再支持 2.6。预计会将不支持 Python 2.6 的新语言特性合并到 1.2 系列中。对于 Python 3 的支持,SQLAlchemy 目前在版本 3.5 和 3.6 上进行了测试。
ORM 中的新功能和改进
“Baked” 加载现在是懒加载的默认设置
sqlalchemy.ext.baked
扩展是在 1.0 系列中首次引入的,允许构建所谓的 BakedQuery
对象,它是一个生成 Query
对象的对象,与表示查询结构的缓存键相结合;然后将此缓存键链接到生成的字符串 SQL 语句,以便后续使用具有相同结构的另一个 BakedQuery
将绕过构建 Query
对象的所有开销,构建内部的核心 select()
对象,以及 select()
编译为字符串,大大减少了通常与构建和发出 ORM Query
对象相关的函数调用开销。
当 ORM 为惰性加载一个relationship()
构造生成“懒惰”查询时,默认情况下现在使用BakedQuery
,例如默认的lazy="select"
关系加载器策略。这将允许在应用程序使用惰性加载查询加载集合和相关对象的范围内显著减少函数调用。以前,此功能在 1.0 和 1.1 中通过使用全局 API 方法或使用baked_select
策略可用,现在是此行为的唯一实现。该功能还得到了改进,以便对于在延迟加载后生效的具有附加加载器选项的对象仍然可以进行缓存。
可以通过relationship.bake_queries
标志在每个关系基础上禁用缓存行为,这对于非常罕见的情况非常有用,比如使用不兼容缓存的自定义Query
实现的关系。
#3954 ### 新的“selectin”急切加载,使用 IN 一次加载所有集合
添加了一个名为“selectin”加载的新急切加载器,这在许多方面类似于“子查询”加载,但是生成了一个更简单的 SQL 语句,该语句也可以缓存并且更有效。
给定如下查询:
q = ( session.query(User) .filter(User.name.like("%ed%")) .options(subqueryload(User.addresses)) )
生成的 SQL 将是针对User
的查询,然后是User.addresses
的子查询加载(注意还列出了参数):
SELECT users.id AS users_id, users.name AS users_name FROM users WHERE users.name LIKE ? ('%ed%',) SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.email_address AS addresses_email_address, anon_1.users_id AS anon_1_users_id FROM (SELECT users.id AS users_id FROM users WHERE users.name LIKE ?) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id ('%ed%',)
使用“selectin”加载,我们得到一个 SELECT 语句,该语句引用在父查询中加载的实际主键值:
q = ( session.query(User) .filter(User.name.like("%ed%")) .options(selectinload(User.addresses)) )
产生:
SELECT users.id AS users_id, users.name AS users_name FROM users WHERE users.name LIKE ? ('%ed%',) SELECT users_1.id AS users_1_id, addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.email_address AS addresses_email_address FROM users AS users_1 JOIN addresses ON users_1.id = addresses.user_id WHERE users_1.id IN (?, ?) ORDER BY users_1.id (1, 3)
上述 SELECT 语句包括以下优点:
- 它不使用子查询,只使用 INNER JOIN,这意味着在像 MySQL 这样不喜欢子查询的数据库上性能会更好。
- 其结构与原始查询无关;与新的扩展的 IN 参数系统结合,我们在大多数情况下可以使用“烘焙”查询来缓存字符串 SQL,从而显著减少每个查询的开销。
- 由于查询仅针对给定的主键标识符列表进行,“selectin” 加载可能与
Query.yield_per()
兼容,以便一次操作 SELECT 结果的一部分,前提是数据库驱动程序允许多个同时游标(SQLite、PostgreSQL;不是 MySQL 驱动程序或 SQL Server ODBC 驱动程序)。联接式急切加载和子查询急切加载都不兼容Query.yield_per()
。
selectin 急切加载的缺点是可能产生大量的 SQL 查询,具有大量的 IN 参数列表。IN 参数列表本身被分组为每组 500 个,因此超过 500 个主对象的结果集将有更多的额外“SELECT IN”查询。此外,对复合主键的支持取决于数据库能否使用包含 IN 的元组,例如 (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?))
。目前,已知 PostgreSQL 和 MySQL 兼容此语法,SQLite 不兼容。
另请参阅
选择 IN 加载
#3944 ### “selectin” 多态加载,使用单独的 IN 查询加载子类
与刚刚描述的“selectin”关系加载功能类似的是“selectin”多态加载。这是一个专门针对联接式急切加载的多态加载功能,允许基本实体的加载通过简单的 SELECT 语句进行,然后额外子类的属性通过额外的 SELECT 语句进行加载:
>>> from sqlalchemy.orm import selectin_polymorphic >>> query = session.query(Employee).options( ... selectin_polymorphic(Employee, [Manager, Engineer]) ... ) >>> query.all() SELECT employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type FROM employee () SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.type AS employee_type, engineer.engineer_name AS engineer_engineer_name FROM employee JOIN engineer ON employee.id = engineer.id WHERE employee.id IN (?, ?) ORDER BY employee.id (1, 2) SELECT manager.id AS manager_id, employee.id AS employee_id, employee.type AS employee_type, manager.manager_name AS manager_manager_name FROM employee JOIN manager ON employee.id = manager.id WHERE employee.id IN (?) ORDER BY employee.id (3,)
另请参阅
使用 selectin_polymorphic()
#3948 ### ORM 属性可以接收临时 SQL 表达式
新的 ORM 属性类型 query_expression()
被添加,类似于 deferred()
,不同之处在于它的 SQL 表达式是在查询时确定的,使用了一个新选项 with_expression()
;如果未指定,则属性默认为 None
:
from sqlalchemy.orm import query_expression from sqlalchemy.orm import with_expression class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) x = Column(Integer) y = Column(Integer) # will be None normally... expr = query_expression() # but let's give it x + y a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first() print(a1.expr)
另请参阅
查询时 SQL 表达式作为映射属性
#3058 ### ORM 支持多表删除
ORM Query.delete()
方法支持多表条件的 DELETE,就像在支持多表条件的 DELETE 中介绍的那样。该功能的工作方式与在 0.8 中首次引入的 UPDATE 的多表条件相同,并在 Query.update()支持 UPDATE…FROM 中描述。
下面,我们对SomeEntity
执行 DELETE 操作,并添加一个 FROM 子句(或等效的,取决于后端)对SomeOtherEntity
进行操作:
query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter( SomeOtherEntity.foo == "bar" ).delete()
另请参阅
支持多表条件的 DELETE
#959 ### 支持混合属性、复合属性的批量更新
现在混合属性(例如sqlalchemy.ext.hybrid
)以及复合属性(复合列类型)在使用Query.update()
时支持在 UPDATE 语句的 SET 子句中使用。
对于混合属性,可以直接使用简单表达式,或者可以使用新的装饰器hybrid_property.update_expression()
将一个值拆分为多个列/表达式:
class Person(Base): # ... first_name = Column(String(10)) last_name = Column(String(10)) @hybrid.hybrid_property def name(self): return self.first_name + " " + self.last_name @name.expression def name(cls): return func.concat(cls.first_name, " ", cls.last_name) @name.update_expression def name(cls, value): f, l = value.split(" ", 1) return [(cls.first_name, f), (cls.last_name, l)]
上面,可以使用以下方式呈现 UPDATE:
session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})
类似的功能也适用于复合属性,其中复合值将被拆分为其各个列以进行批量 UPDATE:
session.query(Vertex).update({Edge.start: Point(3, 4)})
另请参阅
允许批量 ORM 更新 ### 混合属性支持在子类之间重用,重新定义@getter
sqlalchemy.ext.hybrid.hybrid_property
类现在支持在子类中多次调用诸如@setter
、@expression
等的变异器,并且现在提供了@getter
变异器,以便特定的混合属性可以在子类或其他类中重新使用。这与标准 Python 中@property
的行为类似:
class FirstNameOnly(Base): # ... first_name = Column(String) @hybrid_property def name(self): return self.first_name @name.setter def name(self, value): self.first_name = value class FirstNameLastName(FirstNameOnly): # ... last_name = Column(String) @FirstNameOnly.name.getter def name(self): return self.first_name + " " + self.last_name @name.setter def name(self, value): self.first_name, self.last_name = value.split(" ", maxsplit=1) @name.expression def name(cls): return func.concat(cls.first_name, " ", cls.last_name)
上面,FirstNameOnly.name
混合属性被FirstNameLastName
子类引用,以便将其专门用于新子类。这是通过在每次调用@getter
、@setter
以及所有其他变异器方法(如@expression
)中将混合对象复制到新对象中来实现的,从而保持先前混合属性的定义不变。以前,诸如@setter
的方法会直接修改现有的混合属性,干扰了超类的定义。
注意
请务必阅读在子类之间重用混合属性处的文档,了解如何覆盖hybrid_property.expression()
和hybrid_property.comparator()
的重要注意事项,因为在某些情况下可能需要一个特殊的限定符hybrid_property.overrides
来避免与QueryableAttribute
发生名称冲突。
注意
这种对@hybrid_property
的更改意味着,当向@hybrid_property
添加 setter 和其他状态时,方法必须保留原始混合的名称,否则具有附加状态的新混合将作为不匹配的名称存在于类中。这与标准 Python 的@property
构造的行为相同:
class FirstNameOnly(Base): @hybrid_property def name(self): return self.first_name # WRONG - will raise AttributeError: can't set attribute when # assigning to .name @name.setter def _set_name(self, value): self.first_name = value class FirstNameOnly(Base): @hybrid_property def name(self): return self.first_name # CORRECT - note regular Python @property works the same way @name.setter def name(self, value): self.first_name = value
#3912 ### 新的 bulk_replace 事件
为了适应 A @validates method receives all values on bulk-collection set before comparison 中描述的验证用例,添加了一个新的AttributeEvents.bulk_replace()
方法,该方法与AttributeEvents.append()
和AttributeEvents.remove()
事件一起调用。“bulk_replace”在“append”和“remove”之前调用,以便在与现有集合进行比较之前修改集合。之后,单个项目将附加到新的目标集合,触发对于集合中新项目的“append”事件,这与以前的行为相同。下面同时说明了“bulk_replace”和“append”,包括“append”将接收已由“bulk_replace”处理的对象(如果使用集合赋值)。一个新的符号attributes.OP_BULK_REPLACE
可用于确定此“append”事件是否是批量替换的第二部分:
from sqlalchemy.orm.attributes import OP_BULK_REPLACE @event.listens_for(SomeObject.collection, "bulk_replace") def process_collection(target, values, initiator): values[:] = [_make_value(value) for value in values] @event.listens_for(SomeObject.collection, "append", retval=True) def process_collection(target, value, initiator): # make sure bulk_replace didn't already do it if initiator is None or initiator.op is not OP_BULK_REPLACE: return _make_value(value) else: return value
#3896 ### 新的“modified”事件处理程序用于 sqlalchemy.ext.mutable
添加了新的事件处理程序AttributeEvents.modified()
,它与对flag_modified()
方法的调用对应,通常从sqlalchemy.ext.mutable
扩展调用:
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.mutable import MutableDict from sqlalchemy import event Base = declarative_base() class MyDataClass(Base): __tablename__ = "my_data" id = Column(Integer, primary_key=True) data = Column(MutableDict.as_mutable(JSONEncodedDict)) @event.listens_for(MyDataClass.data, "modified") def modified_json(instance): print("json value modified:", instance.data)
上述情况下,当对.data
字典进行原地更改时,事件处理程序将被触发。
#3303 ### 在 Session.refresh 中添加了“for update”参数
为Session.refresh()
方法添加了新参数Session.refresh.with_for_update
。当Query.with_lockmode()
方法被弃用,而是采用Query.with_for_update()
时,Session.refresh()
方法从未更新以反映新选项:
session.refresh(some_object, with_for_update=True)
Session.refresh.with_for_update
参数接受一个选项字典,该字典将作为与Query.with_for_update()
发送的相同参数一样发送的参数:
session.refresh(some_objects, with_for_update={"read": True})
新参数取代了Session.refresh.lockmode
参数。
#3991 ### 原地突变操作符适用于 MutableSet、MutableList
为MutableSet
实现了原地突变操作符__ior__
、__iand__
、__ixor__
和__isub__
,以及MutableList
的__iadd__
。虽然这些方法以前可以成功更新集合,但它们不会正确地触发更改事件。这些操作符像以前一样突变集合,但额外地发出正确的更改事件,以便更改成为下一个刷新过程的一部分:
model = session.query(MyModel).first() model.json_set &= {1, 3}
#3853 ### AssociationProxy 的 any()、has()、contains() 方法与链式关联代理一起工作
AssociationProxy.any()
、AssociationProxy.has()
和AssociationProxy.contains()
比较方法现在支持链接到一个属性,该属性本身也是AssociationProxy
,递归地。下面,A.b_values
是一个关联代理,链接到AtoB.bvalue
,而AtoB.bvalue
本身是一个关联代理,链接到B
:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b_values = association_proxy("atob", "b_value") c_values = association_proxy("atob", "c_value") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) value = Column(String) c = relationship("C") class C(Base): __tablename__ = "c" id = Column(Integer, primary_key=True) b_id = Column(ForeignKey("b.id")) value = Column(String) class AtoB(Base): __tablename__ = "atob" a_id = Column(ForeignKey("a.id"), primary_key=True) b_id = Column(ForeignKey("b.id"), primary_key=True) a = relationship("A", backref="atob") b = relationship("B", backref="atob") b_value = association_proxy("b", "value") c_value = association_proxy("b", "c")
我们可以使用AssociationProxy.contains()
在A.b_values
上进行查询,以跨两个代理A.b_values
、AtoB.b_value
进行查询:
>>> s.query(A).filter(A.b_values.contains("hi")).all() SELECT a.id AS a_id FROM a WHERE EXISTS (SELECT 1 FROM atob WHERE a.id = atob.a_id AND (EXISTS (SELECT 1 FROM b WHERE b.id = atob.b_id AND b.value = :value_1)))
类似地,我们可以使用AssociationProxy.any()
在A.c_values
上进行查询,以跨两个代理A.c_values
、AtoB.c_value
进行查询:
>>> s.query(A).filter(A.c_values.any(value="x")).all() SELECT a.id AS a_id FROM a WHERE EXISTS (SELECT 1 FROM atob WHERE a.id = atob.a_id AND (EXISTS (SELECT 1 FROM b WHERE b.id = atob.b_id AND (EXISTS (SELECT 1 FROM c WHERE b.id = c.b_id AND c.value = :value_1)))))
#3769 ### 身份键增强以支持分片
现在 ORM 使用的身份键结构包含一个额外的成员,以便来自不同上下文的两个相同的主键可以共存于同一身份映射中。
水平分片中的示例已更新以说明这种行为。示例展示了一个分片类WeatherLocation
,引用一个依赖的WeatherReport
对象,其中WeatherReport
类映射到一个存储简单整数主键的表。来自不同数据库的两个WeatherReport
对象可能具有相同的主键值。该示例现在说明了一个新的identity_token
字段跟踪这种差异,以便这两个对象可以共存于同一身份映射中:
tokyo = WeatherLocation("Asia", "Tokyo") newyork = WeatherLocation("North America", "New York") tokyo.reports.append(Report(80.0)) newyork.reports.append(Report(75)) sess = create_session() sess.add_all([tokyo, newyork, quito]) sess.commit() # the Report class uses a simple integer primary key. So across two # databases, a primary key will be repeated. The "identity_token" tracks # in memory that these two identical primary keys are local to different # databases. newyork_report = newyork.reports[0] tokyo_report = tokyo.reports[0] assert inspect(newyork_report).identity_key == (Report, (1,), "north_america") assert inspect(tokyo_report).identity_key == (Report, (1,), "asia") # the token representing the originating shard is also available directly assert inspect(newyork_report).identity_token == "north_america" assert inspect(tokyo_report).identity_token == "asia"
新功能和改进 - 核心
布尔数据类型现在强制使用严格的 True/False/None 值
在 1.1 版本中,描述的更改将非本地布尔整数值强制转换为零/一/None 产生了一个意外的副作用,改变了当Boolean
遇到非整数值(如字符串)时的行为。特别是,先前会生成值False
的字符串值"0"
,现在会产生True
。更糟糕的是,行为的改变只针对某些后端而不是其他后端,这意味着将字符串"0"
值发送给Boolean
的代码在各个后端上会不一致地中断。
这个问题的最终解决方案是不支持字符串值与布尔值,因此在 1.2 版本中,如果传递了非整数/True/False/None 值,将引发严格的TypeError
。此外,只接受整数值 0 和 1。
为了适应希望对布尔值有更自由解释的应用程序,应使用TypeDecorator
。下面演示了一个配方,允许对 1.1 版本之前的Boolean
数据类型进行“自由”行为:
from sqlalchemy import Boolean from sqlalchemy import TypeDecorator class LiberalBoolean(TypeDecorator): impl = Boolean def process_bind_param(self, value, dialect): if value is not None: value = bool(int(value)) return value
#4102 ### 连接池中添加了悲观断开检测
连接池文档长期以来一直提供了一个使用ConnectionEvents.engine_connect()
引擎事件在检出的连接上发出简单语句以测试其活动性的方法。现在,当与适当的方言一起使用时,此配方的功能已添加到连接池本身中。使用新参数create_engine.pool_pre_ping
,每个检出的连接在返回之前都将被测试是否新鲜:
engine = create_engine("mysql+pymysql://", pool_pre_ping=True)
虽然“预先 ping”方法会在连接池检出时增加一点延迟,但对于典型的面向事务的应用程序(包括大多数 ORM 应用程序),这种开销是很小的,并且消除了获取到一个过时连接会引发错误的问题,需要应用程序放弃或重试操作。
该功能不适用于在进行中的事务或 SQL 操作中断开的连接。如果应用程序必须从这些错误中恢复,它需要使用自己的操作重试逻辑来预期这些错误。
另请参阅
断开处理 - 悲观
#3919 ### IN / NOT IN 运算符的空集合行为现在是可配置的;默认表达式简化了
诸如column.in_([])
这样的表达式,假定为 false,现在默认产生表达式1 != 1
,而不是column != column
。这将改变查询结果,比较 SQL 表达式或列与空集合时,产生一个布尔值 false 或 true(对于 NOT IN),而不是 NULL。在这种情况下发出的警告也被移除了。可以使用create_engine.empty_in_strategy
参数来create_engine()
获取旧的行为。
在 SQL 中,IN 和 NOT IN 运算符不支持与明确为空的值集合进行比较;也就是说,这种语法是非法的:
mycolumn IN ()
为了解决这个问题,SQLAlchemy 和其他数据库库检测到这种情况,并渲染一个替代表达式,该表达式评估为 false,或者在 NOT IN 的情况下评估为 true,基于“col IN ()”始终为 false 的理论,因为“空集合”中没有任何内容。通常,为了生成一个跨数据库可移植且在 WHERE 子句上下文中起作用的 false/true 常量,通常使用简单的重言式,如1 != 1
评估为 false,1 = 1
评估为 true(简单的常量“0”或“1”通常不能作为 WHERE 子句的目标)。
SQLAlchemy 在早期也采用了这种方法,但很快有人推测 SQL 表达式column IN ()
如果“column”为 NULL,则不会评估为 false;相反,该表达式会产生 NULL,因为“NULL”表示“未知”,在 SQL 中与 NULL 的比较通常产生 NULL。
为了模拟这个结果,SQLAlchemy 从使用1 != 1
改为使用表达式expr != expr
来处理空的“IN”,并使用expr = expr
来处理空的“NOT IN”;也就是说,我们使用表达式的实际左侧而不是固定值。如果传递的表达式左侧求值为 NULL,则整体比较结果也会得到 NULL 结果,而不是 false 或 true。
不幸的是,用户最终抱怨说这种表达式对一些查询规划器的性能影响非常严重。在那时,当遇到空的 IN 表达式时,会添加警告,建议 SQLAlchemy 继续保持“正确”,并敦促用户避免通常可以安全省略的生成空 IN 谓词的代码。然而,在动态构建查询的情况下,这当然会增加负担,因为输入变量的一组值可能为空。
最近几个月,这个决定的最初假设受到了质疑。表达式“NULL IN ()”应该返回 NULL 的想法只是理论上的,无法测试,因为数据库不支持该语法。然而,事实证明,实际上可以通过模拟空集合来询问关系数据库对于“NULL IN ()”会返回什么值:
SELECT NULL IN (SELECT 1 WHERE 1 != 1)
通过上述测试,我们看到数据库本身无法就答案达成一致。大多数人认为最“正确”的数据库 PostgreSQL 返回 False;因为即使“NULL”代表“未知”,“空集合”意味着没有任何内容,包括所有未知值。另一方面,MySQL 和 MariaDB 对上述表达式返回 NULL,采用更常见的“所有与 NULL 的比较都返回 NULL”的行为。
SQLAlchemy 的 SQL 架构比在做出此设计决定时更复杂,因此现在可以在 SQL 字符串编译时调用任一行为。以前,转换为比较表达式是在构造时完成的,也就是说,在调用ColumnOperators.in_()
或ColumnOperators.notin_()
运算符时。使用编译时行为,可以指示方言本身调用任一方法,即“static”1 != 1
比较或“dynamic”expr != expr
比较。默认已被更改为“static”比较,因为这与 PostgreSQL 在任何情况下的行为一致,这也是绝大多数用户喜欢的。这将改变查询结果,特别是将空表达式与空集进行比较的查询,特别是查询否定where(~null_expr.in_([]))
,因为现在这将评估为 true 而不是 NULL。
现在可以使用标志create_engine.empty_in_strategy
来控制行为,其默认设置为"static"
,但也可以设置为"dynamic"
或"dynamic_warn"
,其中"dynamic_warn"
设置等同于以前发出expr != expr
以及性能警告的行为。然而,预计大多数用户会喜欢"static"
默认设置。
#3907 ### 允许使用缓存语句的延迟扩展 IN 参数集合
添加了一种名为“expanding”的新类型bindparam()
。这用于在语句执行时将元素列表渲染为单独的绑定参数,而不是在语句编译时。这允许将单个绑定参数名称链接到多个元素的 IN 表达式,同时还允许使用查询缓存与 IN 表达式。这一新功能允许相关功能“select in”加载和“polymorphic in”加载利用烘焙查询扩展来减少调用开销:
stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True))) conn.execute(stmt, {"foo": [1, 2, 3]})
该功能在 1.2 系列中应被视为实验性。
#3953 ### 压平比较运算符的运算符优先级
像 IN、LIKE、equals、IS、MATCH 和其他比较运算符的运算符优先级已经被压平到一个级别。当比较运算符组合在一起时,将生成更多的括号,例如:
(column("q") == null()) != (column("y") == null())
现在将生成(q IS NULL) != (y IS NULL)
而不是q IS NULL != y IS NULL
。
#3999 ### 支持在 Table、Column 上的 SQL 注释,包括 DDL、反射
Core 接收了与表和列关联的字符串注释的支持。这些通过Table.comment
和Column.comment
参数指定:
Table( "my_table", metadata, Column("q", Integer, comment="the Q value"), comment="my Q table", )
上面的 DDL 将在表创建时适当地呈现,以将上述注释与模式中的表/列关联起来。当上述表被 autoload 或使用Inspector.get_columns()
检查时,注释将被包含在内。表注释也可以独立使用Inspector.get_table_comment()
方法获得。
当前后端支持包括 MySQL,PostgreSQL 和 Oracle。
#1546 ### 支持多表条件的 DELETE
Delete
构造现在支持多表条件,已在支持的后端实现,目前支持的后端有 PostgreSQL,MySQL 和 Microsoft SQL Server(对目前不工作的 Sybase 方言也添加了支持)。该功能的工作方式与 0.7 和 0.8 系列中首次引入的 UPDATE 的多表条件相同。
给定一个语句如下:
stmt = ( users.delete() .where(users.c.id == addresses.c.id) .where(addresses.c.email_address.startswith("ed%")) ) conn.execute(stmt)
在 PostgreSQL 后端上,上述语句的生成 SQL 将呈现为:
DELETE FROM users USING addresses WHERE users.id = addresses.id AND (addresses.email_address LIKE %(email_address_1)s || '%%')
另请参阅
多表删除
#959 ### 新的“autoescape”选项用于 startswith(),endswith()
“autoescape”参数被添加到ColumnOperators.startswith()
,ColumnOperators.endswith()
,ColumnOperators.contains()
。当设置为True
时,此参数将自动转义所有出现的%
、_
,并使用默认的转义字符,默认为斜杠/
;转义字符本身的出现也会被转义。斜杠用于避免与诸如 PostgreSQL 的standard_confirming_strings
(从 PostgreSQL 9.1 开始默认值已更改)和 MySQL 的NO_BACKSLASH_ESCAPES
设置等设置发生冲突。现在可以使用现有的“escape”参数来更改自动转义字符,如果需要的话。
注意
从 1.2.0b2 的初始实现到 1.2.0,此功能已更改,现在 autoescape 被传递为布尔值,而不是用作转义字符的特定字符。
例如一个表达式:
>>> column("x").startswith("total%score", autoescape=True)
渲染为:
x LIKE :x_1 || '%' ESCAPE '/'
参数“x_1”的值为'total/%score'
。
同样,一个带有反斜杠的表达式:
>>> column("x").startswith("total/score", autoescape=True)
将以相同方式渲染,参数“x_1”的值为'total//score'
。
#2694 ### “float”数据类型的强类型化
一系列更改允许使用Float
数据类型更强烈地将自己与 Python 浮点值联系起来,而不是更通用的Numeric
。这些更改主要与确保 Python 浮点值不会错误地被强制转换为Decimal()
有关,并且在需要时被强制转换为float
,如果应用程序正在处理普通浮点数。
- 传递给 SQL 表达式的普通 Python“float”值现在将被拉入具有类型
Float
的文字参数;以前,类型为Numeric
,默认情况下“asdecimal=True”标志,这意味着结果类型将强制转换为Decimal()
。特别是,这将在 SQLite 上发出令人困惑的警告:
float_value = connection.scalar( select([literal(4.56)]) # the "BindParameter" will now be # Float, not Numeric(asdecimal=True) )
- 在
Numeric
、Float
和Integer
之间的数学运算现在会保留结果表达式的类型,包括asdecimal
标志以及类型是否应该是Float
:
# asdecimal flag is maintained expr = column("a", Integer) * column("b", Numeric(asdecimal=False)) assert expr.type.asdecimal == False # Float subclass of Numeric is maintained expr = column("a", Integer) * column("b", Float()) assert isinstance(expr.type, Float)
- 如果 DBAPI 已知支持本机
Decimal()
模式,则Float
数据类型将无条件地将float()
处理器应用于结果值。一些后端不总是保证浮点数以纯浮点数而不是精确数值(如 MySQL)的形式返回。
支持 GROUPING SETS、CUBE、ROLLUP
所有的 GROUPING SETS、CUBE、ROLLUP 都可以通过func
命名空间访问。在 CUBE 和 ROLLUP 的情况下,这些函数在之前的版本中已经可以使用,但是对于 GROUPING SETS,编译器中添加了一个占位符以便为其腾出空间。现在文档中已经命名了这三个函数:
>>> from sqlalchemy import select, table, column, func, tuple_ >>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q")) >>> stmt = select([func.sum(t.c.value)]).group_by( ... func.grouping_sets( ... tuple_(t.c.x, t.c.y), ... tuple_(t.c.z, t.c.q), ... ) ... ) >>> print(stmt) SELECT sum(t.value) AS sum_1 FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))
用于具有上下文默认生成器的多值插入的参数助手
默认生成函数,例如在上下文敏感默认函数中描述的函数,可以通过DefaultExecutionContext.current_parameters
属性查看与语句相关的当前参数。然而,在通过Insert.values()
方法指定多个 VALUES 子句的Insert
构造中,用户定义的函数会被多次调用,每个参数集一次,但是没有办法知道DefaultExecutionContext.current_parameters
中的哪些键子集适用于该列。添加了一个新函数DefaultExecutionContext.get_current_parameters()
,其中包括一个关键字参数DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups
默认为True
,它执行额外的工作,提供一个DefaultExecutionContext.current_parameters
的子字典,其中的名称被本地化为当前正在处理的 VALUES 子句:
def mydefault(context): return context.get_current_parameters()["counter"] + 12 mytable = Table( "mytable", metadata_obj, Column("counter", Integer), Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault), ) stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}]) conn.execute(stmt)
键行为更改 - ORM
在对象过期之前,after_rollback()
会话事件现在会发出
SessionEvents.after_rollback()
事件现在可以访问对象的属性状态,而不是在它们的状态被过期之前(例如,“快照删除”)。这使得该事件与SessionEvents.after_commit()
事件的行为保持一致,后者也会在“快照”被删除之前发出:
sess = Session() user = sess.query(User).filter_by(name="x").first() @event.listens_for(sess, "after_rollback") def after_rollback(session): # 'user.name' is now present, assuming it was already # loaded. previously this would raise upon trying # to emit a lazy load. print("user name: %s" % user.name) @event.listens_for(sess, "after_commit") def after_commit(session): # 'user.name' is present, assuming it was already # loaded. this is the existing behavior. print("user name: %s" % user.name) if should_rollback: sess.rollback() else: sess.commit()
请注意,Session
仍将禁止在此事件中发出 SQL;这意味着未加载的属性仍然无法在事件范围内加载。
#3934 ### 修复了与 select_from()
结合使用单表继承的问题
当生成 SQL 时,Query.select_from()
方法现在将遵循单表继承列鉴别器;以前,仅查询列列表中的表达式会被考虑进去。
假设 Manager
是 Employee
的子类。像以下这样的查询:
sess.query(Manager.id)
将生成的 SQL 如下:
SELECT employee.id FROM employee WHERE employee.type IN ('manager')
但是,如果仅在列列表中指定了 Manager
,而没有在 Query.select_from()
中指定,那么将不会添加鉴别器:
sess.query(func.count(1)).select_from(Manager)
将生成如下:
SELECT count(1) FROM employee
通过此修复,Query.select_from()
现在可以正确工作,我们可以得到:
SELECT count(1) FROM employee WHERE employee.type IN ('manager')
可能已经通过手动提供 WHERE 子句来解决此问题的应用程序可能需要进行调整。
#3891 ### 替换集合时,先前的集合不再发生变化
当映射的集合成员发生更改时,ORM 会发出事件。在将集合分配给将替换先前集合的属性时,这样做的副作用是被替换的集合也将被改变,这是误导性和不必要的:
>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3") >>> user.addresses = [a1, a2] >>> previous_collection = user.addresses # replace the collection with a new one >>> user.addresses = [a2, a3] >>> previous_collection [Address('a1'), Address('a2')]
在上述更改之前,previous_collection
将已删除 “a1” 成员,对应于不再存在于新集合中的成员。
#3913 ### 在进行批量集合设置之前,@validates 方法接收所有值
在“批量设置”操作期间,使用 @validates
的方法现在将接收到集合的所有成员,然后再对现有集合进行比较。
给定映射如下:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B") @validates("bs") def convert_dict_to_b(self, key, value): return B(data=value["data"]) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) data = Column(String)
在上述情况中,我们可以按照以下方式使用验证器,在集合附加时将传入的字典转换为 B
的实例:
a1 = A() a1.bs.append({"data": "b1"})
但是,集合赋值将失败,因为 ORM 将假定传入的对象已经是 B
的实例,因此在进行集合成员比较之前,它将尝试将它们与现有集合成员进行比较,然后执行实际调用验证器的集合附加操作。这将使得批量设置操作无法适应需要提前修改的非 ORM 对象,如需要提前修改的字典:
a1 = A() a1.bs = [{"data": "b1"}]
新逻辑使用新的 AttributeEvents.bulk_replace()
事件确保所有值在开始时发送到 @validates
函数。
作为此更改的一部分,这意味着验证器现在将在批量设置时接收所有集合成员,而不仅仅是新成员。假设一个简单的验证器如下:
class A(Base): # ... @validates("bs") def validate_b(self, key, value): assert value.data is not None return value
在上述情况下,如果我们从一个集合开始:
a1 = A() b1, b2 = B(data="one"), B(data="two") a1.bs = [b1, b2]
然后,用与第一个重叠的集合替换了该集合:
b3 = B(data="three") a1.bs = [b2, b3]
以前,第二个赋值将仅触发一次 A.validate_b
方法,对于 b3
对象。b2
对象将被视为已经存在于集合中并且不受验证。采用新行为后,b2
和 b3
都会在传递到集合之前传递给 A.validate_b
。因此,验证方法必须采用幂等行为以适应这种情况。
另见
新的 bulk_replace 事件
#3896 ### 使用 flag_dirty() 将对象标记为“脏”,而不改变任何属性
如果使用 flag_modified()
函数标记一个实际未加载的属性为已修改,则现在会引发异常:
a1 = A(data="adf") s.add(a1) s.flush() # expire, similarly as though we said s.commit() s.expire(a1, "data") # will raise InvalidRequestError attributes.flag_modified(a1, "data")
这是因为如果属性在冲刷发生时仍然未出现,则刷新过程很可能无论如何都会失败。要将对象标记为“修改”,而不具体引用任何属性,以便在自定义事件处理程序(如 SessionEvents.before_flush()
)中考虑到刷新过程,请使用新的 flag_dirty()
函数:
from sqlalchemy.orm import attributes attributes.flag_dirty(a1)
#3753 ### 从 scoped_session 中删除“scope”关键字
一个非常古老且未记录的关键字参数 scope
已被删除:
from sqlalchemy.orm import scoped_session Session = scoped_session(sessionmaker()) session = Session(scope=None)
此关键字的目的是尝试允许可变“范围”,其中 None
表示“无范围”,因此将返回一个新的 Session
。此关键字从未被文档化,并且现在如果遇到将会引发 TypeError
。尽管不预期使用此关键字,但如果用户在测试期间报告与此相关的问题,则可以通过弃用来恢复。
#3796 ### 与 onupdate 结合使用的 post_update 的细化
使用 relationship.post_update
功能的关系现在将更好地与设置了 Column.onupdate
值的列进行交互。如果对象插入了列的显式值,则在更新期间重新声明它,以便“onupdate”规则不会覆盖它:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk")) bs = relationship("B", primaryjoin="A.id == B.a_id") favorite_b = relationship( "B", primaryjoin="A.favorite_b_id == B.id", post_update=True ) updated = Column(Integer, onupdate=my_onupdate_function) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id", name="a_fk")) a1 = A() b1 = B() a1.bs.append(b1) a1.favorite_b = b1 a1.updated = 5 s.add(a1) s.flush()
上面,以前的行为是在 INSERT 之后发出 UPDATE,从而触发“onupdate”并覆盖值“5”。现在的 SQL 看起来像这样:
INSERT INTO a (favorite_b_id, updated) VALUES (?, ?) (None, 5) INSERT INTO b (a_id) VALUES (?) (1,) UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ? (1, 5, 1)
此外,如果“updated”的值未设置,那么我们将会正确地在a1.updated
上获取到新生成的值;以前,刷新或使属性过期以允许生成的值出现的逻辑不会对 post-update 触发。在这种情况下,当刷新 flush 内发生时,也会触发InstanceEvents.refresh_flush()
事件。
#3472 ### post_update 与 ORM 版本控制集成
post_update 功能,文档中记录在指向自身的行 / 相互依赖的行,涉及到对特定与关系绑定的外键的更改而发出 UPDATE 语句,除了针对目标行通常会发出的 INSERT/UPDATE/DELETE。这个 UPDATE 语句现在参与版本控制功能,文档记录在配置版本计数器。
鉴于一个映射:
class Node(Base): __tablename__ = "node" id = Column(Integer, primary_key=True) version_id = Column(Integer, default=0) parent_id = Column(ForeignKey("node.id")) favorite_node_id = Column(ForeignKey("node.id")) nodes = relationship("Node", primaryjoin=remote(parent_id) == id) favorite_node = relationship( "Node", primaryjoin=favorite_node_id == remote(id), post_update=True ) __mapper_args__ = {"version_id_col": version_id}
更新将另一个节点关联为“favorite”的节点现在也将增加版本计数器,并匹配当前版本:
node = Node() session.add(node) session.commit() # node is now version #1 node = session.query(Node).get(node.id) node.favorite_node = Node() session.commit() # node is now version #2
注意这意味着一个对象在响应其他属性变化而接收到 UPDATE,并且由于 post_update 关系变化而收到第二个 UPDATE,现在将会为一个 flush 接收到两次版本计数更新。然而,如果对象在当前 flush 内受到 INSERT,版本计数将不会额外增加一次,除非服务器端采用了版本控制方案。
现在讨论 post_update 即使对于 UPDATE 也会发出 UPDATE 的原因在为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?。
另请参阅
指向自身的行 / 相互依赖的行
为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?
关键行为更改 - 核心
自定义运算符的类型行为已经变得一致
可以使用Operators.op()
函数即时制作用户定义的运算符。以前,针对这样的运算符的表达式的类型行为是不一致的,也是不可控的。
而在 1.1 中,以下表达式将产生没有返回类型的结果(假设-%>
是数据库支持的某个特殊运算符):
>>> column("x", types.DateTime).op("-%>")(None).type NullType()
其他类型将使用使用左侧类型作为返回类型的默认行为:
>>> column("x", types.String(50)).op("-%>")(None).type String(length=50)
这些行为大多是偶然发生的,因此行为已经与第二种形式保持一致,即默认返回类型与左侧表达式相同:
>>> column("x", types.DateTime).op("-%>")(None).type DateTime()
由于大多数用户定义的运算符往往是“比较”运算符,通常是由 PostgreSQL 定义的许多特殊运算符之一,Operators.op.is_comparison
标志已经修复,遵循其文档化行为,允许返回类型在所有情况下都是 Boolean
,包括对于 ARRAY
和 JSON
:
>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type Boolean() >>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type Boolean() >>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type Boolean()
为了辅助布尔比较运算符,新增了一个新的简写方法 Operators.bool_op()
。这个方法应该优先用于即时布尔运算符:
>>> print(column("x", types.Integer).bool_op("-%>")(5)) x -%> :x_1 ```### literal_column() 中的百分号现在有条件地转义 `literal_column` 构造现在根据使用的 DBAPI 是否使用了百分号敏感的参数风格(例如‘format’或‘pyformat’)有条件地转义百分号字符。 以前,无法生成一个声明单个百分号的 `literal_column` 构造: ```py >>> from sqlalchemy import literal_column >>> print(literal_column("some%symbol")) some%%symbol
百分号现在不受未设置为使用‘format’或‘pyformat’参数风格的方言的影响;大多数 MySQL 方言等声明了其中一个参数风格的方言将继续适当地转义:
>>> from sqlalchemy import literal_column >>> print(literal_column("some%symbol")) some%symbol >>> from sqlalchemy.dialects import mysql >>> print(literal_column("some%symbol").compile(dialect=mysql.dialect())) some%%symbol
作为这一变化的一部分,使用像 ColumnOperators.contains()
、ColumnOperators.startswith()
和 ColumnOperators.endswith()
这样的运算符时,现在只在适当时才会发生加倍。
#3740 ### 列级别的 COLLATE 关键字现在引用排序规则名称
修复了在collate()
和ColumnOperators.collate()
函数中的一个错误,用于在语句级别提供临时列排序规则,其中区分大小写的名称不会被引用:
stmt = select([mytable.c.x, mytable.c.y]).order_by( mytable.c.somecolumn.collate("fr_FR") )
现在呈现为:
SELECT mytable.x, mytable.y, FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"
以前,区分大小写的名称“fr_FR”不会被引用。目前,手动引用“fr_FR”名称不会被检测到,因此手动引用标识符的应用程序应进行调整。请注意,此更改不影响在类型级别使用排序规则(例如在数据类型上指定的String
在表级别),其中已经应用了引用。
方言改进和更改 - PostgreSQL
支持批处理模式 / 快速执行助手
已确定 psycopg2 的 cursor.executemany()
方法性能较差,特别是在 INSERT 语句中。为了缓解这一问题,psycopg2 添加了快速执行助手,通过将多个 DML 语句批量发送,将语句重新组织为更少的服务器往返次数。SQLAlchemy 1.2 现在包括对这些助手的支持,以便在 Engine
使用 cursor.executemany()
对多个参数集调用语句时,可以透明地使用这些助手。该功能默认关闭,可以通过在 create_engine()
上使用 use_batch_mode
参数来启用:
engine = create_engine( "postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True )
目前该功能被视为��验性质,但可能在将来的版本中默认开启。
另请参阅
Psycopg2 快速执行助手
#4109 ### 支持 INTERVAL 中字段规范的指定,包括完整反射
PostgreSQL 的 INTERVAL 数据类型中的“fields”规范允许指定要存储的间隔的字段,包括诸如“YEAR”、“MONTH”、“YEAR TO MONTH”等值。 INTERVAL
数据类型现在允许指定这些值:
from sqlalchemy.dialects.postgresql import INTERVAL Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))
此外,现在所有 INTERVAL 数据类型都可以独立于“fields”规范进行反射;数据类型本身中的“fields”参数也将存在:
>>> inspect(engine).get_columns("my_table") [{'comment': None, 'name': u'some_interval', 'nullable': True, 'default': None, 'autoincrement': False, 'type': INTERVAL(fields=u'day to second')}]
方言改进和更改 - MySQL
支持 INSERT…ON DUPLICATE KEY UPDATE
MySQL 支持的 INSERT
的 ON DUPLICATE KEY UPDATE
子句现在可以使用 MySQL 特定版本的 Insert
对象来支持,通过 sqlalchemy.dialects.mysql.dml.insert()
。这个 Insert
子类添加了一个新方法 Insert.on_duplicate_key_update()
,实现了 MySQL 的语法:
from sqlalchemy.dialects.mysql import insert insert_stmt = insert(my_table).values(id="some_id", data="some data to insert") on_conflict_stmt = insert_stmt.on_duplicate_key_update( data=insert_stmt.inserted.data, status="U" ) conn.execute(on_conflict_stmt)
以上将呈现为:
INSERT INTO my_table (id, data) VALUES (:id, :data) ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1
另请参阅
INSERT…ON DUPLICATE KEY UPDATE (Upsert)
方言改进和变更 - Oracle
cx_Oracle 方言、类型系统的重大重构
随着 cx_Oracle DBAPI 的 6.x 系列的引入,SQLAlchemy 的 cx_Oracle 方言已经重新设计和简化,以利用 cx_Oracle 的最新改进,并放弃了在 cx_Oracle 的 5.x 系列之前更相关的模式支持。
- 支持的最低 cx_Oracle 版本现在是 5.1.3;推荐使用 5.3 或最新的 6.x 系列。
- 数据类型的处理已经重构。根据 cx_Oracle 的开发人员建议,
cursor.setinputsizes()
方法不再用于除 LOB 类型之外的任何数据类型。因此,参数auto_setinputsizes
和exclude_setinputsizes
已被弃用,也不再起作用。 - 当将
coerce_to_decimal
标志设置为 False 以指示不应发生具有精度和标度的数值类型到Decimal
的强制转换时,仅影响未经类型化的语句(例如,没有TypeEngine
对象的普通字符串)。包含Numeric
类型或子类型的 Core 表达式现在将遵循该类型的十进制强制转换规则。 - “两阶段”事务支持在方言中已经在 cx_Oracle 的 6.x 系列中被删除,现在已完全移除,因为这个功能从未正确工作过,也不太可能被投入生产使用。因此,
allow_twophase
方言标志已被弃用,也不再起作用。 - 修复了涉及带有 RETURNING 的列键的 bug。给定如下语句:
result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))
- 以前,结果中每行的键将是
ret_0
和ret_1
,这是 cx_Oracle RETURNING 实现内部的标识符。现在键将是a
和b
,与其他方言的预期相符。 - cx_Oracle 的 LOB 数据类型将返回值表示为
cx_Oracle.LOB
对象,这是一个与游标关联的代理,通过.read()
方法返回最终数据值。从历史上看,如果在消耗这些 LOB 对象之前读取了更多行(具体来说,读取了比 cursor.arraysize 值更多的行,这会导致读取新批次的行),这些 LOB 对象将引发错误“在后续获取后 LOB 变量不再有效”。SQLAlchemy 通过其类型系统自动调用这些 LOB 的.read()
,以及使用特殊的BufferedColumnResultSet
来解决这个问题,该结果集将确保在使用cursor.fetchmany()
或cursor.fetchall()
这样的调用时,这些数据被缓冲。
方言现在使用 cx_Oracle outputtypehandler 来处理这些.read()
调用,以便无论获取多少行,它们始终被提前调用,因此不再会发生此错误。因此,BufferedColumnResultSet
的使用,以及一些其他特定于此用例的 CoreResultSet
内部部分已被移除。由于类型对象不再需要处理二进制列结果,因此它们也变得更简化。
此外,cx_Oracle 6.x 已删除了发生此错误的任何情况,因此不再可能发生错误。如果在使用极少(如果有的话)使用的auto_convert_lobs=False
选项的情况下,与先前的 5.x 系列 cx_Oracle 结合使用,并且在 LOB 对象可以被消耗之前读取了更多行,则可能会在 SQLAlchemy 中发生此错误。升级到 cx_Oracle 6.x 将���决此问题。### Oracle Unique, Check 约束现在反映出来
UNIQUE 和 CHECK 约束现在通过Inspector.get_unique_constraints()
和 Inspector.get_check_constraints()
反映出来。被反映的Table
对象现在也将包括CheckConstraint
对象。有关此处行为怪癖的信息,请参阅约束反射,包括大多数Table
对象仍然不会包括任何UniqueConstraint
对象,因为这些通常通过Index
表示。
另请参见
约束反射
#4003 ### Oracle 外键约束名称现在是“名称标准化”
在表反射期间传递给 ForeignKeyConstraint
对象的外键约束名称以及在 Inspector.get_foreign_keys()
方法中,现在将被“名称标准化”,即,以小写形式表示以进行大小写不敏感的名称,而不是 Oracle 使用的原始大写格式:
>>> insp.get_indexes("addresses") [{'unique': False, 'column_names': [u'user_id'], 'name': u'address_idx', 'dialect_options': {}}] >>> insp.get_pk_constraint("addresses") {'name': u'pk_cons', 'constrained_columns': [u'id']} >>> insp.get_foreign_keys("addresses") [{'referred_table': u'users', 'referred_columns': [u'id'], 'referred_schema': None, 'name': u'user_id_fk', 'constrained_columns': [u'user_id']}]
以前,外键结果看起来像:
[ { "referred_table": "users", "referred_columns": ["id"], "referred_schema": None, "name": "USER_ID_FK", "constrained_columns": ["user_id"], } ]
上述可能会特别与 Alembic autogenerate 创建问题。
方言改进和更改 - SQL Server
支持带有嵌入点的 SQL Server 模式名称
SQL Server 方言具有这样的行为,即假定具有其中一个点的模式名称是“数据库”。“所有者”标识符对,这在表和组件反射操作以及在呈现模式名称的引号时必须将这两个符号分开时会被分开。现在可以使用括号传递模式参数以手动指定此拆分发生的位置,从而允许数据库和/或所有者名称本身包含一个或多个点:
Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")
上表将考虑“所有者”为 MyDataBase.dbo
,在呈现时也将被引用,并且“数据库”为 None。要单独引用数据库名称和所有者,请使用两对括号:
Table( "some_table", metadata, Column("q", String(50)), schema="[MyDataBase.SomeDB].[MyDB.owner]", )
此外,当传递给 SQL Server 方言的“模式”时,现在将尊重 quoted_name
构造;如果引号标志为 True,则给定的符号不会在点上拆分,并且将被解释为“所有者”。
另请参阅
多部分模式名称
AUTOCOMMIT 隔离级别支持
现在 PyODBC 和 pymssql 方言都支持由 Connection.execution_options()
设置的“AUTOCOMMIT”隔离级别,这将在 DBAPI 连接对象上建立正确的标志。
SqlAlchemy 2.0 中文文档(七十四)(2)https://developer.aliyun.com/article/1562362