SQLAlchemy 1.0 的新特性?

关于本文档

本文档描述了 SQLAlchemy 版本 0.9(截至 2014 年 5 月正在进行维护版本)和 SQLAlchemy 版本 1.0(于 2015 年 4 月发布)之间的更改。

文档最后更新时间:2015 年 6 月 9 日

简介

本指南介绍了 SQLAlchemy 1.0 版本的新特性,并记录了将应用程序从 SQLAlchemy 0.9 系列迁移到 1.0 的用户会受到的影响。

请仔细阅读关于行为变更的章节,了解潜在的向后不兼容的行为变更。

新特性和改进 - ORM

新的 Session 批量 INSERT/UPDATE API

创建了一系列新的 Session 方法,这些方法直接挂钩到工作单元的工具,用于发出 INSERT 和 UPDATE 语句。如果正确使用,这种面向专家的系统可以允许使用 ORM 映射来生成批量插入和更新语句,这些语句被批处理到 executemany 组中,从而使语句能够以与直接使用 Core 相当的速度进行。

另请参阅

批量操作 - 介绍和完整文档

#3100

新的性能示例套件

受为 批量操作 功能以及 FAQ 的 如何分析 SQLAlchemy 驱动的应用程序? 部分所做的基准测试的启发,添加了一个新的示例部分,其中包含多个旨在说明各种 Core 和 ORM 技术的相对性能概况的脚本。这些脚本按用例组织,并打包在单个控制台界面下,以便可以运行任何演示组合,输出计时、Python 分析结果和/或 RunSnake 分析显示。

另请参阅

性能

“Baked” 查询

“baked” 查询功能是一种不寻常的新方法,它允许直接构建和调用 Query 对象,使用缓存,在连续调用时,Python 函数调用开销大大降低(超过 75%)。通过将 Query 对象指定为仅调用一次的 lambda 系列,查询作为预编译单元开始变得可行

from sqlalchemy.ext import baked
from sqlalchemy import bindparam

bakery = baked.bakery()


def search_for_user(session, username, email=None):
    baked_query = bakery(lambda session: session.query(User))
    baked_query += lambda q: q.filter(User.name == bindparam("username"))

    baked_query += lambda q: q.order_by(User.id)

    if email:
        baked_query += lambda q: q.filter(User.email == bindparam("email"))

    result = baked_query(session).params(username=username, email=email).all()

    return result

另请参阅

Baked 查询

#3054

ORM 完整对象获取速度提升 25%

loading.py 模块的机制以及标识映射已进行了多次内联、重构和修剪,因此原始行加载现在填充基于 ORM 的对象的速度提高了约 25%。假设一个 1M 行的表,如下所示的脚本说明了改进最大的加载类型

import time
from sqlalchemy import Integer, Column, create_engine, Table
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Foo(Base):
    __table__ = Table(
        "foo",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("a", Integer(), nullable=False),
        Column("b", Integer(), nullable=False),
        Column("c", Integer(), nullable=False),
    )


engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)

sess = Session(engine)

now = time.time()

# avoid using all() so that we don't have the overhead of building
# a large list of full objects in memory
for obj in sess.query(Foo).yield_per(100).limit(1000000):
    pass

print("Total time: %d" % (time.time() - now))

本地 MacBookPro 结果基准测试从 0.9 的 19 秒降至 1.0 的 14 秒。Query.yield_per() 调用始终是批量处理大量行时的一个好主意,因为它防止 Python 解释器不得不为所有对象及其检测一次分配大量内存。如果没有 Query.yield_per(),上面的脚本在 MacBookPro 上在 0.9 上为 31 秒,在 1.0 上为 26 秒,额外的时间用于设置非常大的内存缓冲区。

新的 KeyedTuple 实现显著加快速度

我们研究了 KeyedTuple 实现,希望改进这样的查询

rows = sess.query(Foo.a, Foo.b, Foo.c).all()

KeyedTuple 类而不是 Python 的 collections.namedtuple() 类,因为后者具有非常复杂的类型创建例程,其基准测试速度比 KeyedTuple 慢得多。但是,当获取数十万行时,collections.namedtuple() 迅速超过 KeyedTuple,随着实例调用的增加,KeyedTuple 变得非常慢。该怎么办?一种介于两者之间的新类型。根据 “size”(返回的行数)和 “num”(不同查询的数量)对所有三种类型进行基准测试,新的 “轻量级键值元组” 要么优于两者,要么略微落后于更快的对象,具体取决于哪种情况。在 “最佳点”,我们在其中创建了大量的新的类型,并获取了大量的行,轻量级对象完全超越了 namedtuple 和 KeyedTuple

-----------------
size=10 num=10000                 # few rows, lots of queries
namedtuple: 3.60302400589         # namedtuple falls over
keyedtuple: 0.255059957504        # KeyedTuple very fast
lw keyed tuple: 0.582715034485    # lw keyed trails right on KeyedTuple
-----------------
size=100 num=1000                 # <--- sweet spot
namedtuple: 0.365247011185
keyedtuple: 0.24896979332
lw keyed tuple: 0.0889317989349   # lw keyed blows both away!
-----------------
size=10000 num=100
namedtuple: 0.572599887848
keyedtuple: 2.54251694679
lw keyed tuple: 0.613876104355
-----------------
size=1000000 num=10               # few queries, lots of rows
namedtuple: 5.79669594765         # namedtuple very fast
keyedtuple: 28.856498003          # KeyedTuple falls over
lw keyed tuple: 6.74346804619     # lw keyed trails right on namedtuple

#3176

结构内存使用方面的重大改进

通过更显着地使用许多内部对象的 __slots__,结构内存使用得到了改进。此优化特别针对具有大量表和列的大型应用程序的基本内存大小,并减少了各种大容量对象的内存大小,包括事件侦听内部结构、比较器对象以及 ORM 属性和加载器策略系统的部分。

一个使用 heapy 测量 Nova 启动大小的基准测试说明,在基本导入 “nova.db.sqlalchemy.models” 中,SQLAlchemy 的对象、关联字典以及 weakrefs 占用的内存减少了约 3.7 兆字节,即 46%。

# reported by heapy, summation of SQLAlchemy objects +
# associated dicts + weakref-related objects with core of Nova imported:

    Before: total count 26477 total bytes 7975712
    After: total count 18181 total bytes 4236456

# reported for the Python module space overall with the
# core of Nova imported:

    Before: Partition of a set of 355558 objects. Total size = 61661760 bytes.
    After: Partition of a set of 346034 objects. Total size = 57808016 bytes.

UPDATE 语句现在在 flush 中与 executemany() 批量处理

UPDATE 语句现在可以在 ORM flush 中批量处理为性能更高的 executemany() 调用,类似于 INSERT 语句的批量处理方式;这将基于以下条件在 flush 中调用

  • 序列中的两个或多个 UPDATE 语句涉及要修改的相同列集。

  • 语句的 SET 子句中没有嵌入式 SQL 表达式。

  • 映射不使用 mapper.version_id_col,或者后端方言支持 executemany() 操作的 “合理” 行数;大多数 DBAPI 现在都正确地支持这一点。

Session.get_bind() 处理更广泛的继承场景

当查询或工作单元 flush 过程尝试查找与特定类对应的数据库引擎时,会调用 Session.get_bind() 方法。该方法已得到改进,可以处理各种面向继承的场景,包括

  • 绑定到 Mixin 或抽象类

    class MyClass(SomeMixin, Base):
        __tablename__ = "my_table"
        # ...
    
    
    session = Session(binds={SomeMixin: some_engine})
  • 基于表单独绑定到继承的具体子类

    class BaseClass(Base):
        __tablename__ = "base"
    
        # ...
    
    
    class ConcreteSubClass(BaseClass):
        __tablename__ = "concrete"
    
        # ...
    
        __mapper_args__ = {"concrete": True}
    
    
    session = Session(binds={base_table: some_engine, concrete_table: some_other_engine})

#3035

Session.get_bind() 将在所有相关的 Query 案例中接收 Mapper

修复了一系列问题,其中 Session.get_bind() 不会接收 Query 的主 Mapper,即使此映射器很容易获得(主映射器是与 Query 对象关联的单个映射器,或者替代地是第一个映射器)。

当传递给 Session.get_bind() 时,Mapper 对象通常由使用 Session.binds 参数将映射器与一系列引擎关联的会话使用(尽管在这种用例中,事情在大多数情况下经常 “工作”,因为绑定将通过映射的表对象定位),或者更具体地实现用户定义的 Session.get_bind() 方法,该方法提供基于映射器选择引擎的某种模式,例如水平分片或将查询路由到不同后端的所谓 “路由” 会话。

这些场景包括

  • Query.count():

    session.query(User).count()
  • Query.update()Query.delete(),用于 UPDATE/DELETE 语句以及 “fetch” 策略使用的 SELECT

    session.query(User).filter(User.id == 15).update(
        {"name": "foob"}, synchronize_session="fetch"
    )
    
    session.query(User).filter(User.id == 15).delete(synchronize_session="fetch")
  • 针对单个列的查询

    session.query(User.id, User.name).all()
  • 针对间接映射(例如 column_property)的 SQL 函数和其他表达式

    class User(Base):
        ...
    
        score = column_property(func.coalesce(self.tables.users.c.name, None))
    
    
    session.query(func.max(User.score)).scalar()

#3227 #3242 #1326

.info 字典改进

InspectionAttr.info 集合现在可用于可以从 Mapper.all_orm_descriptors 集合检索的每种对象。这包括 hybrid_propertyassociation_proxy()。但是,由于这些对象是类绑定的描述符,因此必须从它们附加到的类单独访问它们,以便访问属性。下面使用 Mapper.all_orm_descriptors 命名空间对此进行说明

class SomeObject(Base):
    # ...

    @hybrid_property
    def some_prop(self):
        return self.value + 5


inspect(SomeObject).all_orm_descriptors.some_prop.info["foo"] = "bar"

它也可用作所有 SchemaItem 对象(例如 ForeignKeyUniqueConstraint 等)以及剩余的 ORM 构造(例如 synonym())的构造函数参数。

#2971

#2963

ColumnProperty 构造在别名、order_by 方面效果更好

修复了与 column_property() 相关的各种问题,最具体的是关于 aliased() 构造以及 0.9 中引入的 “按标签排序” 逻辑(请参阅 标签构造现在可以在 ORDER BY 中仅呈现为名称)。

给定如下所示的映射

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(ForeignKey("a.id"))


A.b = column_property(select([func.max(B.id)]).where(B.a_id == A.id).correlate(A))

包含两次 “A.b” 的简单场景将无法正确呈现

print(sess.query(A, a1).order_by(a1.b))

这将按错误的列排序

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1 FROM b
WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_1

新输出

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_2

还有许多场景 “按标签排序” 逻辑无法按标签排序,例如,如果映射是 “多态的”

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {"polymorphic_on": type, "with_polymorphic": "*"}

order_by 将无法使用标签,因为它会因多态加载而匿名化

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY (SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a.id)

现在,按标签排序会跟踪匿名化的标签,现在可以正常工作

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY anon_1

这些修复程序中包含各种 heisenbug,这些 heisenbug 可能会破坏 aliased() 构造的状态,从而导致标签逻辑再次失败;这些问题也已修复。

#3148 #3188

新特性和改进 - Core

Select/Query LIMIT / OFFSET 可以指定为任意 SQL 表达式

Select.limit()Select.offset() 方法现在除了整数值之外,还接受任何 SQL 表达式作为参数。ORM Query 对象还将任何表达式传递给底层的 Select 对象。通常,这用于允许传递绑定参数,该参数稍后可以用值替换

sel = select([table]).limit(bindparam("mylimit")).offset(bindparam("myoffset"))

不支持非整数 LIMIT 或 OFFSET 表达式的方言可能继续不支持此行为;第三方方言也可能需要修改才能利用新行为。当前使用 ._limit._offset 属性的方言对于 limit/offset 指定为简单整数值的情况将继续有效。但是,当指定 SQL 表达式时,这两个属性在访问时将引发 CompileError 异常。希望支持新特性的第三方方言现在应调用 ._limit_clause._offset_clause 属性以接收完整的 SQL 表达式,而不是整数值。

ForeignKeyConstraint 上的 use_alter 标志(通常)不再需要

MetaData.create_all()MetaData.drop_all() 方法现在将使用一个系统,该系统自动为表之间相互依赖的循环中涉及的外键约束呈现 ALTER 语句,而无需指定 ForeignKeyConstraint.use_alter。此外,外键约束不再需要名称才能通过 ALTER 创建;只有 DROP 操作才需要名称。在 DROP 的情况下,该功能将确保只有具有显式名称的约束才实际包含在 ALTER 语句中。如果 DROP 中存在无法解决的循环,则当 DROP 无法继续时,系统现在会发出简洁明了的错误消息。

ForeignKeyConstraint.use_alterForeignKey.use_alter 标志仍然存在,并继续具有相同的效果,即为在 CREATE/DROP 场景中需要 ALTER 的那些约束建立标志。

从 1.0.1 版本开始,在 SQLite 的情况下,当 DROP 期间给定表具有无法解决的循环时,特殊逻辑会接管;在这种情况下,会发出警告,并且表将以顺序的方式删除,这在 SQLite 上通常是可以的,除非启用了约束。为了解决警告并在 SQLite 数据库(特别是启用了约束的数据库)上至少以部分顺序继续操作,请将“use_alter”标志重新应用于那些应显式从排序中省略的 ForeignKeyForeignKeyConstraint 对象。

另请参阅

通过 ALTER 创建/删除外键约束 - 新行为的完整描述。

#3282

ResultProxy “自动关闭”现在是“软”关闭

在许多版本中,ResultProxy 对象总是在所有结果行都被获取时自动关闭。这是为了允许在无需显式调用 ResultProxy.close() 的情况下使用该对象;由于所有 DBAPI 资源都已释放,因此可以安全地丢弃该对象。但是,该对象保持了严格的“关闭”行为,这意味着随后对 ResultProxy.fetchone()ResultProxy.fetchmany()ResultProxy.fetchall() 的任何调用现在都会引发 ResourceClosedError 异常。

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
exception: ResourceClosedError

此行为与 pep-249 的规定不一致,后者指出即使在结果耗尽后,您也可以重复调用 fetch 方法。它还会干扰某些 ResultProxy 实现的行为,例如 cx_oracle 方言用于某些数据类型的 BufferedColumnResultProxy

为了解决这个问题,ResultProxy 的“关闭”状态已分为两个状态;“软关闭”执行“关闭”的大部分操作,因为它释放了 DBAPI 游标,并且在“带结果关闭”对象的情况下,还将释放连接;而“关闭”状态包括“软关闭”所包含的所有内容,并将 fetch 方法建立为“已关闭”。ResultProxy.close() 方法现在永远不会被隐式调用,只有非公开的 ResultProxy._soft_close() 方法会被调用。

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
None  # still None
>>> result.fetchall()
[]
>>> result.close()
>>> result.fetchone()
exception: ResourceClosedError  # *now* it raises

#3330 #3329

CHECK 约束现在支持命名约定中的 %(column_0_name)s 令牌

%(column_0_name)s 将从 CheckConstraint 表达式中找到的第一个列派生。

metadata = MetaData(naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"})

foo = Table("foo", metadata, Column("value", Integer))

CheckConstraint(foo.c.value > 5)

将呈现

CREATE TABLE foo (
    value INTEGER,
    CONSTRAINT ck_foo_value CHECK (value > 5)
)

命名约定与 SchemaType(例如 BooleanEnum)生成的约束的组合现在也将使用所有 CHECK 约束约定。

#3299

引用未附加列的约束可以在其引用的列附加时自动附加到表

自至少 0.8 版本以来,Constraint 具有基于传递表附加列将自身“自动附加”到 Table 的能力。

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint

m = MetaData()

t = Table("t", m, Column("a", Integer), Column("b", Integer))

uq = UniqueConstraint(t.c.a, t.c.b)  # will auto-attach to Table

assert uq in t.constraints

为了帮助解决声明式中经常出现的一些情况,即使 Column 对象尚未与 Table 关联,这种相同的自动附加逻辑现在也可以工作;建立了额外的事件,以便当这些 Column 对象关联时,也会添加 Constraint

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, b)

t = Table("t", m, a, b)

assert uq in t.constraints  # constraint auto-attached

上述功能是 1.0.0b3 版本的后期添加项。1.0.4 版本的修复程序 #3411 确保如果 Constraint 引用了 Column 对象和字符串列名称的混合,则不会发生此逻辑;因为我们尚未跟踪向 Table 添加名称的情况。

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, "b")

t = Table("t", m, a, b)

# constraint *not* auto-attached, as we do not have tracking
# to locate when a name 'b' becomes available on the table
assert uq not in t.constraints

在上面,列 “a” 到表 “t” 的附加事件将在列 “b” 附加之前触发(因为 “a” 在 Table 构造函数中在 “b” 之前声明),如果约束尝试附加,则将无法找到 “b”。为了保持一致性,如果约束引用任何字符串名称,则会跳过 column-attach 时的自动附加逻辑。

当然,如果 Table 在构造 Constraint 时已经包含所有目标 Column 对象,则原始的自动附加逻辑仍然有效。

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)


t = Table("t", m, a, b)

uq = UniqueConstraint(a, "b")

# constraint auto-attached normally as in older versions
assert uq in t.constraints

#3341 #3411

INSERT FROM SELECT 现在包含 Python 和 SQL 表达式默认值

Insert.from_select() 现在包含 Python 和 SQL 表达式默认值(如果未另行指定);非服务器列默认值未包含在 INSERT FROM SELECT 中的限制现已解除,这些表达式将呈现为 SELECT 语句中的常量。

from sqlalchemy import Table, Column, MetaData, Integer, select, func

m = MetaData()

t = Table(
    "t", m, Column("x", Integer), Column("y", Integer, default=func.somefunction())
)

stmt = select([t.c.x])
print(t.insert().from_select(["x"], stmt))

将呈现

INSERT INTO t (x, y) SELECT t.x, somefunction() AS somefunction_1
FROM t

可以使用 Insert.from_select.include_defaults 禁用此功能。

列服务器默认值现在呈现字面值

DefaultClause(由 Column.server_default 设置)作为要编译的 SQL 表达式存在时,“字面值绑定”编译器标志将打开。这允许 SQL 中嵌入的字面值正确呈现,例如

from sqlalchemy import Table, Column, MetaData, Text
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects.postgresql import ARRAY, array
from sqlalchemy.dialects import postgresql

metadata = MetaData()

tbl = Table(
    "derp",
    metadata,
    Column("arr", ARRAY(Text), server_default=array(["foo", "bar", "baz"])),
)

print(CreateTable(tbl).compile(dialect=postgresql.dialect()))

现在呈现

CREATE TABLE derp (
    arr TEXT[] DEFAULT ARRAY['foo', 'bar', 'baz']
)

以前,字面值 "foo"、"bar"、"baz" 将呈现为绑定参数,这在 DDL 中是无用的。

#3087

UniqueConstraint 现在是表反射过程的一部分

使用 autoload=True 填充的 Table 对象现在将包括 UniqueConstraint 构造以及 Index 构造。此逻辑对于 PostgreSQL 和 MySQL 有一些注意事项。

PostgreSQL

PostgreSQL 具有这样的行为:当创建 UNIQUE 约束时,它也会隐式创建与该约束对应的 UNIQUE INDEX。Inspector.get_indexes()Inspector.get_unique_constraints() 方法将继续返回这些不同的条目,其中 Inspector.get_indexes() 现在在索引条目中具有一个令牌 duplicates_constraint,指示检测到时的相应约束。但是,当使用 Table(..., autoload=True) 执行完整的表反射时,Index 构造被检测为链接到 UniqueConstraint,并且存在于 Table.indexes 集合中;只有 UniqueConstraint 将存在于 Table.constraints 集合中。此去重逻辑通过在查询 pg_index 时连接到 pg_constraint 表来查看这两个构造是否链接。

MySQL

MySQL 对于 UNIQUE INDEX 和 UNIQUE 约束没有单独的概念。虽然它在创建表和索引时都支持这两种语法,但它不会以任何不同的方式存储它们。Inspector.get_indexes()Inspector.get_unique_constraints() 方法将继续为 MySQL 中的 UNIQUE 索引返回一个条目,其中 Inspector.get_unique_constraints() 在约束条目中具有一个新令牌 duplicates_index,指示这是一个与该索引对应的重复条目。但是,当使用 Table(..., autoload=True) 执行完整的表反射时,UniqueConstraint 构造在任何情况下都是完全反射的 Table 构造的一部分;此构造始终由 Index 表示,其中 Table.indexes 集合中存在 unique=True 设置。

#3184

安全发出参数化警告的新系统

长期以来,一直存在一个限制,即警告消息不能引用数据元素,这样特定的函数可能会发出无限数量的唯一警告。这种情况发生的主要位置是 Unicode type received non-unicode bind param value 警告。将数据值放在此消息中意味着该模块的 Python __warningregistry__,或者在某些情况下是 Python 全局 warnings.onceregistry,将无限增长,因为在大多数警告情况下,这两个集合之一会填充每个不同的警告消息。

这里的更改是,通过使用一种特殊的 string 类型,该类型专门更改字符串的哈希方式,我们可以控制大量参数化消息仅在少量可能的哈希值上进行哈希处理,这样像 Unicode type received non-unicode bind param value 这样的警告可以定制为仅发出特定次数;超过此次数,Python 警告注册表将开始将它们记录为重复项。

为了说明,以下测试脚本将显示仅为十个参数集发出十个警告,总共 1000 个。

from sqlalchemy import create_engine, Unicode, select, cast
import random
import warnings

e = create_engine("sqlite://")

# Use the "once" filter (which is also the default for Python
# warnings).  Exactly ten of these warnings will
# be emitted; beyond that, the Python warnings registry will accumulate
# new values as dupes of one of the ten existing.
warnings.filterwarnings("once")

for i in range(1000):
    e.execute(
        select([cast(("foo_%d" % random.randint(0, 1000000)).encode("ascii"), Unicode)])
    )

此处的警告格式为

/path/lib/sqlalchemy/sql/sqltypes.py:186: SAWarning: Unicode type received
  non-unicode bind param value 'foo_4852'. (this warning may be
  suppressed after 10 occurrences)

#3178

主要行为变更 - ORM

query.update() 现在将字符串名称解析为映射的属性名称

Query.update() 的文档指出,给定的 values 字典是“以属性名称为键的字典”,这意味着这些是映射的属性名称。不幸的是,该函数的设计更多地考虑了接收属性和 SQL 表达式,而不是字符串;当传递字符串时,这些字符串将直接传递到核心更新语句,而无需解析这些名称在映射类上的表示方式,这意味着名称必须与表列的名称完全匹配,而不是该名称的属性如何映射到类上。

字符串名称现在被认真地解析为属性名称。

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

在上面,列 user_name 被映射为 name。以前,传递字符串的 Query.update() 调用必须按如下方式调用:

session.query(User).update({"user_name": "moonbeam"})

给定的字符串现在针对实体进行解析。

session.query(User).update({"name": "moonbeam"})

通常最好直接使用属性,以避免任何歧义。

session.query(User).update({User.name: "moonbeam"})

此更改还表明,同义词和混合属性也可以通过字符串名称引用。

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

    @hybrid_property
    def fullname(self):
        return self.name


session.query(User).update({"fullname": "moonbeam"})

#3228

当将对象与 None 值与关系进行比较时发出警告

此更改是 1.0.1 版本的新增功能。一些用户正在执行本质上是这种形式的查询:

session.query(Address).filter(Address.user == User(id=None))

SQLAlchemy 当前不支持此模式。对于所有版本,它都会发出类似于以下的 SQL:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE ? = address.user_id
(None,)

请注意上面,有一个比较 WHERE ? = address.user_id,其中绑定值 ? 接收 None 或 SQL 中的 NULL这在 SQL 中始终返回 False。理论上,此处的比较将生成如下 SQL:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE address.user_id IS NULL

但是现在,它不会。依赖于 “NULL = NULL” 在所有情况下都产生 False 这一事实的应用程序面临着有一天 SQLAlchemy 可能会修复此问题以生成 “IS NULL” 的风险,并且查询将产生不同的结果。因此,对于这种操作,您将看到警告:

SAWarning: Got None for value of column user.id; this is unsupported
for a relationship comparison and will not currently produce an
IS comparison (but may in a future release)

请注意,此模式在大多数情况下在 1.0.0 版本(包括所有 beta 版本)中已损坏;将生成类似 SYMBOL('NEVER_SET') 的值。此问题已得到修复,但由于发现了此模式,现在出现了警告,以便我们可以更安全地修复此损坏的行为(现在已在 #3373 中捕获)在未来的版本中。

#3371

“取反包含或等于”关系比较将使用属性的当前值,而不是数据库值

此更改是 1.0.1 版本的新增功能;虽然我们希望将其包含在 1.0.0 版本中,但它只是由于 #3371 才变得显而易见。

给定一个映射:

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(ForeignKey("a.id"))
    a = relationship("A")

给定 A,主键为 7,但我们已将其更改为 10 而未刷新:

s = Session(autoflush=False)
a1 = A(id=7)
s.add(a1)
s.commit()

a1.id = 10

针对以该对象为目标的多对一关系的查询将在绑定参数中使用值 10。

s.query(B).filter(B.a == a1)

产生:

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(10,)

但是,在此更改之前,此条件的反义词不会使用 10,它将使用 7,除非首先刷新对象。

s.query(B).filter(B.a != a1)

产生(在 0.9 及 1.0.1 之前的所有版本中):

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE b.a_id != ? OR b.a_id IS NULL
(7,)

对于瞬态对象,它将产生一个损坏的查询。

SELECT b.id, b.a_id
FROM b
WHERE b.a_id != :a_id_1 OR b.a_id IS NULL
-- {u'a_id_1': symbol('NEVER_SET')}

这种不一致性已得到修复,并且在所有查询中,现在都将使用当前属性值,在本例中为 10

#3374

关于没有预先存在值的属性的属性事件和其他操作的更改

在此更改中,访问对象时 None 的默认返回值现在在每次访问时动态返回,而不是在首次访问时隐式地使用特殊的“set”操作设置属性的状态。此更改的可见结果是 obj.__dict__ 不会在 get 时隐式修改,并且 get_history() 和相关函数的一些次要行为也发生了变化。

给定一个没有状态的对象:

>>> obj = Foo()

如果访问从未设置过的标量或多对一属性,则始终是 SQLAlchemy 的行为,它会作为 None 返回。

>>> obj.someattr
None

实际上,None 的值现在是 obj 状态的一部分,并且与我们显式设置属性(例如 obj.someattr = None)没有什么不同。但是,此处的 “get 时设置” 在历史记录和事件方面表现不同。它不会发出任何属性事件,此外,如果我们查看历史记录,我们会看到:

>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=[None], deleted=())   # 0.9 and below

也就是说,就好像属性始终为 None 并且从未更改过一样。这与我们首先设置属性的情况明显不同:

>>> obj = Foo()
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())  # all versions

以上意味着我们的 “set” 操作的行为可能会因值较早通过 “get” 访问的事实而损坏。在 1.0 中,通过不再在默认 “getter” 被使用时实际设置任何内容来解决此不一致性。

>>> obj = Foo()
>>> obj.someattr
None
>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=(), deleted=())  # 1.0
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())

上述行为没有产生太大影响的原因是,关系数据库中的 INSERT 语句在大多数情况下都认为缺少值与 NULL 相同。SQLAlchemy 是否收到针对设置为 None 的特定属性的 history 事件通常无关紧要;因为发送 None/NULL 或不发送之间的差异不会产生影响。但是,正如 #3060(此处在 关系绑定属性与 FK 绑定属性的属性更改优先级可能看起来会发生变化 中描述)所示,在某些极少数边缘情况下,我们实际上希望明确设置 None。此外,允许此处的属性事件意味着现在可以为 ORM 映射属性创建 “默认值” 函数。

作为此更改的一部分,对于过去发生这种情况的其他情况,隐式 “None” 的生成现在已被禁用;这包括在接收到多对一属性的设置操作时;以前,如果 “old” 值未以其他方式设置,则为 “None”;现在它将发送值 NEVER_SET,这是一个现在可以发送到属性侦听器的值。当调用映射器实用程序函数(例如 Mapper.primary_key_from_instance())时,也可能会收到此符号;如果主键属性根本没有设置,而以前该值将为 None,则现在将是 NEVER_SET 符号,并且不会发生对对象状态的更改。

#3061

关系绑定属性与 FK 绑定属性的属性更改优先级可能看起来会发生变化

作为 #3060 的副作用,将关系绑定属性设置为 None 现在是一个跟踪的历史事件,指的是将 None 持久化到该属性的意图。由于始终是这种情况,即设置关系绑定属性将胜过对外键属性的直接赋值,因此在赋值 None 时可以看到行为的变化。给定一个映射:

class A(Base):
    __tablename__ = "table_a"

    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = "table_b"

    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("table_a.id"))
    a = relationship(A)

在 1.0 中,关系绑定属性在所有情况下都优先于 FK 绑定属性,无论我们分配的值是对 A 对象的引用还是 None。在 0.9 中,行为不一致,仅当赋值时才生效;None 不被考虑。

a1 = A(id=1)
a2 = A(id=2)
session.add_all([a1, a2])
session.flush()

b1 = B()
b1.a = a1  # we expect a_id to be '1'; takes precedence in 0.9 and 1.0

b2 = B()
b2.a = None  # we expect a_id to be None; takes precedence only in 1.0

b1.a_id = 2
b2.a_id = 2

session.add_all([b1, b2])
session.commit()

assert b1.a is a1  # passes in both 0.9 and 1.0
assert b2.a is None  # passes in 1.0, in 0.9 it's a2

#3060

session.expunge() 将完全分离已删除的对象

关于已删除对象,Session.expunge() 的行为存在一个错误,导致行为不一致。object_session() 函数以及 InstanceState.session 属性在 expunge 操作之后仍然会将对象报告为属于 Session

u1 = sess.query(User).first()
sess.delete(u1)

sess.flush()

assert u1 not in sess
assert inspect(u1).session is sess  # this is normal before commit

sess.expunge(u1)

assert u1 not in sess
assert inspect(u1).session is None  # would fail

请注意,当事务在删除操作后仍在进行,并且 Session.expunge() 尚未被调用时,u1 not in sess 为 True 而 inspect(u1).session 仍然指向会话是正常的;完整的脱离通常在事务提交后完成。此问题也会影响依赖 Session.expunge() 的函数,例如 make_transient()

#3139

使用 yield_per 时显式禁止连接/子查询预先加载

为了使 Query.yield_per() 方法更易于使用,如果在使用 yield_per 时有任何子查询预加载器或将使用集合的连接预加载器生效,则会引发异常,因为这些目前与 yield_per 不兼容(子查询加载在理论上可能是兼容的)。当引发此错误时,可以使用星号发送 lazyload() 选项

q = sess.query(Object).options(lazyload("*")).yield_per(100)

或使用 Query.enable_eagerloads()

q = sess.query(Object).enable_eagerloads(False).yield_per(100)

lazyload() 选项的优势在于,仍然可以使用其他多对一的连接加载器选项

q = (
    sess.query(Object)
    .options(lazyload("*"), joinedload("some_manytoone"))
    .yield_per(100)
)

处理重复连接目标的变化和修复

此处的更改涵盖了在某些情况下,当两次连接到同一实体,或在没有使用基于关系的 ON 子句的情况下,针对同一表连接到多个单表实体,以及多次连接到同一目标关系时,会发生意外和不一致行为的错误。

从如下映射开始

from sqlalchemy import Integer, Column, String, ForeignKey
from sqlalchemy.orm import Session, relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

一个查询两次连接到 A.bs

print(s.query(A).join(A.bs).join(A.bs))

将呈现

SELECT a.id AS a_id
FROM a JOIN b ON a.id = b.a_id

查询会删除冗余的 A.bs,因为它试图支持以下情况

s.query(A).join(A.bs).filter(B.foo == "bar").reset_joinpoint().join(A.bs, B.cs).filter(
    C.bar == "bat"
)

也就是说,A.bs 是 “路径” 的一部分。作为 #3367 的一部分,在没有成为更大路径的一部分的情况下两次到达同一端点现在将发出警告

SAWarning: Pathed join target A.bs has already been joined to; skipping

更大的变化涉及在不使用基于关系的路径连接到实体时。如果我们两次连接到 B

print(s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id))

在 0.9 版本中,这将渲染如下

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id

这是有问题的,因为别名是隐式的,并且在 ON 子句不同的情况下可能导致不可预测的结果。

在 1.0 版本中,不应用自动别名,我们得到

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id

这将引发数据库错误。虽然如果我们从冗余关系与冗余的非关系目标连接时,“重复连接目标” 的行为相同可能更好,但目前我们只在先前会发生隐式别名的更严重的情况下更改行为,并且仅在关系情况下发出警告。最终,在没有任何别名来消除歧义的情况下两次连接到同一事物应该在所有情况下都引发错误。

此更改还会影响单表继承目标。使用如下映射

from sqlalchemy import Integer, Column, String, ForeignKey
from sqlalchemy.orm import Session, relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "a"}


class ASub1(A):
    __mapper_args__ = {"polymorphic_identity": "asub1"}


class ASub2(A):
    __mapper_args__ = {"polymorphic_identity": "asub2"}


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)

    a_id = Column(Integer, ForeignKey("a.id"))

    a = relationship("A", primaryjoin="B.a_id == A.id", backref="b")


s = Session()

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, B.a))

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, ASub2.id == B.a_id))

底部的两个查询是等效的,并且都应该渲染相同的 SQL

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a ON b.a_id = a.id AND a.type IN (:type_1)
WHERE a.type IN (:type_2)

上面的 SQL 是无效的,因为它在 FROM 列表中渲染了两次 “a”。但是,隐式别名错误只会发生在第二个查询中,并渲染为以下内容

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1
ON a_1.id = b.a_id AND a_1.type IN (:type_1)
WHERE a_1.type IN (:type_2)

在上面,对 “a” 的第二次连接使用了别名。虽然这看起来很方便,但这并不是单继承查询的通常工作方式,并且具有误导性和不一致性。

最终结果是,依赖此错误的应用程序现在将收到数据库引发的错误。解决方案是使用预期的形式。当在查询中引用单继承实体的多个子类时,您必须手动使用别名来消除表的歧义,因为所有子类通常都指向同一张表

asub2_alias = aliased(ASub2)

print(s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias)))

#3233 #3367

延迟列不再隐式取消延迟

标记为延迟且没有显式取消延迟的映射属性现在将保持 “延迟” 状态,即使它们的列以某种方式出现在结果集中。这是一个性能增强,因为 ORM 加载不再花费时间在获得结果集时搜索每个延迟列。但是,对于一直依赖此行为的应用程序,现在应该使用显式的 undefer() 或类似选项,以防止在访问属性时发出 SELECT。

已删除弃用的 ORM 事件钩子

以下 ORM 事件钩子已被删除,其中一些自 0.5 版本起已被弃用:translate_rowpopulate_instanceappend_resultcreate_instance。这些钩子的用例起源于 SQLAlchemy 非常早期的 0.1 / 0.2 系列,并且早已变得不必要。特别是,这些钩子在很大程度上是不可用的,因为这些事件中的行为约定与周围的内部机制紧密相关,例如实例需要如何创建和初始化,以及如何在 ORM 生成的行中定位列。删除这些钩子极大地简化了 ORM 对象加载的机制。

当使用自定义行加载器时,新 Bundle 功能的 API 更改

0.9 版本的新 Bundle 对象在 API 中有一个小的更改,即当在自定义类上重写 create_row_processor() 方法时。以前,示例代码如下所示

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row, result):
            return dict(zip(labels, (proc(row, result) for proc in procs)))

        return proc

现在删除了未使用的 result 成员

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row):
            return dict(zip(labels, (proc(row) for proc in procs)))

        return proc

右内连接嵌套现在是 joinedload 中 innerjoin=True 的默认设置

joinedload.innerjoin 以及 relationship.innerjoin 的行为现在是使用 “嵌套” 内连接,即右嵌套,作为当内连接预先加载链接到外连接预先加载时的默认行为。为了获得当外连接存在时将所有连接的预先加载链接为外连接的旧行为,请使用 innerjoin="unnested"

正如版本 0.9 的 在连接的预先加载中提供右嵌套内连接 中引入的那样,innerjoin="nested" 的行为是,链接到外连接预先加载的内连接预先加载将使用右嵌套连接。"nested" 现在在使用 innerjoin=True 时是隐含的

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True)
)

使用新的默认设置,这将以如下形式渲染 FROM 子句

FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>

也就是说,为 INNER 连接使用右嵌套连接,以便可以返回 users 的完整结果。使用 INNER 连接比使用 OUTER 连接更有效,并允许 joinedload.innerjoin 优化参数在所有情况下都生效。

要获得旧的行为,请使用 innerjoin="unnested"

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin="unnested")
)

这将避免右嵌套连接,并在所有连接中使用 OUTER 连接将连接链接在一起,尽管有 innerjoin 指令

FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>

正如 0.9 注释中指出的那样,唯一难以处理右嵌套连接的数据库后端是 SQLite;从 0.9 版本开始,SQLAlchemy 将右嵌套连接转换为子查询,作为 SQLite 上的连接目标。

另请参阅

在连接的预先加载中提供右嵌套内连接 - 版本 0.9.4 中引入的功能描述。

#3008

子查询不再应用于 uselist=False 连接的预先加载

给定如下连接的预先加载

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", uselist=False)


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))


s = Session()
print(s.query(A).options(joinedload(A.b)).limit(5))

SQLAlchemy 认为关系 A.b 是 “一对多,作为单个值加载”,这本质上是 “一对一” 关系。但是,连接的预先加载始终将上述情况视为需要将主查询置于子查询中的情况,这通常对于应用了 LIMIT 的主查询中的 B 对象集合是必需的

SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM (SELECT a.id AS a_id
FROM a LIMIT :param_1) AS anon_1
LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id

但是,由于内部查询与外部查询之间的关系是,在 uselist=False 的情况下,最多只共享一行(与多对一相同),因此在这种情况下,在使用 LIMIT + 连接的预先加载的 “子查询” 现在被删除

SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
LIMIT :param_1

在 LEFT OUTER JOIN 返回多于一行的情况下,ORM 始终在此处发出警告,并忽略 uselist=False 的其他结果,因此该错误情况下的结果不应更改。

#3249

如果 query.update() / query.delete() 与 join(), select_from(), from_self() 一起使用,则会引发异常

当针对也调用了 Query.join()Query.outerjoin()Query.select_from()Query.from_self() 的查询调用 Query.update()Query.delete() 方法时,SQLAlchemy 0.9.10(截至 2015 年 6 月 9 日尚未发布)会发出警告。这些是不受支持的用例,在 0.9 系列中一直到 0.9.10 版本都会静默失败,并在该版本中发出警告。在 1.0 版本中,这些情况会引发异常。

#3349

在多表更新时,使用 `synchronize_session='evaluate'` 的 query.update() 会引发异常

Query.update() 的 “评估器” 不适用于多表更新,当存在多个表时,需要将其设置为 synchronize_session=Falsesynchronize_session='fetch'。新行为是现在会引发显式异常,并显示更改同步设置的消息。这是从 0.9.7 版本开始发出的警告升级而来的。

#3117

Resurrect 事件已被删除

“resurrect” ORM 事件已完全删除。自从 0.8 版本从工作单元中删除了旧的 “mutable” 系统以来,此事件就已不再具有任何功能。

当使用 from_self(), count() 时,单表继承条件的变化

给定单表继承映射,例如

class Widget(Base):
    __table__ = "widget_table"


class FooWidget(Widget):
    pass

对子类使用 Query.from_self()Query.count() 将生成子查询,但随后将子类型的 “WHERE” 条件添加到外部

sess.query(FooWidget).from_self().all()

渲染

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets) AS anon_1
WHERE anon_1.widgets_type IN (?)

这个问题是,如果内部查询未指定所有列,那么我们就无法在外部添加 WHERE 子句(它实际上会尝试这样做,并生成错误的查询)。这个决定显然可以追溯到 0.6.5 版本,并带有注释 “可能需要对此进行更多调整”。好吧,这些调整已经到来!所以现在上面的查询将渲染为

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

以便不包含 “type” 的查询仍然可以工作!

sess.query(FooWidget.id).count()

渲染

SELECT count(*) AS count_1
FROM (SELECT widgets.id AS widgets_id
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

#3177

单表继承条件无条件地添加到所有 ON 子句

当连接到单表继承子类目标时,ORM 始终在关系上连接时添加 “单表条件”。给定如下映射

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    related_id = Column(ForeignKey("related.id"))
    related = relationship("Related", backref="widget")
    __mapper_args__ = {"polymorphic_on": type}


class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}


class Related(Base):
    __tablename__ = "related"
    id = Column(Integer, primary_key=True)

在关系上进行 JOIN 时,渲染类型 “单继承” 子句一直是相当长一段时间的行为

s.query(Related).join(FooWidget, Related.widget).all()

SQL 输出

SELECT related.id AS related_id
FROM related JOIN widget ON related.id = widget.related_id AND widget.type IN (:type_1)

在上面,因为我们连接到子类 FooWidgetQuery.join() 知道将 AND widget.type IN ('foo') 条件添加到 ON 子句。

这里的更改是 AND widget.type IN() 条件现在附加到任何 ON 子句,而不仅仅是那些从关系生成的子句,包括显式声明的子句

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget, FooWidget.related_id == Related.id).all()

以及当未声明任何类型的 ON 子句时的 “隐式” 连接

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget).all()

以前,这些子句的 ON 子句将不包括单继承条件。已经添加此条件以解决此问题的应用程序将希望删除其显式使用,尽管如果条件恰好在中间渲染两次,它应该继续工作良好。

#3222

关键行为变化 - Core

当强制将完整 SQL 片段转换为 text() 时发出警告

自 SQLAlchemy 诞生以来,一直强调不要妨碍纯文本的使用。Core 和 ORM 表达式系统旨在允许用户在许多点上仅使用纯文本 SQL 表达式,不仅是在您可以将完整的 SQL 字符串发送到 Connection.execute() 的意义上,而且在您可以将带有 SQL 表达式的字符串发送到许多函数中,例如 Select.where()Query.filter()Select.order_by()

请注意,通过 “SQL 表达式”,我们指的是 SQL 字符串的完整片段,例如

# the argument sent to where() is a full SQL expression
stmt = select([sometable]).where("somecolumn = 'value'")

我们 不是在谈论字符串参数,即传递成为参数化的字符串值的正常行为

# This is a normal Core expression with a string argument -
# we aren't talking about this!!
stmt = select([sometable]).where(sometable.c.somecolumn == "value")

Core 教程长期以来一直以使用此技术的示例为特色,使用 select() 构造,其中几乎所有组件都指定为直白的字符串。然而,尽管存在这种长期存在的行为和示例,但用户显然对这种行为的存在感到惊讶,并且在向社区询问时,我无法找到任何实际上惊讶于您可以将完整字符串发送到像 Query.filter() 这样的方法的用户。

因此,这里的更改是为了鼓励用户在编写部分或完全由文本片段组成的 SQL 时,对文本字符串进行限定。当如下编写 select 时

stmt = select(["a", "b"]).where("a = b").select_from("sometable")

语句正常构建,具有与以前相同的强制转换。但是,您将看到以下警告发出

SAWarning: Textual column expression 'a' should be explicitly declared
with text('a'), or use column('a') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual column expression 'b' should be explicitly declared
with text('b'), or use column('b') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL expression 'a = b' should be explicitly declared
as text('a = b') (this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL FROM expression 'sometable' should be explicitly
declared as text('sometable'), or use table('sometable') for more
specificity (this warning may be suppressed after 10 occurrences)

这些警告试图通过显示参数以及接收字符串的位置来准确显示问题所在。这些警告利用了 Session.get_bind() 处理更广泛的继承场景,以便可以安全地发出参数化警告而不会耗尽内存,并且与往常一样,如果希望警告成为异常,则应使用 Python 警告过滤器

import warnings

warnings.simplefilter("error")  # all warnings raise an exception

鉴于上述警告,我们的语句工作正常,但为了消除警告,我们将按如下方式重写我们的语句

from sqlalchemy import select, text

stmt = (
    select([text("a"), text("b")]).where(text("a = b")).select_from(text("sometable"))
)

正如警告建议的那样,如果我们使用 column()table(),我们可以为我们的语句提供更多关于文本的特异性

from sqlalchemy import select, text, column, table

stmt = (
    select([column("a"), column("b")])
    .where(text("a = b"))
    .select_from(table("sometable"))
)

其中还要注意,table()column() 现在可以从 “sqlalchemy” 导入,而无需 “sql” 部分。

此处的行为适用于 select() 以及 Query 上的关键方法,包括 Query.filter()Query.from_statement()Query.having()

ORDER BY 和 GROUP BY 是特殊情况

有一种情况下,字符串的使用具有特殊含义,并且作为此更改的一部分,我们增强了其功能。当我们有一个 select()Query 引用某些列名或命名标签时,我们可能希望按已知的列或标签进行 GROUP BY 和/或 ORDER BY

stmt = (
    select([user.c.name, func.count(user.c.id).label("id_count")])
    .group_by("name")
    .order_by("id_count")
)

在上面的语句中,我们期望看到 “ORDER BY id_count”,而不是函数的重新声明。给定的字符串参数在编译期间会主动与 columns 子句中的条目匹配,因此上面的语句将按我们的预期生成,而不会发出警告(但请注意,"name" 表达式已解析为 users.name!)

SELECT users.name, count(users.id) AS id_count
FROM users GROUP BY users.name ORDER BY id_count

但是,如果我们引用无法定位的名称,那么我们再次收到警告,如下所示

stmt = select([user.c.name, func.count(user.c.id).label("id_count")]).order_by(
    "some_label"
)

输出执行了我们所说的操作,但再次警告我们

SAWarning: Can't resolve label reference 'some_label'; converting to
text() (this warning may be suppressed after 10 occurrences)
SELECT users.name, count(users.id) AS id_count
FROM users ORDER BY some_label

上述行为适用于我们可能想要引用所谓的 “标签引用” 的所有位置;ORDER BY 和 GROUP BY,也适用于 OVER 子句以及引用列的 DISTINCT ON 子句(例如 PostgreSQL 语法)。

我们仍然可以使用 text() 为 ORDER BY 或其他指定任何任意表达式

stmt = select([users]).order_by(text("some special expression"))

整个更改的结果是,SQLAlchemy 现在希望我们在发送字符串时告诉它,此字符串显式是 text() 构造,或列、表等,并且如果我们在 order by、group by 或其他表达式中将其用作标签名称,SQLAlchemy 期望该字符串解析为已知的内容,否则应再次使用 text() 或类似的进行限定。

#2992

当使用多值插入时,为每行单独调用 Python 端默认值

当使用 Insert.values() 的多值版本时,Python 端列默认值的支持基本上未实现,并且仅在特定情况下 “意外地” 工作,即当使用的方言使用非位置(例如命名)样式的绑定参数时,以及当不需要为每行调用 Python 端可调用对象时。

该功能已彻底修改,使其工作方式更类似于 “executemany” 样式的调用

import itertools

counter = itertools.count(1)
t = Table(
    "my_table",
    metadata,
    Column("id", Integer, default=lambda: next(counter)),
    Column("data", String),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {"data": "d3"},
        ]
    )
)

上面的示例将为每一行单独调用 next(counter),正如预期的那样

INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)
(1, 'd1', 2, 'd2', 3, 'd3')

以前,位置方言会失败,因为不会为其他位置生成绑定

Incorrect number of bindings supplied. The current statement uses 6,
and there are 4 supplied.
[SQL: u'INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)']
[parameters: (1, 'd1', 'd2', 'd3')]

对于 “命名” 方言,将在每一行中重复使用 “id” 的相同值(因此此更改与依赖此系统的系统向后不兼容)

INSERT INTO my_table (id, data) VALUES (:id, :data_0), (:id, :data_1), (:id, :data_2)
-- {u'data_2': 'd3', u'data_1': 'd2', u'data_0': 'd1', 'id': 1}

该系统还将拒绝调用 “服务器端” 默认值作为内联渲染的 SQL,因为它无法保证服务器端默认值与此兼容。如果 VALUES 子句为特定列渲染,则需要 Python 端值;如果省略的值仅引用服务器端默认值,则会引发异常

t = Table(
    "my_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("data", String, server_default="some default"),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {},
        ]
    )
)

将引发

sqlalchemy.exc.CompileError: INSERT value for column my_table.data is
explicitly rendered as a boundparameter in the VALUES clause; a
Python-side value or SQL expression is required

以前,值 “d1” 将被复制到第三行的值中(但同样,仅适用于命名格式!)

INSERT INTO my_table (data) VALUES (:data_0), (:data_1), (:data_0)
-- {u'data_1': 'd2', u'data_0': 'd1'}

#3288

事件监听器不能在事件运行器内部添加或删除

从同一事件内部删除事件监听器会修改迭代期间的列表元素,这将导致仍然附加的事件监听器静默失败而无法触发。为了防止这种情况,同时保持性能,列表已替换为 collections.deque(),它不允许在迭代期间进行任何添加或删除,而是引发 RuntimeError

#3163

INSERT…FROM SELECT 构造现在意味着 `inline=True`

使用 Insert.from_select() 现在意味着 insert() 上的 inline=True。这有助于修复一个错误,即 INSERT…FROM SELECT 构造会无意中在支持的后端编译为 “隐式返回”,这将导致在插入零行的情况下(因为隐式返回期望一行)以及在插入多行的情况下(例如,仅限多行中的第一行)导致中断。类似的更改也应用于具有多个参数集的 INSERT..VALUES;隐式 RETURNING 也不会为此语句发出。由于这两个构造都处理可变行数,因此 ResultProxy.inserted_primary_key 访问器不适用。以前,有一个文档注释,指出人们可能更喜欢使用 inline=True 和 INSERT..FROM SELECT,因为某些数据库不支持返回,因此无法进行 “隐式” 返回,但是在任何情况下,INSERT…FROM SELECT 都不需要隐式返回。如果需要插入的数据,则应使用常规的显式 Insert.returning() 来返回可变数量的结果行。

#3169

`autoload_with` 现在意味着 `autoload=True`

可以通过单独传递 Table.autoload_with 来设置 Table 以进行反射

my_table = Table("my_table", metadata, autoload_with=some_engine)

#3027

DBAPI 异常包装和 handle_error() 事件改进

Connection 对象失效,然后尝试重新连接并遇到错误的情况下,SQLAlchemy 的 DBAPI 异常包装没有发生;此问题已得到解决。

此外,最近添加的 ConnectionEvents.handle_error() 事件现在会在以下情况下被调用:初始连接时、重新连接时,以及当通过 create_engine.creator 使用自定义连接函数调用 create_engine() 时发生的错误。

ExceptionContext 对象新增了一个数据成员 ExceptionContext.engine,在 Connection 对象不可用时(例如,在初始连接时),它将始终指向正在使用的 Engine

#3266

ForeignKeyConstraint.columns 现在是一个 ColumnCollection

ForeignKeyConstraint.columns 之前是一个普通列表,其中包含字符串或 Column 对象,具体取决于 ForeignKeyConstraint 的构造方式以及是否与表关联。现在,该集合是一个 ColumnCollection,并且仅在 ForeignKeyConstraintTable 关联后才初始化。添加了一个新的访问器 ForeignKeyConstraint.column_keys,用于无条件地返回本地列集合的字符串键,而不管对象的构造方式或当前状态如何。

MetaData.sorted_tables 访问器是“确定性的”

MetaData.sorted_tables 访问器产生的表排序是“确定性的”;在所有情况下,排序都应相同,而与 Python 哈希无关。这是通过首先按名称对表进行排序,然后再将它们传递给拓扑算法来完成的,该算法在迭代时保持该顺序。

请注意,此更改尚**不**适用于在发出 MetaData.create_all()MetaData.drop_all() 时应用的排序。

#3084

null()、false() 和 true() 常量不再是单例

这三个常量在 0.9 版本中被更改为返回“单例”值;不幸的是,这将导致如下查询无法按预期呈现

select([null(), null()])

仅呈现 SELECT NULL AS anon_1,因为两个 null() 构造将变为相同的 NULL 对象,而 SQLAlchemy 的 Core 模型基于对象标识来确定词法意义。0.9 中的更改除了希望节省对象开销外,没有其他重要意义;通常,未命名的构造需要保持词法上的唯一性,以便对其进行唯一标记。

#3170

SQLite/Oracle 具有用于临时表/视图名称报告的不同方法

在 SQLite/Oracle 的情况下,Inspector.get_table_names()Inspector.get_view_names() 方法也会返回临时表和视图的名称,这在任何其他方言中都不提供(至少在 MySQL 的情况下甚至是不可能的)。此逻辑已移至两个新方法 Inspector.get_temp_table_names()Inspector.get_temp_view_names()

请注意,对于大多数(如果不是全部)方言,通过 Table('name', autoload=True) 或通过诸如 Inspector.get_columns() 之类的方法来反射特定的命名临时表或临时视图的功能仍然存在。特别是对于 SQLite,还有一个针对从临时表反射 UNIQUE 约束的错误修复,即 #3203

#3204

方言改进和更改 - PostgreSQL

ENUM 类型创建/删除规则的全面修订

PostgreSQL ENUM 的规则在 TYPE 的创建和删除方面已变得更加严格。

一个**未**显式关联到 MetaData 对象的 ENUM 将根据 Table.create()Table.drop() 进行创建*和*删除

table = Table(
    "sometable", metadata, Column("some_enum", ENUM("a", "b", "c", name="myenum"))
)

table.create(engine)  # will emit CREATE TYPE and CREATE TABLE
table.drop(engine)  # will emit DROP TABLE and DROP TYPE - new for 1.0

这意味着如果第二个表也具有名为 ‘myenum’ 的枚举,则上述 DROP 操作现在将失败。为了适应通用共享枚举类型的使用情况,已增强了元数据关联枚举的行为。

一个**已**显式关联到 MetaData 对象的 ENUM 将*不会*根据 Table.create()Table.drop() 进行创建*或*删除,除非使用 checkfirst=True 标志调用 Table.create()

my_enum = ENUM("a", "b", "c", name="myenum", metadata=metadata)

table = Table("sometable", metadata, Column("some_enum", my_enum))

# will fail: ENUM 'my_enum' does not exist
table.create(engine)

# will check for enum and emit CREATE TYPE
table.create(engine, checkfirst=True)

table.drop(engine)  # will emit DROP TABLE, *not* DROP TYPE

metadata.drop_all(engine)  # will emit DROP TYPE

metadata.create_all(engine)  # will emit CREATE TYPE

#3319

新的 PostgreSQL 表选项

在通过 Table 构造呈现 DDL 时,添加了对 PG 表选项 TABLESPACE、ON COMMIT、WITH(OUT) OIDS 和 INHERITS 的支持。

另请参阅

PostgreSQL 表选项

#2051

PostgreSQL 方言的新 get_enums() 方法

在 PostgreSQL 的情况下,inspect() 方法返回一个 PGInspector 对象,其中包括一个新的 PGInspector.get_enums() 方法,该方法返回有关所有可用 ENUM 类型的信息

from sqlalchemy import inspect, create_engine

engine = create_engine("postgresql+psycopg2://host/dbname")
insp = inspect(engine)
print(insp.get_enums())

PostgreSQL 方言反映了物化视图、外部表

更改如下

反射的更改涉及将 'm''f' 添加到我们查询 pg_class.relkind 时使用的限定符列表中,但此更改在 1.0.0 中是新的,以避免为在生产环境中运行 0.9 的用户带来任何向后不兼容的意外。

#2891

PostgreSQL 的 has_table() 现在适用于临时表

这是一个简单的修复,使临时表的“has table”现在可以工作,以便可以继续执行如下代码

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    # checkfirst will succeed
    user_tmp.create(conn, checkfirst=True)

这种情况非常不可能导致一个非失败的应用程序表现不同,因为 PostgreSQL 允许非临时表静默覆盖临时表。因此,如下代码现在的行为将完全不同,不再在临时表之后创建真实表

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    m2 = MetaData()
    user = Table(
        "user_tmp",
        m2,
        Column("id", INT, primary_key=True),
        Column("name", VARCHAR(50)),
    )

    # in 0.9, *will create* the new table, overwriting the old one.
    # in 1.0, *will not create* the new table
    user.create(conn, checkfirst=True)

#3264

PostgreSQL FILTER 关键字

从 9.4 版本开始,PostgreSQL 现在支持聚合函数的 SQL 标准 FILTER 关键字。SQLAlchemy 允许使用 FunctionElement.filter() 来实现这一点

func.count(1).filter(True)

PG8000 方言支持客户端编码

pg8000 方言现在支持 create_engine.encoding 参数,它使用连接处理程序发出与所选编码匹配的 SET CLIENT_ENCODING

PG8000 原生 JSONB 支持

添加了对 1.10.1 以上版本的 PG8000 的支持,其中原生支持 JSONB。

在 PyPy 上支持 psycopg2cffi 方言

添加了对 pypy psycopg2cffi 方言的支持。

方言改进和更改 - MySQL

MySQL TIMESTAMP 类型现在在所有情况下都呈现 NULL / NOT NULL

如果 TIMESTAMP 列设置为 nullable=True,则 MySQL 方言始终通过为此类类型发出 NULL 来解决 MySQL 与 TIMESTAMP 列关联的隐式 NOT NULL 默认值。但是,MySQL 5.6.6 及更高版本具有一个新的标志 explicit_defaults_for_timestamp,它修复了 MySQL 的非标准行为,使其行为类似于任何其他类型;为了适应这一点,SQLAlchemy 现在为所有 TIMESTAMP 列无条件地发出 NULL/NOT NULL。

另请参阅

TIMESTAMP 列和 NULL

#3155

MySQL SET 类型经过全面修订,以支持空集、Unicode、空白值处理

SET 类型在历史上不包含单独处理空白集和空值的系统;由于不同的驱动程序对于空字符串和空字符串集表示的处理方式不同,因此 SET 类型仅尝试在这些行为之间进行对冲,选择将空集视为 set(['']),这仍然是 MySQL-Connector-Python DBAPI 的当前行为。此处的某些理由是,否则实际上不可能在 MySQL SET 中存储空白字符串,因为驱动程序返回给我们字符串,但无法区分 set([''])set()。这留给用户来确定 set(['']) 实际上是否意味着“空集”。

新行为将空白字符串的用例(这是一个不寻常的用例,甚至在 MySQL 的文档中都没有记录)移至特殊情况,而 SET 的默认行为现在是

  • 将 MySQL-python 返回的空字符串 '' 转换为空集 set()

  • 将 MySQL-Connector-Python 返回的单空白值集 set(['']) 转换为空集 set()

  • 为了处理希望在其可能值列表中实际包含空白值 '' 的集合类型的情况,实现了一个新功能(在这种用例中是必需的),其中集合值作为按位整数值持久化和加载;添加了标志 SET.retrieve_as_bitwise 以启用此功能。

使用 SET.retrieve_as_bitwise 标志允许持久化和检索集合,而不会产生值歧义。从理论上讲,只要给定类型的值列表与数据库中声明的顺序完全匹配,就可以在所有情况下都启用此标志;它只会使 SQL 回显输出看起来有些不寻常。

SET 的默认行为在其他方面保持不变,使用字符串循环值。基于字符串的行为现在完全支持 Unicode,包括使用 use_unicode=0 的 MySQL-python。

#3283

MySQL 内部 “no such table” 异常不会传递给事件处理程序

MySQL 方言现在将禁用 ConnectionEvents.handle_error() 事件,以防止其用于内部检测表是否存在的语句触发。这是通过使用执行选项 skip_user_error_events 来实现的,该选项在执行范围内禁用错误处理事件。这样,重写异常的用户代码无需担心 MySQL 方言或其他偶尔需要捕获 SQLAlchemy 特定异常的方言。

更改了 MySQL-Connector 的 raise_on_warnings 的默认值

将 MySQL-Connector 的 “raise_on_warnings” 的默认值更改为 False。由于某些原因,它被设置为 True。“buffered” 标志不幸地必须保持为 True,因为除非完全获取所有结果,否则 MySQLconnector 不允许关闭游标。

#2515

MySQL 布尔符号 “true”、“false” 再次起作用

0.9 对 IS/IS NOT 运算符以及 #2682 中的布尔类型的全面修订,不允许 MySQL 方言在 “IS” / “IS NOT” 的上下文中利用 “true” 和 “false” 符号。显然,即使 MySQL 没有 “boolean” 类型,当使用特殊的 “true” 和 “false” 符号时,它也支持 IS / IS NOT,即使这些符号在其他方面与 “1” 和 “0” 同义(并且 IS/IS NOT 不适用于数值)。

因此,这里的更改是 MySQL 方言仍然是 “非原生布尔型”,但 true()false() 符号再次生成关键字 “true” 和 “false”,因此诸如 column.is_(true()) 之类的表达式再次在 MySQL 上起作用。

#3186

match() 运算符现在返回与 MySQL 的浮点返回值兼容的不可知 MatchType

ColumnOperators.match() 表达式的返回类型现在是一个名为 MatchType 的新类型。这是 Boolean 的子类,可以被方言拦截,以便在 SQL 执行时生成不同的结果类型。

如下代码现在可以正确运行,并在 MySQL 上返回浮点数

>>> connection.execute(
...     select(
...         [
...             matchtable.c.title.match("Agile Ruby Programming").label("ruby"),
...             matchtable.c.title.match("Dive Python").label("python"),
...             matchtable.c.title,
...         ]
...     ).order_by(matchtable.c.id)
... )
[
    (2.0, 0.0, 'Agile Web Development with Ruby On Rails'),
    (0.0, 2.0, 'Dive Into Python'),
    (2.0, 0.0, "Programming Matz's Ruby"),
    (0.0, 0.0, 'The Definitive Guide to Django'),
    (0.0, 1.0, 'Python in a Nutshell')
]

#3263

Drizzle 方言现在是一个外部方言

Drizzle 的方言现在是一个外部方言,可在 https://bitbucket.org/zzzeek/sqlalchemy-drizzle 上获得。此方言在 SQLAlchemy 能够很好地适应第三方方言之前添加到 SQLAlchemy;展望未来,所有不属于 “普遍使用” 类别的数据库都是第三方方言。该方言的实现没有改变,并且仍然基于 SQLAlchemy 中的 MySQL + MySQLdb 方言。该方言尚未发布,并且处于 “阁楼” 状态;但是,它通过了大多数测试,并且通常处于良好的工作状态,如果有人想对其进行完善,可以继续。

方言改进和更改 - SQLite

SQLite 命名和未命名的 UNIQUE 和 FOREIGN KEY 约束将进行检查和反射

UNIQUE 和 FOREIGN KEY 约束现在在 SQLite 上得到完全反射,无论是否命名。以前,外键名称被忽略,未命名的唯一约束被跳过。特别是,这将有助于 Alembic 的新 SQLite 迁移功能。

为了实现这一点,对于外键和唯一约束,PRAGMA foreign_keys、index_list 和 index_info 的结果与 CREATE TABLE 语句的正则表达式解析相结合,以形成约束名称的完整图景,并区分创建为 UNIQUE 与未命名的 INDEX 的 UNIQUE 约束。

#3244

#3261

方言改进和更改 - SQL Server

使用基于主机名的 SQL Server 连接时,需要 PyODBC 驱动程序名称

使用 PyODBC 连接到 SQL Server 而不使用 DSN 连接(例如,使用显式主机名)现在需要驱动程序名称 - SQLAlchemy 将不再尝试猜测默认值

engine = create_engine(
    "mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0"
)

SQLAlchemy 以前硬编码的 “SQL Server” 默认值在 Windows 上已过时,SQLAlchemy 无法根据操作系统/驱动程序检测来猜测最佳驱动程序。使用 DSN 始终是使用 ODBC 时避免此问题的首选方法。

#3182

SQL Server 2012 大型文本/二进制类型呈现为 VARCHAR、NVARCHAR、VARBINARY

对于 SQL Server 2012 及更高版本,TextClauseUnicodeTextLargeBinary 类型的呈现方式已更改,并提供完全控制行为的选项,基于 Microsoft 的弃用指南。有关详细信息,请参阅 大型文本/二进制类型弃用

方言改进和更改 - Oracle

改进了 Oracle 中对 CTE 的支持

CTE 支持已针对 Oracle 进行了修复,并且还有一个新功能 CTE.with_suffixes() 可以帮助处理 Oracle 的特殊指令

included_parts = (
    select([part.c.sub_part, part.c.part, part.c.quantity])
    .where(part.c.part == "p1")
    .cte(name="included_parts", recursive=True)
    .suffix_with(
        "search depth first by part set ord1",
        "cycle part set y_cycle to 1 default 0",
        dialect="oracle",
    )
)

#3220

Oracle DDL 的新关键字

诸如 COMPRESS、ON COMMIT、BITMAP 之类的关键字

Oracle 数据库表选项

Oracle 数据库特定索引选项