ORM 配置¶
如何映射没有主键的表?¶
SQLAlchemy ORM 为了映射到特定表,至少需要有一列表示为主键列;多列(即复合主键)当然也是完全可行的。这些列实际上不需要被数据库知道为主键
列,尽管它们是个好主意。 只需要将列
行为与 primary key 一样,例如作为 row 的唯一且不可为 null 的标识符。
大多数 ORM 要求对象定义某种主键,因为内存中的对象必须与数据库表中唯一可识别的行相对应;至少,这允许对象可以作为 UPDATE 和 DELETE 语句的目标,这些语句将仅影响该对象的行,而不会影响其他行。但是,主键的重要性远不止于此。在 SQLAlchemy 中,所有 ORM 映射的对象在 Session
中始终是唯一链接的
使用一种称为身份映射的模式添加到它们的特定数据库行中,该模式是 SQLAlchemy 采用的工作单元系统的核心,也是最常见(和不太常见)的 ORM 使用模式的关键。
注意
需要注意的是,我们只在谈论 SQLAlchemy ORM;一个基于 Core 构建并仅处理 Table
对象的应用程序,
select()
结构等不需要任何主键出现在表上或以任何方式与表关联(尽管同样,在 SQL 中,所有表都应该有某种主键,以免您实际需要更新或删除特定行)。
在几乎所有情况下,表都有一个所谓的候选键,它是唯一标识一行的一列或一系列列。如果一个表真的没有这个,并且有实际的完全重复的行,则该表不对应于第一范式,并且无法映射。否则,构成最佳候选键的任何列都可以直接应用于 mapper:
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 保留字或类似字的 Column?¶
可以为基于列的属性指定映射中所需的任何名称。看
显式命名声明性映射列。
如何获取给定映射类的所有列、关系、映射属性等的列表?¶
此信息都可以从 Mapper
对象中获得。
要获取特定映射类的 Mapper
,请调用
inspect()
函数:
from sqlalchemy import inspect
mapper = inspect(MyClass)
从那里,可以通过以下属性访问有关类的所有信息:
Mapper.attrs
- 所有映射属性的命名空间。属性本身是MapperProperty
的实例,其中包含可导致映射的 SQL 表达式或列的其他属性(如果适用)。Mapper.column_attrs
- 映射的属性命名空间 仅限于 column 和 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
- 此映射器的“本地”表
;这与Mapper.mapped_table
不同,因为使用继承将 Mapper 映射到 Composed Selectable。
我收到关于 “Implicitically combining column X under attribute Y” 的警告或错误¶
这种情况是指映射包含两列,由于它们的名称,这两个列被映射在相同的属性名称下,但没有迹象表明这是有意为之。映射类需要为要存储独立值的每个属性具有显式名称;当两列具有相同的名称且未消除歧义时,它们属于同一属性,其效果是,根据首先分配给属性的列,将一列中的值复制到另一列中。
这种行为通常是可取的,并且在这种情况下是允许的,恕不另行通知
其中,两列通过外键关系链接在一起
在继承映射中。 当出现警告或异常时,
可以通过将列分配给 Different-Named 来解决
属性,或者如果需要将它们组合在一起,请使用
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 开始,检测到上述情况,并会警告 A
和 B
的 id
列正在合并在同名属性 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"))
我正在使用 Declarative 并使用 and_()
或 or_()
设置 primaryjoin/secondaryjoin,并且收到有关外键的错误消息。¶
您正在执行此作吗?
class MyClass(Base):
# ....
foo = relationship(
"Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
)
这是两个字符串表达式的 and_(),
SQLAlchemy 无法对其应用任何映射。Declarative 允许将 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])
为什么建议使用 ORDER BY
和 LIMIT
(尤其是 subqueryload()
)?¶
当 ORDER BY 不用于返回行的 SELECT 语句时,
关系数据库可以自由地返回任意
次序。 虽然这种排序通常对应于自然
表中的行顺序,并非所有数据库和所有
查询。这样做的结果是,任何使用
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|
+------------+-----------------+---------------+
在上面,我们收到 2 user.id
的 2 个地址
行,1 个地址没有。我们浪费了两行,并且未能实际加载集合。这是一个阴险的错误,因为如果不查看 SQL 和结果,ORM 不会显示存在任何问题;如果我们访问地址
对于我们拥有的 User
,它将为集合发出延迟加载,我们不会看到任何实际出错的地方。
此问题的解决方案是始终指定确定性的排序顺序,以便主查询始终返回相同的行集。这通常意味着您应该对 table 上的唯一列Select.order_by()。
主键是一个不错的选择:
session.scalars(
select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()
请注意,joinedload()
急切加载器策略不会遇到同样的问题,因为只发出一个查询,因此 load 查询不能与主查询不同。同样,selectinload()
Eager Loader 策略也没有这个问题,因为它链接了它的集合
直接加载到刚刚加载的主键值。
另请参阅
什么是 default
、default_factory
和 insert_default
,我应该使用什么?¶
由于添加了 PEP-681 数据类转换,SQLAlchemy 的 API 在这里存在一些冲突,这对其命名约定非常严格。如果你使用的是 MappedAsDataclass,PEP-681
就会发挥作用,如 声明式数据类映射 中所示。如果您未使用 MappedAsDataclass,则它不适用。
第一部分 - 不使用数据类的经典 SQLAlchemy¶
当不使用 MappedAsDataclass
时,就像 SQLAlchemy 中多年来的情况一样,mapped_column()(
和 Column
)构造支持参数 mapped_column.default
。这表示在发出 INSERT
语句时将发生 Python 端默认值(而不是服务器端默认值,服务器端默认值将成为数据库架构定义的一部分)。此默认值可以是静态 Python 值(如字符串)、Python 可调用函数或 SQLAlchemy SQL 构造中的任何一个。mapped_column.default
的完整文档位于
客户端调用的 SQL 表达式。
当 mapped_column.default
与非
使用 MappedAsData类
时,此默认值 /callable 不会显示
在首次构造对象时将其置于对象上。仅当 SQLAlchemy 为您的对象执行 INSERT
语句时,才会发生此事件。
需要注意的非常重要的一点是,在使用 mapped_column()
时
(和 Column
),则经典的 mapped_column.default
parameter 也以一个名为
mapped_column.insert_default
中。如果您构建了一个
mapped_column()
并且您没有使用 MappedAsData类
,则
mapped_column.default
和 mapped_column.insert_default
parameters 是同义词。
第二部分 - 使用 MappedAsDataclass 的 Dataclasses 支持¶
当您使用 MappedAsDataclass
时,即 Declarative Dataclass Mapping 中使用的特定映射形式,则
mapped_column.default
关键字更改。我们认识到,这个名称改变其行为并不理想,但是没有其他选择,因为 PEP-681 需要 mapped_column.default
才能具有此含义。
使用数据类时,mapped_column.default
参数必须
按照
Python 数据类 - 它引用字符串或数字等常量值,并应用于您的对象
在构建时立即。目前,它也应用于
mapped_column.default
参数,即使不存在,它也会自动在
INSERT
语句中使用
在对象上。如果您想为数据类使用可调用对象,则
,它将在构造时应用于对象,您将使用
mapped_column.default_factory
中。
访问 mapped_column.default
的仅 INSERT
行为
如上面的第一部分所述,您将使用
mapped_column.insert_default
参数。
mapped_column.insert_default
(当使用数据类时)仍然是到核心级“default”进程的直接路由,其中参数可以是静态值或可调用的。
|
|
|
|
|
|
---|---|---|---|---|---|
✔ |
✔ |
✔ |
|
|
|
✔ |
✔ |
✔ |
✔ |
✖ |
|
✔ |
✖ |
✖ |
✔ |
|