SQLAlchemy 2.0 文档
常见问题解答
- 安装
- 连接 / 引擎
- 元数据 / 架构
- SQL 表达式
- ORM 配置¶
- 性能
- 会话 / 查询
- 第三方集成问题
项目版本
- 上一篇: SQL 表达式
- 下一篇: 性能
- 上一级: 首页
- 本页内容
ORM 配置¶
如何映射没有主键的表?¶
为了映射到特定表,SQLAlchemy ORM 需要至少有一个列被标记为主键列;多列(即复合)主键当然也完全可行。这些列不需要实际在数据库中被识别为主键列,尽管它们是主键列是一个好主意。唯一的要求是这些列的行为必须与主键相同,例如作为行的唯一且不可为空的标识符。
大多数 ORM 要求对象定义某种类型的主键,因为内存中的对象必须对应于数据库表中唯一可识别的行;至少,这允许对象可以被定位用于 UPDATE 和 DELETE 语句,这些语句将只影响该对象的特定行,而不会影响其他任何行。但是,主键的重要性远远超出了此范围。在 SQLAlchemy 中,所有 ORM 映射的对象始终在 Session
中与其特定数据库行唯一关联,使用称为标识映射 的模式,此模式是 SQLAlchemy 使用的工作单元系统的核心,也是大多数(以及非典型)ORM 使用模式的关键。
注意
重要的是要注意,我们只讨论 SQLAlchemy ORM;基于 Core 并且只处理 Table
对象、select()
结构等应用程序不需要在任何方式上存在于表上或与表相关联的主键(尽管再次,在 SQL 中,所有表都应该真正具有某种类型的 主键,否则您将需要实际更新或删除特定行)。
在几乎所有情况下,表都具有所谓的候选键,它是一列或一系列唯一标识行的列。如果表真的没有候选键,并且具有完全重复的行,则该表不符合第一范式,无法映射。否则,构成最佳候选键的任何列都可以直接应用到映射器
class SomeClass(Base):
__table__ = some_table_with_no_pk
__mapper_args__ = {
"primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
}
更好的方法是,当使用完全声明的表元数据时,在这些列上使用 primary_key=True
标志
class SomeClass(Base):
__tablename__ = "some_table_with_no_pk"
uid = Column(Integer, primary_key=True)
bar = Column(String, primary_key=True)
关系数据库中的所有表都应该具有主键。即使是多对多关联表 - 主键也将是两个关联列的组合
CREATE TABLE my_association (
user_id INTEGER REFERENCES user(id),
account_id INTEGER REFERENCES account(id),
PRIMARY KEY (user_id, account_id)
)
如何配置一个 Python 保留字或类似的列?¶
基于列的属性可以在映射中指定任何想要的名称。请参见显式命名声明性映射列.
如何获取给定映射类的所有列、关系、映射属性等的列表?¶
所有这些信息都可以在 Mapper
对象中找到。
要获得特定映射类的 Mapper
,请在其上调用 inspect()
函数
from sqlalchemy import inspect
mapper = inspect(MyClass)
从那里开始,所有关于该类的信息都可以通过以下属性访问
Mapper.attrs
- 所有映射属性的命名空间。属性本身是MapperProperty
的实例,其中包含可以导致映射的 SQL 表达式或列的附加属性(如果适用)。Mapper.column_attrs
- 映射属性命名空间,仅限于列和 SQL 表达式属性。您可能希望使用Mapper.columns
直接获取Column
对象。Mapper.relationships
- 所有RelationshipProperty
属性的命名空间。Mapper.all_orm_descriptors
- 所有映射属性以及使用hybrid_property
、AssociationProxy
等系统定义的用户定义属性的命名空间。Mapper.columns
- 与映射关联的Column
对象和其他命名 SQL 表达式的命名空间。Mapper.mapped_table
- 此映射器映射到的Table
或其他可选择项。Mapper.local_table
- 对此映射器“本地”的Table
;在使用继承映射到组合可选择项的映射器的情况下,这与Mapper.mapped_table
不同。
我收到关于“隐式组合属性 Y 下的列 X”的警告或错误¶
这种情况是指,当映射包含两列,由于它们的名称,这两列在相同的属性名称下被映射,但没有迹象表明这是故意的。映射类需要为每个存储独立值的属性指定显式名称;当两个列具有相同的名称且没有区分时,它们将位于相同的属性下,其效果是,来自一个列的值被复制到另一个列中,具体取决于哪个列首先被分配给该属性。
这种行为通常是理想的,并且在两列通过继承映射中的外键关系链接在一起的情况下,无需警告即可允许。当警告或异常发生时,可以通过将列分配给不同名称的属性来解决此问题,或者如果希望将它们组合在一起,可以使用 column_property()
来明确地实现这一点。
假设有以下示例
from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("a.id"))
从 SQLAlchemy 版本 0.9.5 开始,上述情况会被检测到,并会警告说 id
列在 A
和 B
中被组合到同一个名称的属性 id
下,这上面是一个严重的问题,因为它意味着 B
对象的主键将始终反映其 A
的主键。
解决此问题的映射如下
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
b_id = Column("id", Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("a.id"))
假设我们确实希望 A.id
和 B.id
互相镜像,尽管事实上 B.a_id
是 A.id
所关联的地方。我们可以使用 column_property()
将它们组合在一起
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = "b"
# probably not what you want, but this is a demonstration
id = column_property(Column(Integer, primary_key=True), A.id)
a_id = Column(Integer, ForeignKey("a.id"))
我正在使用声明式映射,并使用 and_()
或 or_()
设置 primaryjoin/secondaryjoin,但我收到了有关外键的错误消息。¶
你是在这样做吗?
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
)
这是一个 and_()
,它包含两个字符串表达式,SQLAlchemy 无法对它们进行任何映射。声明式映射允许 relationship()
参数以字符串形式指定,这些字符串使用 eval()
转换为表达式对象。但这不会发生在 and_()
表达式内部 - 这是一个声明式映射专门应用于传递给 primaryjoin 或其他参数的字符串的全部内容的操作
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
)
或者,如果所需的这些对象已经可用,则跳过字符串
class MyClass(Base):
# ....
foo = relationship(
Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
)
同样的想法也适用于所有其他参数,例如 foreign_keys
# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])
# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")
# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])
# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
foo_id = Column(...)
bar_id = Column(...)
# ...
foo = relationship(Dest, foreign_keys=[foo_id, bar_id])
为什么推荐在使用 LIMIT
(尤其是与 subqueryload()
一起使用)时使用 ORDER BY
?¶
当返回行的 SELECT 语句未使用 ORDER BY 时,关系型数据库可以自由地以任何任意顺序返回匹配的行。虽然这种排序通常对应于表中行的自然顺序,但这并不适用于所有数据库和所有查询。其结果是,任何使用 LIMIT
或 OFFSET
限制行,或者仅仅选择结果集的第一行并丢弃其余行的查询,在返回结果行方面都不会是确定性的,假设有多个行匹配查询的条件。
虽然我们可能没有注意到对通常以自然顺序返回行的数据库进行简单查询时出现的这种情况,但如果我们还使用 subqueryload()
加载相关集合,并且我们可能没有按预期加载集合,这种情况就会成为一个更大的问题。
SQLAlchemy 通过发出单独的查询来实现 subqueryload()
,其结果与第一个查询的结果相匹配。我们看到发出如下两个查询
>>> session.scalars(select(User).options(subqueryload(User.addresses))).all()
-- the "main" query
SELECT users.id AS users_id
FROM users
-- the "load" query issued by subqueryload
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id FROM users) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
第二个查询将第一个查询嵌入为行源。当内部查询使用 OFFSET
和/或 LIMIT
而没有排序时,这两个查询可能看不到相同的结果
>>> user = session.scalars(
... select(User).options(subqueryload(User.addresses)).limit(1)
... ).first()
-- the "main" query
SELECT users.id AS users_id
FROM users
LIMIT 1
-- the "load" query issued by subqueryload
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id FROM users LIMIT 1) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
根据数据库的具体情况,这两个查询可能会得到以下类似的结果
-- query #1
+--------+
|users_id|
+--------+
| 1|
+--------+
-- query #2
+------------+-----------------+---------------+
|addresses_id|addresses_user_id|anon_1_users_id|
+------------+-----------------+---------------+
| 3| 2| 2|
+------------+-----------------+---------------+
| 4| 2| 2|
+------------+-----------------+---------------+
上面,我们收到了两个 addresses
行用于 user.id
为 2 的行,而没有用于 1 的行。我们浪费了两行,并且未能真正加载集合。这是一个隐蔽的错误,因为如果不查看 SQL 和结果,ORM 将不会显示有任何问题;如果我们访问我们拥有的 User
的 addresses
,它会发出对该集合的延迟加载,我们不会看到任何实际出错。
解决此问题的方法是始终指定确定性的排序顺序,以便主查询始终返回相同的行集。这通常意味着你应该对表上的唯一列进行 Select.order_by()
。主键是此操作的良好选择
session.scalars(
select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()
请注意,joinedload()
积极加载策略不会遇到同样的问题,因为只发出一个查询,因此加载查询不能与主查询不同。类似地,selectinload()
积极加载策略也不存在此问题,因为它将它的集合加载直接链接到刚刚加载的主键值。
另请参阅
什么是 default
、default_factory
和 insert_default
,我应该使用哪个?¶
由于添加了 PEP-681 数据类转换,SQLAlchemy 的 API 中存在一些冲突,该转换对其命名约定非常严格。如果你使用的是 MappedAsDataclass
,如 声明式数据类映射 中所示,PEP-681 将生效。如果你没有使用 MappedAsDataclass,则它不适用。
第一部分 - 不使用数据类的经典 SQLAlchemy¶
当不使用 MappedAsDataclass
时,这在 SQLAlchemy 中已经存在多年了,mapped_column()
(和 Column
)构造支持一个参数 mapped_column.default
。这表示一个 Python 侧的默认值(与数据库模式定义的一部分的服务器端默认值相反),该默认值将在发出 INSERT
语句时生效。此默认值可以是任何静态 Python 值(如字符串),或 Python 可调用函数,或 SQLAlchemy SQL 构造。有关 mapped_column.default
的完整文档,请参阅 客户端调用的 SQL 表达式。
当在不使用 MappedAsDataclass
的 ORM 映射中使用 mapped_column.default
时,此默认值/可调用函数不会在你首次构造对象时显示在你的对象上。它只会在 SQLAlchemy 生成你的对象的 INSERT
语句时生效。
需要注意的一件非常重要的事情是,当使用 mapped_column()
(和 Column
)时,经典的 mapped_column.default
参数也可以使用一个新名称,名为 mapped_column.insert_default
。如果你构建了一个 mapped_column()
并且你没有使用 MappedAsDataclass
,则 mapped_column.default
和 mapped_column.insert_default
参数是同义词。
第二部分 - 在 MappedAsDataclass 中使用 Dataclasses 支持¶
当您正在使用 MappedAsDataclass
时,即在 声明式 Dataclass 映射 中使用的特定映射形式,mapped_column.default
关键字的含义会发生变化。我们认识到,这个名称改变其行为并非理想,但别无选择,因为 PEP-681 要求 mapped_column.default
承担这个含义。
当使用 dataclasses 时,mapped_column.default
参数必须按照 Python Dataclasses 中描述的方式使用 - 它指的是一个常数值,例如字符串或数字,并且在构造时立即应用于您的对象。目前它也应用于 mapped_column.default
参数的 Column
,即使在对象中不存在时,它也会在 INSERT
语句中自动使用。如果您想在 dataclass 中使用一个可调用对象,它将在构造时应用于对象,那么您将使用 mapped_column.default_factory
。
要访问上面第一部分中描述的 mapped_column.default
的仅 INSERT
行为,您将使用 mapped_column.insert_default
参数。当使用 dataclasses 时,mapped_column.insert_default
仍然是 Core 级别的“默认”过程的直接途径,其中参数可以是静态值或可调用对象。
构造 |
与 dataclasses 一起使用? |
在没有 dataclasses 的情况下使用? |
接受标量? |
接受可调用对象? |
立即填充对象? |
---|---|---|---|---|---|
✔ |
✔ |
✔ |
只有在没有 dataclasses 的情况下 |
只有在 dataclasses 的情况下 |
|
✔ |
✔ |
✔ |
✔ |
✖ |
|
✔ |
✖ |
✖ |
✔ |
只有在 dataclasses 的情况下 |
flambé! 龙和炼金术士 图片设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档最后生成时间:2024 年 11 月 8 日星期五上午 08:41:19 EST