ORM 快速入门¶
对于想要快速了解基本 ORM 用法的新用户,这里有一个
映射和示例的缩写形式
SQLAlchemy Unified 教程。这里的代码完全可以从干净的命令行运行。
由于本节中的描述故意非常简短,请继续阅读完整的 SQLAlchemy Unified Tutorial,以更深入地了解此处说明的每个概念。
在 2.0 版更改: ORM 快速入门已更新为最新版本
使用新构造的 PEP 484 感知功能,包括
mapped_column()
中。 请参阅该部分
用于迁移信息的 ORM 声明性模型。
声明模型¶
在这里,我们定义了将形成结构体的模块级结构
我们将从数据库中查询。 这种结构称为
声明性映射 (Declarative Mapping) 同时定义了 Python 对象模型,以及描述特定数据库中存在或将要存在的真实 SQL 表的数据库元数据:
>>> from typing import List
>>> from typing import Optional
>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import String
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship
>>> class Base(DeclarativeBase):
... pass
>>> class User(Base):
... __tablename__ = "user_account"
...
... id: Mapped[int] = mapped_column(primary_key=True)
... name: Mapped[str] = mapped_column(String(30))
... fullname: Mapped[Optional[str]]
...
... addresses: Mapped[List["Address"]] = relationship(
... back_populates="user", cascade="all, delete-orphan"
... )
...
... def __repr__(self) -> str:
... return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
>>> class Address(Base):
... __tablename__ = "address"
...
... id: Mapped[int] = mapped_column(primary_key=True)
... email_address: Mapped[str]
... user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...
... user: Mapped["User"] = relationship(back_populates="addresses")
...
... def __repr__(self) -> str:
... return f"Address(id={self.id!r}, email_address={self.email_address!r})"
映射从基类开始,上面称为 Base
,它是通过对 DeclarativeBase
创建一个简单的子类来创建的
类。
然后,通过创建 Base
的子类来创建单个映射类。映射类通常引用单个特定的数据库表,其名称通过使用 __tablename__
类级属性来指示。
接下来,通过添加包含名为 Mapped
的特殊类型注释的属性来声明属于表的列。每个属性的名称对应于要成为数据库表一部分的列。每个列的数据类型首先取自与每个 Mapped
注释关联的 Python 数据类型;int
的
INTEGER
、VARCHAR
的 str
等。可为 Null 性派生于是否使用 Optional[]
类型修饰符。更具体的键入信息
可以使用右侧的 SQLAlchemy 类型对象来指示
mapped_column()
指令,例如上面在 User.name
列中使用的 String
数据类型。Python 类型之间的关联
SQL 类型可以使用
键入 Annotation Map。
mapped_column()
指令用于需要更具体自定义的所有基于列的属性。除了键入信息之外,该指令还接受各种参数,这些参数指示有关数据库列的特定详细信息,包括服务器默认值和约束信息,例如主键和外键中的成员资格。mapped_column()
指令接受 SQLAlchemy Column
类接受的参数超集,SQLAlchemy Core 使用该类来表示数据库列。
所有 ORM 映射类都要求至少将一列声明为主键的一部分,通常使用 Column.primary_key
parameter 应用于应属于 key 的 mapped_column()
对象上。在上面的示例中,User.id
和 Address.id
列标记为 Primary Key。
总而言之,字符串表名和列声明列表的组合在 SQLAlchemy 中称为 table metadata。使用 Core 和 ORM 方法设置表元数据在 SQLAlchemy Unified 教程中的使用数据库元数据中进行了介绍。
上面的映射是所谓的
带注释的声明式表
配置。
Mapped
的其他变体可用,最常见的是上面指示的 relationship()
结构。与基于列的属性相反,relationship()
表示两个 ORM 类之间的链接。在上面的示例中,User.addresses
链接
User
to Address,address.user
将 Address
链接到 User
。relationship()
结构在
SQLAlchemy Unified 教程,位于 Working with ORM Related Objects.
最后,上面的示例类包括一个 __repr__()
方法,该方法不是必需的,但对调试很有用。可以使用数据类自动生成的 __repr__()
等方法创建映射类。有关数据类映射的更多信息,请参阅声明性数据类映射。
创建引擎¶
Engine
是一个可以为我们创建新的数据库连接的工厂,它还保留连接池内的连接以便快速重用。为了方便起见,我们通常使用 SQLite 纯内存数据库:
>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://", echo=True)
提示
echo=True
参数表示连接发出的 SQL 将被记录到 standard out。
发出 CREATE TABLE DDL¶
使用我们的表元数据和引擎,我们可以使用名为 MetaData.create_all()
的方法在目标 SQLite 数据库中立即生成我们的架构:
>>> Base.metadata.create_all(engine)
BEGIN (implicit)
PRAGMA main.table_...info("user_account")
...
PRAGMA main.table_...info("address")
...
CREATE TABLE user_account (
id INTEGER NOT NULL,
name VARCHAR(30) NOT NULL,
fullname VARCHAR,
PRIMARY KEY (id)
)
...
CREATE TABLE address (
id INTEGER NOT NULL,
email_address VARCHAR NOT NULL,
user_id INTEGER NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user_account (id)
)
...
COMMIT
我们编写的那段 Python 代码发生了很多事情。有关 Table 元数据的完整概述,请继续学习 Using Database Metadata 中的教程。
创建对象并持久化¶
现在,我们已准备好在数据库中插入数据。我们通过创建 User
和 Address
类的实例来实现这一点,这些实例具有由声明式映射过程自动建立的 __init__()
方法。然后,我们使用一个名为 Session 的对象将它们传递给数据库,该对象使用 Engine
与数据库交互。这里使用 Session.add_all()
方法一次添加多个对象,而 Session.commit()
方法将用于刷新对数据库的任何待处理更改,然后提交当前的数据库事务,每当 Session
用于:
>>> from sqlalchemy.orm import Session
>>> with Session(engine) as session:
... spongebob = User(
... name="spongebob",
... fullname="Spongebob Squarepants",
... addresses=[Address(email_address="spongebob@sqlalchemy.org")],
... )
... sandy = User(
... name="sandy",
... fullname="Sandy Cheeks",
... addresses=[
... Address(email_address="sandy@sqlalchemy.org"),
... Address(email_address="sandy@squirrelpower.org"),
... ],
... )
... patrick = User(name="patrick", fullname="Patrick Star")
...
... session.add_all([spongebob, sandy, patrick])
...
... session.commit()
BEGIN (implicit)
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('spongebob', 'Spongebob Squarepants')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('sandy', 'Sandy Cheeks')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('patrick', 'Patrick Star')
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('spongebob@sqlalchemy.org', 1)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('sandy@sqlalchemy.org', 2)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('sandy@squirrelpower.org', 2)
COMMIT
提示
建议以上述上下文管理器样式使用 Session
,即使用 Python with:
语句。Session
对象表示活动的数据库资源,因此最好确保在完成一系列作时将其关闭。在下一节中,我们将保留一个 Session
仅用于说明目的。
有关创建 Session
的基础知识,请参阅
使用 ORM 会话执行等内容,请参阅使用 Session 的基础知识。
然后,在 使用 ORM Unit of Work 模式插入行中介绍了一些基本持久性作。
简单的 SELECT¶
对于数据库中的一些行,这是发出 SELECT 的最简单形式
语句加载一些对象。要创建 SELECT 语句,我们使用
select()
函数创建一个新的 Select
对象,然后使用 Session
调用该对象。在查询 ORM 对象时,通常有用的方法是 Session.scalars()
方法,它将返回一个 ScalarResult
对象,该对象将遍历我们选择的 ORM 对象:
>>> from sqlalchemy import select
>>> session = Session(engine)
>>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))
>>> for user in session.scalars(stmt):
... print(user)
BEGIN (implicit)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name IN (?, ?)
[...] ('spongebob', 'sandy')
User(id=1, name='spongebob', fullname='Spongebob Squarepants')
User(id=2, name='sandy', fullname='Sandy Cheeks')
上面的查询还使用了 Select.where()
方法来添加 WHERE 条件,并且还使用了 ColumnOperators.in_()
方法,它是所有 SQLAlchemy 列式结构的一部分,以使用
SQL IN 运算符。
有关如何选择对象和单个列的更多详细信息,请参阅
选择 ORM 实体和列。
SELECT 与 JOIN¶
一次在多个表之间进行查询是很常见的,在 SQL 中,JOIN 关键字是发生这种情况的主要方式。精选
construct 使用 Select.join()
方法创建连接:
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "sandy")
... .where(Address.email_address == "sandy@sqlalchemy.org")
... )
>>> sandy_address = session.scalars(stmt).one()
SELECT address.id, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? AND address.email_address = ?
[...] ('sandy', 'sandy@sqlalchemy.org')
>>> sandy_address
Address(id=2, email_address='sandy@sqlalchemy.org')
上面的查询说明了使用 AND 自动链接在一起的多个 WHERE 条件,以及如何使用 SQLAlchemy 列状对象创建“相等”比较,它使用覆盖的 Python 方法 ColumnOperators.__eq__()
来生成 SQL 条件对象。
有关上述概念的更多背景信息,请参阅
WHERE 子句和显式 FROM 子句和 JOIN。
进行更改¶
Session
对象,结合我们的 ORM 映射类
User
和 Address
在对象发生更改时自动跟踪这些更改,这会导致 SQL 语句将在下次 Session
刷新时发出。下面,我们更改了一个与 “sandy” 关联的电子邮件地址,并在发出 SELECT 以检索 “patrick” 的行后,向 “patrick” 添加了一个新的电子邮件地址:
>>> stmt = select(User).where(User.name == "patrick")
>>> patrick = session.scalars(stmt).one()
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
>>> patrick.addresses.append(Address(email_address="patrickstar@sqlalchemy.org"))
SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,)
>>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org"
>>> session.commit()
UPDATE address SET email_address=? WHERE address.id = ?
[...] ('sandy_cheeks@sqlalchemy.org', 2)
INSERT INTO address (email_address, user_id) VALUES (?, ?)
[...] ('patrickstar@sqlalchemy.org', 3)
COMMIT
请注意,当我们访问 patrick.addresses
时,会发出一个 SELECT。这称为延迟加载。Loader Strategies 中介绍了使用或多或少 SQL 访问相关项的不同方法的背景。
有关 ORM 数据作的详细演练从
使用 ORM 进行数据作。
一些删除¶
所有事情都必须结束,就像我们的一些数据库行一样 - 以下是两种不同形式的删除的快速演示,根据特定的用例,这两种形式都很重要。
首先,我们将从 “sandy” 用户中删除一个 Address
对象。当 Session
下次刷新时,这将导致该行被删除。此行为是我们在 Map 中配置的内容,称为 delete cascade。我们可以处理沙地
object 替换为主键,然后使用 Session.get()
处理该对象:
>>> sandy = session.get(User, 2)
BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,)
>>> sandy.addresses.remove(sandy_address)
SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (2,)
上面的最后一个 SELECT 是延迟加载作,以便可以加载 sandy.addresses
集合,以便我们可以删除
sandy_address
成员。还有其他方法可以执行这一系列作,这些作不会发出那么多的 SQL。
我们可以选择为到目前为止要更改的内容发出 DELETE SQL,而不使用
提交事务,使用
Session.flush()
方法:
>>> session.flush()
DELETE FROM address WHERE address.id = ?
[...] (2,)
接下来,我们将完全删除 “patrick” 用户。对于对象本身的顶级删除,我们使用 Session.delete()
方法;此方法实际上并不执行删除,而是将对象设置为在下一次刷新时删除。该作还将根据我们配置的 cascade 选项级联到相关对象,在本例中,级联到相关的 Address
对象上:
>>> session.delete(patrick)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (3,)
SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,)
在这个特定情况下,Session.delete()
方法发出了两个
SELECT 语句,即使它没有发出 DELETE,这可能看起来令人惊讶。
这是因为当该方法去检查对象时,结果发现
Patrick
Object 已过期,这发生在我们上次调用
Session.commit(),
并且发出的 SQL 是从新事务中重新加载行。此过期是可选的,在正常使用中,我们通常会在它不太适用的情况下将其关闭。
为了说明要删除的行,下面是提交:
>>> session.commit()
DELETE FROM address WHERE address.id = ?
[...] (4,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
COMMIT
本教程在 Deleting ORM Objects using the Unit of Work 模式中讨论了 ORM 删除。对象过期的背景为 Expiring/Refreshing;Cascades 中对 cascades 进行了深入讨论。
深入学习上述概念¶
对于新用户来说,上述部分可能是一次旋风式的游览。上述每个步骤中都有很多重要的概念没有涵盖。在快速了解了事情的样子之后,建议先完成 SQLAlchemy Unified Tutorial,以获得对上面真正发生的事情的扎实工作知识。祝你好运!