SQLAlchemy & Oracle Database 23c Free 集成之旅

SQLAlchemy & Oracle Database 23c Free 集成之旅

  • [1. SQLAlchemy 是什么](#1. SQLAlchemy 是什么)
  • [2. Oracle Database 23c Free 是什么](#2. Oracle Database 23c Free 是什么)
  • [3. 运行 Oracle Database 23c Free](#3. 运行 Oracle Database 23c Free)
  • [4. 学习 SQLAlchemy 统一教程](#4. 学习 SQLAlchemy 统一教程)
    • [4-1. 安装依赖库](#4-1. 安装依赖库)
    • [4-2. 建立连接 - 引擎](#4-2. 建立连接 - 引擎)
    • [4-3. 使用事务和 DBAPI](#4-3. 使用事务和 DBAPI)
      • [4-3-1. 获取连接](#4-3-1. 获取连接)
      • [4-3-2. 提交更改](#4-3-2. 提交更改)
      • [4-3-3. 语句执行的基础知识](#4-3-3. 语句执行的基础知识)
        • [4-3-3-1. 获取行](#4-3-3-1. 获取行)
        • [4-3-3-2: 发送参数](#4-3-3-2: 发送参数)
        • [4-3-3-3. 发送多个参数](#4-3-3-3. 发送多个参数)
      • [4-3-4. 使用 ORM 会话执行](#4-3-4. 使用 ORM 会话执行)
      • [4-3-5. 使用数据库元数据](#4-3-5. 使用数据库元数据)
        • [4-3-5-1. 使用表对象设置元数据](#4-3-5-1. 使用表对象设置元数据)
          • [4-3-5-1-1. Table 的组成部分](#4-3-5-1-1. Table 的组成部分)
          • [4-3-5-1-2. 声明简单约束](#4-3-5-1-2. 声明简单约束)
          • [4-3-5-1-3. 向数据库发送 DDL](#4-3-5-1-3. 向数据库发送 DDL)
      • [4-3-6. 使用 ORM 声明式形式定义表元数据](#4-3-6. 使用 ORM 声明式形式定义表元数据)
        • [4-3-6-1. 建立声明性基础](#4-3-6-1. 建立声明性基础)
        • [4-3-6-2. 声明映射类](#4-3-6-2. 声明映射类)
        • [4-3-6-3. 从 ORM 映射向数据库发送 DDL](#4-3-6-3. 从 ORM 映射向数据库发送 DDL)
      • [4-3-7. 表反射](#4-3-7. 表反射)
    • [4-4. 处理数据](#4-4. 处理数据)
      • [4-4-1. 使用 INSERT 语句](#4-4-1. 使用 INSERT 语句)
        • [4-4-1-1. insert() SQL 表达式结构](#4-4-1-1. insert() SQL 表达式结构)
        • [4-4-1-2. 执行语句](#4-4-1-2. 执行语句)
        • [4-4-1-3. INSERT 通常会自动生成"values"子句](#4-4-1-3. INSERT 通常会自动生成“values”子句)
        • [4-4-1-4. 插入...返回](#4-4-1-4. 插入…返回)
        • [4-4-1-5. 插入...从选择](#4-4-1-5. 插入…从选择)
      • [4-4-2. 使用 SELECT 语句](#4-4-2. 使用 SELECT 语句)
        • [4-4-2-1. select() SQL 表达式结构](#4-4-2-1. select() SQL 表达式结构)
        • [4-4-2-2. 设置 COLUMNS 和 FROM 子句](#4-4-2-2. 设置 COLUMNS 和 FROM 子句)
        • [4-4-2-3. 选择 ORM 实体和列](#4-4-2-3. 选择 ORM 实体和列)
        • [4-4-2-4. 从带标签的 SQL 表达式中选择](#4-4-2-4. 从带标签的 SQL 表达式中选择)
        • [4-4-2-5. 使用文本列表达式进行选择](#4-4-2-5. 使用文本列表达式进行选择)
        • [4-4-2-6. WHERE 子句](#4-4-2-6. WHERE 子句)
        • [4-4-2-7. 显式 FROM 子句和 JOIN](#4-4-2-7. 显式 FROM 子句和 JOIN)
        • [4-4-2-8. 设置 ON 子句](#4-4-2-8. 设置 ON 子句)
        • [4-4-2-9. 外部连接和完整连接](#4-4-2-9. 外部连接和完整连接)
        • [4-4-2-10. ORDER BY, GROUP BY, HAVING](#4-4-2-10. ORDER BY, GROUP BY, HAVING)
        • [4-4-2-11. 使用别名](#4-4-2-11. 使用别名)
        • [4-4-2-12. 子查询和 CTEs](#4-4-2-12. 子查询和 CTEs)
        • [4-4-2-13. 标量和相关子查询](#4-4-2-13. 标量和相关子查询)

1. SQLAlchemy 是什么

SQLAlchemy 是 Python SQL 工具包和对象关系映射器,为应用程序开发人员提供 SQL 的全部功能和灵活性。

它提供了一整套众所周知的企业级持久化模式,专为高效、高性能的数据库访问而设计,并适应于简单且Pythonic的领域语言。

官网地址:https://www.sqlalchemy.org/

SQLAlchemy SQL 工具包和对象关系映射器是一套用于处理数据库和 Python 的综合工具。它具有几个不同的功能领域,可以单独使用或组合在一起使用。其主要组件如下图所示,组件依赖关系组织成层:

上面,SQLAlchemy 的两个最重要的前端部分是对象关系映射器 (ORM) 和核心。

Core 包含 SQLAlchemy 的 SQL 和数据库集成以及描述服务的广度,其中最突出的部分是 SQL 表达式语言。

SQL 表达式语言本身就是一个工具包,独立于 ORM 包,它提供了一个构建由可组合对象表示的 SQL 表达式的系统,然后可以在特定事务范围内对目标数据库"执行",返回一个结果集。插入、更新和删除(即 DML)是通过传递代表这些语句的 SQL 表达式对象以及代表要与每个语句一起使用的参数的字典来实现的。

ORM 构建在 Core 之上,提供了一种使用映射到数据库模式的域对象模型的方法。使用 ORM 时,SQL 语句的构建方式与使用 Core 时的方式大致相同,但是 DML 任务(此处指的是数据库中业务对象的持久性)是使用称为工作单元的模式自动化的,该模式将将针对可变对象的状态更改为 INSERT、UPDATE 和 DELETE 构造,然后根据这些对象调用这些构造。 SELECT 语句还通过 ORM 特定的自动化和以对象为中心的查询功能进行了增强。

虽然使用 Core 和 SQL 表达式语言提供了以模式为中心的数据库视图以及面向不变性的编程范式,但 ORM 在此基础上构建了以域为中心的数据库视图,其编程范式是更明确地面向对象并依赖于可变性。由于关系数据库本身就是一个可变服务,因此不同之处在于 Core/SQL Expression 语言是面向命令的,而 ORM 是面向状态的。

参考文档:https://docs.sqlalchemy.org/en/20/intro.html

2. Oracle Database 23c Free 是什么

Oracle Database 23c Free 是关系数据库服务 Oracle Database 23c(2023年9月时点还未正式发布) 的开发者版本,此容器映像包含具有一个 PDB 的多租户配置中的默认数据库。

3. 运行 Oracle Database 23c Free

我们需要先启动一个 Oracle Database 23c Free,做为 SQLAlchemy 的后端数据库使用。

创建挂载路径,请根据各自环境修改,

sudo mkdir -p /u01/data/oracledb && sudo chown oracle:oracle /u01/data/oracledb && sudo chmod 777 /u01/data/oracledb

运行 Oracle Database Free Release 23c,

docker run --name oracledb23c --restart=always \
-p 1521:1521 \
-e ORACLE_PWD=<your database passwords> \
-v /u01/data/oracledb:/opt/oracle/oradata \
-d container-registry.oracle.com/database/free:latest

注意:Oracle 建议输入的密码应该至少有8个字符的长度,至少包含1个大写字母,1个小写字母和1个数字[0-9]。SYS、SYSTEM和PDBADMIN账户将使用同一个密码。

查看安装日志,

docker logs oracledb23c -f

--- output
(略)
Disconnected from Oracle Database 23c Free, Release 23.0.0.0.0 - Developer-Release
Version 23.2.0.0.0
The Oracle base remains unchanged with value /opt/oracle
The Oracle base remains unchanged with value /opt/oracle
#########################
DATABASE IS READY TO USE!
#########################
The following output is now a tail of the alert.log:

XDB initialized.
ALTER PLUGGABLE DATABASE FREEPDB1 SAVE STATE
Completed: ALTER PLUGGABLE DATABASE FREEPDB1 SAVE STATE
2023-04-08T01:36:03.608797+00:00
ALTER SYSTEM SET control_files='/opt/oracle/oradata/FREE/control01.ctl' SCOPE=SPFILE;
2023-04-08T01:36:03.625859+00:00
ALTER SYSTEM SET local_listener='' SCOPE=BOTH;
ALTER PLUGGABLE DATABASE FREEPDB1 SAVE STATE
Completed: ALTER PLUGGABLE DATABASE FREEPDB1 SAVE STATE
---

从容器内连接,

docker exec -it oracledb23c sqlplus / as sysdba
docker exec -it oracledb23c sqlplus sys/<your_password>@FREE as sysdba
docker exec -it oracledb23c sqlplus system/<your_password>@FREE
docker exec -it oracledb23c sqlplus pdbadmin/<your_password>@FREEPDB1

从容器外部连接,

# To connect to the database at the CDB$ROOT level as sysdba:
  $ sqlplus sys/<your password>@//localhost:<port mapped to 1521>/FREE as sysdba

# To connect as non sysdba at the CDB$ROOT level:
  $ sqlplus system/<your password>@//localhost:<port mapped to 1521>/FREE

# To connect to the default Pluggable Database (PDB) within the FREE Database:
  $ sqlplus pdbadmin/<your password>@//localhost:<port mapped to 1521>/FREEPDB1

为了后续使用方便,使用 sys 或者 system 用户连接到 FREEPDB1 之后,赋予 pdbadmin 用户 dba 的权限,

grant dba to pdbadmin;

4. 学习 SQLAlchemy 统一教程

SQLAlchemy 的新用户以应该从 SQLAlchemy 统一教程开始,其中涵盖了 Alchemist 在使用 ORM 或仅使用 Core 时需要了解的所有内容。

4-1. 安装依赖库

先通过 pip 安装 SQLAlchemy 库,

pip install SQLAlchemy

通过 Oracle Database 的 python-oracledb 库,python-oracledb 驱动程序允许 Python 3 应用程序连接到 Oracle 数据库。

pip install oracledb

参考文档:https://oracle.github.io/python-oracledb/

https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html

4-2. 建立连接 - 引擎

每个连接到数据库的 SQLAlchemy 应用程序都需要使用 Engine 。这个简短的部分适合所有人。

任何 SQLAlchemy 应用程序的开始都是一个名为 Engine 的对象。该对象充当特定数据库连接的中央源,为这些数据库连接提供工厂以及称为连接池的保存空间。该引擎通常是为特定数据库服务器仅创建一次的全局对象,并使用 URL 字符串进行配置,该字符串将描述它应如何连接到数据库主机或后端。

在本教程中,我们将使用 Oracle Database 23c 数据库。Engine 是使用 create_engine() 函数创建的:

from sqlalchemy import create_engine
engine = create_engine("oracle+oracledb://pdbadmin:<YOUR_PASSWORD>@<YOUR_IP>:1521?service_name=FREEPDB1", echo=True)

连接字符串 URL 格式说明:

oracle+oracledb://user:pass@hostname:port[/dbname][?service_name=<service>[&key=value&key=value...]]

create_engine 的主要参数是一个字符串 URL,该字符串向 Engine 指示三个重要事实:

  • 我们正在与什么样的数据库进行通信?这是上面的 oracle 部分,它在 SQLAlchemy 中链接到称为 dialect 的对象。
  • 我们使用什么 DBAPI? Python DBAPI 是 SQLAlchemy 用于与特定数据库交互的第三方驱动程序。在本例中,我们使用名称 oracledb。
  • 我们如何定位数据库呢?在本例中,我们的 URL 包含短语 ?service_name=FREEPDB1,指定我们使用 FREEPDB1 这个 PDB 库。

Lazy Connecting 惰性连接:

当 create_engine() 首次返回时, Engine 尚未真正尝试连接到数据库;这种情况仅在第一次被要求对数据库执行任务时发生。这是一种称为延迟初始化的软件设计模式。

4-3. 使用事务和 DBAPI

随着 Engine 对象准备就绪,我们现在可以继续深入了解 Engine 及其主要交互端点 Connection 和 Result 。我们还将额外介绍这些对象的 ORM facade,称为 Session 。

使用 ORM 时, Engine 由另一个名为 Session 的对象管理。现代 SQLAlchemy 中的 Session 强调事务和 SQL 执行模式,该模式与下面讨论的 Connection 基本相同,因此虽然本小节是以核心为中心的,但这里的所有概念本质上也与 ORM 使用相关,建议所有 ORM 学习者使用。 Connection 使用的执行模式将在本节末尾与 Session 的执行模式进行对比。

由于我们尚未介绍 SQLAlchemy 表达式语言(它是 SQLAlchemy 的主要功能),因此我们将使用此包中的一个简单构造,称为 text() 构造,它允许我们将 SQL 语句编写为文本 SQL 。请放心,对于大多数任务来说,日常 SQLAlchemy 使用中的文本 SQL 是例外,而不是规则,尽管它始终保持完全可用。

4-3-1. 获取连接

从面向用户的角度来看, Engine 对象的唯一目的是提供一个称为 Connection 的数据库连接单元。当直接使用 Core 时, Connection 对象是完成与数据库的所有交互的方式。由于 Connection 代表针对数据库的开放资源,因此我们希望始终将该对象的使用范围限制在特定上下文中,而最好的方法是使用 Python 上下文管理器形式,也称为 with 语句。下面我们使用文本 SQL 语句来说明"Hello World"。文本 SQL 是使用名为 text() 的构造发出的,稍后将更详细地讨论该构造:

先创建 engine,

from sqlalchemy import create_engine, text

# 创建引擎
engine = create_engine("oracle+oracledb://pdbadmin:<YOUR_PASSWORD>@<YOUR_IP>:1521?service_name=FREEPDB1", echo=True)

示例代码,

with engine.connect() as conn:
    result = conn.execute(text("select 'hello world'"))
    print(result.all())

输入结果如下,

2023-09-16 19:51:00,475 INFO sqlalchemy.engine.Engine select sys_context( 'userenv', 'current_schema' ) from dual
2023-09-16 19:51:00,476 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:51:00,481 INFO sqlalchemy.engine.Engine SELECT value FROM v$parameter WHERE name = 'compatible'
2023-09-16 19:51:00,481 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:51:00,490 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 19:51:00,490 INFO sqlalchemy.engine.Engine select 'hello world'
2023-09-16 19:51:00,491 INFO sqlalchemy.engine.Engine [generated in 0.00083s] {}
[('hello world',)]
2023-09-16 19:51:00,491 INFO sqlalchemy.engine.Engine ROLLBACK

在上面的示例中,上下文管理器提供了数据库连接,并且还构建了事务内部的操作。 Python DBAPI 的默认行为包括事务始终在进行中;当连接范围被释放时,会发出 ROLLBACK 来结束事务。事务不会自动提交;当我们想要提交数据时,我们通常需要调用 Connection.commit() ,我们将在下一节中看到。

"自动提交"模式可用于特殊情况。设置事务隔离级别(包括 DBAPI 自动提交)部分对此进行了讨论。

我们的 SELECT 的结果也在一个名为 Result 的对象中返回,稍后将讨论该对象,但是目前我们将添加最好确保该对象在"connect"块中使用,并且不会在我们的连接范围之外传递。

4-3-2. 提交更改

我们刚刚了解到 DBAPI 连接是非自动提交的。如果我们想提交一些数据怎么办?我们可以更改上面的示例来创建一个表并插入一些数据,然后使用 Connection.commit() 方法提交事务,该方法在我们获取 Connection 对象的块内调用:

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE some_table (x int, y int)"))
    conn.execute(
        text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
        [{"x": 1, "y": 1}, {"x": 2, "y": 4}],
    )
    conn.commit()

输出结果如下,

2023-09-16 19:53:42,935 INFO sqlalchemy.engine.Engine select sys_context( 'userenv', 'current_schema' ) from dual
2023-09-16 19:53:42,936 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:53:42,942 INFO sqlalchemy.engine.Engine SELECT value FROM v$parameter WHERE name = 'compatible'
2023-09-16 19:53:42,942 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:53:42,950 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 19:53:42,951 INFO sqlalchemy.engine.Engine CREATE TABLE some_table (x int, y int)
2023-09-16 19:53:42,951 INFO sqlalchemy.engine.Engine [generated in 0.00095s] {}
2023-09-16 19:53:42,959 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (:x, :y)
2023-09-16 19:53:42,959 INFO sqlalchemy.engine.Engine [generated in 0.00040s] [{'x': 1, 'y': 1}, {'x': 2, 'y': 4}]
2023-09-16 19:53:43,119 INFO sqlalchemy.engine.Engine COMMIT

上面,我们发出了两个通常是事务性的 SQL 语句,一个"CREATE TABLE"语句 [1] 和一个参数化的"INSERT"语句(上面的参数化语法将在下面的发送多个参数中讨论)。由于我们希望在块内提交已完成的工作,因此我们调用提交事务的 Connection.commit() 方法。在块内调用此方法后,我们可以继续运行更多 SQL 语句,如果我们选择,我们可以为后续语句再次调用 Connection.commit() 。 SQLAlchemy 将这种风格称为"随时提交"。

还有另一种提交数据的方式,即我们可以预先将"连接"块声明为事务块。对于这种操作模式,我们使用 Engine.begin() 方法来获取连接,而不是 Engine.connect() 方法。此方法将管理 Connection 的范围,并且还将事务内的所有内容包含在事务内,假设成功阻止,则在末尾使用 COMMIT,或者在引发异常时使用 ROLLBACK。这种风格可以称为开始一次:

with engine.begin() as conn:
    conn.execute(
        text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
        [{"x": 6, "y": 8}, {"x": 9, "y": 10}],
    )

输出结果如下,

2023-09-16 19:57:10,476 INFO sqlalchemy.engine.Engine select sys_context( 'userenv', 'current_schema' ) from dual
2023-09-16 19:57:10,476 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:57:10,483 INFO sqlalchemy.engine.Engine SELECT value FROM v$parameter WHERE name = 'compatible'
2023-09-16 19:57:10,483 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 19:57:10,491 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 19:57:10,492 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (:x, :y)
2023-09-16 19:57:10,492 INFO sqlalchemy.engine.Engine [generated in 0.00053s] [{'x': 6, 'y': 8}, {'x': 9, 'y': 10}]
2023-09-16 19:57:10,494 INFO sqlalchemy.engine.Engine COMMIT

"开始一次"风格通常是首选,因为它更简洁,并且预先表明了整个块的意图。然而,在本教程中,我们通常会使用"随心提交"风格,因为它对于演示目的更加灵活。

什么是"BEGIN(隐式)"?

您可能已经注意到事务块开头的日志行"BEGIN(隐式)"。这里的"隐式"是指SQLAlchemy实际上没有向数据库发送任何命令;它只是认为这是 DBAPI 隐式事务的开始。例如,您可以注册事件挂钩来拦截此事件。

DDL 是指 SQL 的子集,它指示数据库创建、修改或删除架构级构造(例如表)。建议将诸如"CREATE TABLE"之类的 DDL 放在以 COMMIT 结尾的事务块内,因为许多数据库使用事务性 DDL,以便在提交事务之前不会发生架构更改。然而,正如我们稍后将看到的,我们通常让 SQLAlchemy 为我们运行 DDL 序列,作为更高级别操作的一部分,我们通常不需要担心 COMMIT。

4-3-3. 语句执行的基础知识

我们已经看到了一些针对数据库运行 SQL 语句的示例,使用名为 Connection.execute() 的方法,结合名为 text() 的对象,并返回名为 Result 。在本节中,我们将更详细地说明这些组件的机制和交互。

本节中的大部分内容同样适用于使用 Session.execute() 方法时的现代 ORM 使用,该方法的工作方式与 Connection.execute() 非常相似,包括使用Core 使用相同的 Result 接口。

4-3-3-1. 获取行

我们首先将通过使用之前插入的行,在我们创建的表上运行文本 SELECT 语句来更仔细地说明 Result 对象:

with engine.connect() as conn:
    result = conn.execute(text("SELECT x, y FROM some_table"))
    for row in result:
        print(f"x: {row.x}  y: {row.y}")

输出结果如下,

2023-09-16 20:02:31,277 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:02:31,278 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table
2023-09-16 20:02:31,278 INFO sqlalchemy.engine.Engine [generated in 0.00102s] {}
x: 1  y: 1
x: 2  y: 4
x: 6  y: 8
x: 9  y: 10
2023-09-16 20:02:31,283 INFO sqlalchemy.engine.Engine ROLLBACK

上面,我们执行的"SELECT"字符串选择了表中的所有行。返回的对象称为 Result 并表示结果行的可迭代对象。

Result 有许多用于获取和转换行的方法,例如前面介绍的 Result.all() 方法,它返回所有 Row 对象的列表。它还实现了Python迭代器接口,以便我们可以直接迭代 Row 对象的集合。

Row 对象本身的作用类似于 Python 命名元组。下面我们举例说明访问行的多种方法。

  • 元组分配 - 这是最符合 Python 习惯的风格,即在收到变量时按位置将变量分配给每一行:

    result = conn.execute(text("select x, y from some_table"))
    
    for x, y in result:
        ...
    
  • 整数索引 - 元组是 Python 序列,因此也可以使用常规整数访问:

    result = conn.execute(text("select x, y from some_table"))
    
    for row in result:
        x = row[0]
    
  • 属性名称 - 由于这些是 Python 命名的元组,因此元组具有与每列名称匹配的动态属性名称。这些名称通常是 SQL 语句分配给每行中的列的名称。虽然它们通常是相当可预测的,并且也可以通过标签进行控制,但在定义较少的情况下,它们可能会受到特定于数据库的行为的影响:

    result = conn.execute(text("select x, y from some_table"))
    
    for row in result:
        y = row.y
    
        # illustrate use with Python f-strings
        print(f"Row: {row.x} {y}")
    
  • 映射访问 - 要接收行作为 Python 映射对象(本质上是 Python 通用 dict 对象接口的只读版本), Result 可以转换为 MappingResult 修饰符的 对象;这是一个结果对象,它生成类似字典的 RowMapping 对象而不是 Row 对象:

    result = conn.execute(text("select x, y from some_table"))
    
    for dict_row in result.mappings():
        x = dict_row["x"]
        y = dict_row["y"]
    
4-3-3-2: 发送参数

SQL 语句通常伴随着与语句本身一起传递的数据,正如我们在前面的 INSERT 示例中看到的那样。因此, Connection.execute() 方法也接受参数,这些参数称为绑定参数。一个基本的示例可能是,如果我们希望将 SELECT 语句限制为仅满足特定条件的行,例如"y"值大于传递给函数的特定值的行。

为了实现这一点,以便 SQL 语句可以保持固定并且驱动程序可以正确清理该值,我们在语句中添加一个 WHERE 条件,命名一个名为"y"的新参数; text() 构造使用冒号格式" :y "接受这些。然后," :y "的实际值以字典的形式作为第二个参数传递给 Connection.execute() :

with engine.connect() as conn:
    result = conn.execute(text("SELECT x, y FROM some_table WHERE y > :y"), {"y": 2})
    for row in result:
        print(f"x: {row.x}  y: {row.y}")

输出结果如下,

2023-09-16 20:07:23,153 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:07:23,153 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table WHERE y > :y
2023-09-16 20:07:23,154 INFO sqlalchemy.engine.Engine [generated in 0.00075s] {'y': 2}
x: 2  y: 4
x: 6  y: 8
x: 9  y: 10
2023-09-16 20:07:23,165 INFO sqlalchemy.engine.Engine ROLLBACK

在记录的 SQL 输出中,我们可以看到绑定参数 :y 在发送到 SQLite 数据库时被转换为问号。这是因为 SQLite 数据库驱动程序使用一种称为"qmark 参数样式"的格式,这是 DBAPI 规范允许的六种不同格式之一。 SQLAlchemy 将这些格式抽象为一种,即使用冒号的"命名"格式。

始终使用绑定参数:

正如本节开头提到的,文本 SQL 并不是我们使用 SQLAlchemy 的常用方式。然而,当使用文本 SQL 时,Python 文本值,即使是整数或日期等非字符串,也不应该直接字符串化为 SQL 字符串;应始终使用参数。这就是最著名的当数据不受信任时如何避免 SQL 注入攻击。然而,它还允许 SQLAlchemy 方言和/或 DBAPI 正确处理后端的传入输入。除了纯文本 SQL 用例之外,SQLAlchemy 的核心表达式 API 还确保 Python 文字值在适当的情况下作为绑定参数传递。

4-3-3-3. 发送多个参数

在提交更改的示例中,我们执行了一条 INSERT 语句,看起来我们能够一次将多行插入数据库。对于"INSERT"、"UPDATE"和"DELETE"等 DML 语句,我们可以通过传递字典列表而不是单个字典来向 Connection.execute() 方法发送多个参数集,这表明单个字典SQL 语句应调用多次,每个参数集调用一次。这种执行方式称为executemany:

with engine.connect() as conn:
    conn.execute(
        text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
        [{"x": 11, "y": 12}, {"x": 13, "y": 14}],
    )
    conn.commit()

输出结果如下,

2023-09-16 20:09:13,761 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:09:13,761 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (:x, :y)
2023-09-16 20:09:13,762 INFO sqlalchemy.engine.Engine [cached since 723.3s ago] [{'x': 11, 'y': 12}, {'x': 13, 'y': 14}]
2023-09-16 20:09:13,763 INFO sqlalchemy.engine.Engine COMMIT

上述操作相当于为每个参数集运行一次给定的 INSERT 语句,不同之处在于该操作将被优化以获得跨多行的更好性能。

"execute"和"executemany"之间的一个关键行为差异是后者不支持返回结果行,即使语句包含 RETURNING 子句。唯一的例外是使用核心 insert() 构造(本教程稍后在使用 INSERT 语句中介绍),它还指示使用 Insert.returning() 方法返回。在这种情况下,SQLAlchemy 使用特殊逻辑来重新组织 INSERT 语句,以便可以为许多行调用它,同时仍然支持 RETURNING。

也可以看看:

executemany - 在术语表中,描述了用于大多数"executemany"执行的 DBAPI 级别的cursor.executemany() 方法。

INSERT 语句的"插入多个值"行为 - 在使用引擎和连接中,描述了 Insert.returning() 用于通过"executemany"执行传递结果集的专用逻辑。

4-3-4. 使用 ORM 会话执行

如前所述,上面的大多数模式和示例也适用于 ORM,因此这里我们将介绍这种用法,以便随着教程的进行,我们将能够从 Core 和 ORM 一起使用的角度来说明每个模式。

使用 ORM 时的基本事务/数据库交互对象称为 Session 。在现代 SQLAlchemy 中,该对象的使用方式与 Connection 非常相似,事实上,在使用 Session 时,它指的是 Connection 在内部它用来发出 SQL。

当 Session 与非 ORM 构造一起使用时,它会传递我们给它的 SQL 语句,并且通常不会与 Connection 直接执行的操作有很大不同,因此我们可以这里用我们已经学过的简单文本 SQL 操作来说明它。

Session 有几种不同的创建模式,但在这里我们将说明最基本的模式,它精确跟踪 Connection 的使用方式,即在上下文管理器中构造它:

from sqlalchemy.orm import Session

stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y")
with Session(engine) as session:
    result = session.execute(stmt, {"y": 6})
    for row in result:
        print(f"x: {row.x}  y: {row.y}")

输出结果如下,

2023-09-16 20:12:26,727 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:12:26,727 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y
2023-09-16 20:12:26,729 INFO sqlalchemy.engine.Engine [generated in 0.00047s] {'y': 6}
x: 6  y: 8
x: 9  y: 10
x: 11  y: 12
x: 13  y: 14
2023-09-16 20:12:26,732 INFO sqlalchemy.engine.Engine ROLLBACK

上面的例子可以与上一节发送参数中的例子进行比较 - 我们直接用 with Session(engine) as session 替换对 with engine.connect() as conn 的调用,然后使用 Session.execute() 方法所做的那样。

此外,与 Connection 一样, Session 具有使用 Session.commit() 方法的"随用随付"行为,如下图所示,使用文本 UPDATE 语句来更改某些内容我们的数据:

with Session(engine) as session:
    result = session.execute(
        text("UPDATE some_table SET y=:y WHERE x=:x"),
        [{"x": 9, "y": 11}, {"x": 13, "y": 15}],
    )
    session.commit()

输出结果如下,

2023-09-16 20:14:15,791 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:14:15,792 INFO sqlalchemy.engine.Engine UPDATE some_table SET y=:y WHERE x=:x
2023-09-16 20:14:15,792 INFO sqlalchemy.engine.Engine [generated in 0.00035s] [{'y': 11, 'x': 9}, {'y': 15, 'x': 13}]
2023-09-16 20:14:15,804 INFO sqlalchemy.engine.Engine COMMIT

上面,我们使用发送多个参数中引入的绑定参数、"executemany"执行风格调用了 UPDATE 语句,并以"随时提交"提交结束该块。

Tip:

Session 在结束事务后实际上并不保留 Connection 对象。下次需要对数据库执行 SQL 时,它会从 Engine 获取新的 Connection 。
也可以看看:
使用会话的基础知识 - 介绍 Session 对象的基本创建和使用模式。

4-3-5. 使用数据库元数据

随着引擎和 SQL 执行的停止,我们准备开始一些 Alchemy。 SQLAlchemy Core 和 ORM 的核心元素是 SQL 表达式语言,它允许流畅、可组合地构建 SQL 查询。这些查询的基础是表示表和列等数据库概念的 Python 对象。这些对象统称为数据库元数据。

SQLAlchemy 中数据库元数据最常见的基础对象称为 MetaData 、 Table 和 Column 。下面的部分将说明如何在面向 Core 的风格和面向 ORM 的风格中使用这些对象。

ORM 读者,请关注我们!

与其他部分一样,Core 用户可以跳过 ORM 部分,但 ORM 用户最好从两个角度熟悉这些对象。这里讨论的 Table 对象在使用 ORM 时以更间接(也是完全 Python 类型)的方式声明,但是 ORM 的配置中仍然有一个 Table 对象。

4-3-5-1. 使用表对象设置元数据

当我们使用关系数据库时,我们查询的数据库中的基本数据保存结构称为表。在 SQLAlchemy 中,数据库"表"最终由一个类似名称 Table 的 Python 对象表示。

要开始使用 SQLAlchemy 表达式语言,我们需要构造 Table 对象来表示我们感兴趣的所有数据库表。 Table 以编程方式构造,可以直接使用 Table 构造函数,也可以间接使用 ORM 映射类(稍后将在使用 ORM 声明式表单定义表元数据中进行描述)。还可以选择从现有数据库加载部分或全部表信息,称为反射。

无论使用哪种方法,我们总是从一个集合开始,该集合将是我们放置表的位置,称为 MetaData 对象。该对象本质上是围绕 Python 字典的外观,该字典存储一系列以其字符串名称为键的 Table 对象。虽然 ORM 提供了一些关于从哪里获取此集合的选项,但我们始终可以选择直接创建一个集合,如下所示:

from sqlalchemy import MetaData
metadata_obj = MetaData()

一旦我们有了一个 MetaData 对象,我们就可以声明一些 Table 对象。本教程将从经典的 SQLAlchemy 教程模型开始,该模型有一个名为 user_account 的表,用于存储网站用户等信息,以及一个相关的表 address ,用于存储电子邮件与 user_account 表中的行关联的地址。当根本不使用 ORM 声明性模型时,我们直接构造每个 Table 对象,通常将每个对象分配给一个变量,这将是我们在应用程序代码中引用表的方式:

from sqlalchemy import Table, Column, Integer, String
user_table = Table(
    "user_account",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(30)),
    Column("fullname", String(100)),
)

在上面的示例中,当我们希望编写引用数据库中的 user_account 表的代码时,我们将使用 user_table Python 变量来引用它。

我什么时候在程序中创建 MetaData 对象?

整个应用程序有一个 MetaData 对象是最常见的情况,在应用程序中的单个位置表示为模块级变量,通常在"models"或"dbschema"类型的包中。通过以 ORM 为中心的 registry 或 Declarative Base 基类访问 MetaData 也是很常见的,因此相同的 MetaData 在 ORM 之间共享和核心声明的 Table 对象。

也可以有多个 MetaData 集合; Table 对象可以不受限制地引用其他集合中的 Table 对象。然而,对于彼此相关的 Table 对象组,实际上,从声明的角度来看,将它们设置在单个 MetaData 集合中要简单得多它们,以及从以正确顺序发出的 DDL(即 CREATE 和 DROP)语句的角度来看。

4-3-5-1-1. Table 的组成部分

我们可以观察到,用 Python 编写的 Table 结构与 SQL CREATE TABLE 语句非常相似;从表名开始,然后列出每一列,其中每一列都有一个名称和数据类型。上面我们使用的对象是:

  • Table - 表示数据库表并将其自身分配给 MetaData 集合。

  • Column - 表示数据库表中的列,并将其自身分配给 Table 对象。 Column 通常包含字符串名称和类型对象。就父 Table 而言的 Column 对象集合通常通过位于 Table.c 的关联数组进行访问:

    user_table.c.name
    
    user_table.c.keys()
    
  • Integer 、 String - 这些类表示 SQL 数据类型,并且可以传递给 Column ,无论是否需要实例化。上面,我们想要为"name"列指定长度"30",因此我们实例化了 String(30) 。但是对于"id"和"fullname"我们没有指定这些,所以我们可以发送类本身。

也可以看看:

MetaData 、 Table 和 Column 的参考和 API 文档位于使用元数据描述数据库。数据类型的参考文档位于 SQL 数据类型对象。

在接下来的部分中,我们将说明 Table 的基本功能之一,即在特定数据库连接上生成 DDL。但首先我们将声明第二个 Table 。

4-3-5-1-2. 声明简单约束

示例 user_table 中的第一个 Column 包含 Column.primary_key 参数,该参数是一种简写技术,指示此 Column 应该是该表的主键。主键本身通常是隐式声明的,并由 PrimaryKeyConstraint 构造表示,我们可以在 Table 对象的 Table.primary_key 属性上看到它:

user_table.primary_key

最典型地显式声明的约束是与数据库外键约束相对应的 ForeignKeyConstraint 对象。当我们声明彼此相关的表时,SQLAlchemy 使用这些外键约束声明的存在,不仅使它们在 CREATE 语句中发送到数据库,而且还帮助构建 SQL 表达式。

仅涉及目标表上单个列的 ForeignKeyConstraint 通常是通过 ForeignKey 对象使用列级速记符号来声明的。下面我们声明第二个表 address ,它将具有引用 user 表的外键约束:

from sqlalchemy import ForeignKey
address_table = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", ForeignKey("user_account.id"), nullable=False),
    Column("email_address", String(100), nullable=False),
)

上表还具有第三种约束,在 SQL 中是"NOT NULL"约束,上面使用 Column.nullable 参数表示。

Tip:

当在 Column 定义中使用 ForeignKey 对象时,我们可以省略该 Column 的数据类型;它是从相关列的数据类型自动推断出来的,在上面的示例中是 user_account.id 列的 Integer 数据类型。

在下一节中,我们将为 user 和 address 表发出完整的 DDL 以查看完整的结果。

4-3-5-1-3. 向数据库发送 DDL

我们构建了一个对象结构,表示数据库中的两个数据库表,从根 MetaData 对象开始,然后到两个 Table 对象,每个对象都保存 < 的集合b2> 和 Constraint 对象。这个对象结构将成为我们未来使用 Core 和 ORM 执行的大多数操作的中心。

我们可以使用此结构做的第一个有用的事情是向我们的 SQLite 数据库发出 CREATE TABLE 语句或 DDL,以便我们可以从中插入和查询数据。我们已经拥有执行此操作所需的所有工具,通过调用 MetaData 上的 MetaData.create_all() 方法,向其发送引用目标数据库的 Engine :

metadata_obj.create_all(engine)

输出结果如下,

2023-09-16 20:27:24,738 INFO sqlalchemy.engine.Engine select sys_context( 'userenv', 'current_schema' ) from dual
2023-09-16 20:27:24,738 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 20:27:24,746 INFO sqlalchemy.engine.Engine SELECT value FROM v$parameter WHERE name = 'compatible'
2023-09-16 20:27:24,746 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 20:27:24,754 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:27:24,757 INFO sqlalchemy.engine.Engine SELECT tables_and_views.table_name 
FROM (SELECT a_tables.table_name AS table_name, a_tables.owner AS owner 
FROM all_tables a_tables UNION ALL SELECT a_views.view_name AS table_name, a_views.owner AS owner 
FROM all_views a_views) tables_and_views 
WHERE tables_and_views.table_name = :table_name AND tables_and_views.owner = :owner
2023-09-16 20:27:24,757 INFO sqlalchemy.engine.Engine [generated in 0.00048s] {'table_name': 'USER_ACCOUNT', 'owner': 'PDBADMIN'}
2023-09-16 20:27:24,760 INFO sqlalchemy.engine.Engine SELECT tables_and_views.table_name 
FROM (SELECT a_tables.table_name AS table_name, a_tables.owner AS owner 
FROM all_tables a_tables UNION ALL SELECT a_views.view_name AS table_name, a_views.owner AS owner 
FROM all_views a_views) tables_and_views 
WHERE tables_and_views.table_name = :table_name AND tables_and_views.owner = :owner
2023-09-16 20:27:24,760 INFO sqlalchemy.engine.Engine [cached since 0.003355s ago] {'table_name': 'ADDRESS', 'owner': 'PDBADMIN'}
2023-09-16 20:27:24,761 INFO sqlalchemy.engine.Engine 
CREATE TABLE address (
	id INTEGER NOT NULL, 
	user_id INTEGER NOT NULL, 
	email_address VARCHAR2(100 CHAR) NOT NULL, 
	PRIMARY KEY (id), 
	FOREIGN KEY(user_id) REFERENCES user_account (id)
)


2023-09-16 20:27:24,762 INFO sqlalchemy.engine.Engine [no key 0.00033s] {}
2023-09-16 20:27:24,772 INFO sqlalchemy.engine.Engine COMMIT

上面的 DDL 创建过程包括一些特定于 SQLite 的 PRAGMA 语句,这些语句在发出 CREATE 之前测试每个表是否存在。完整的一系列步骤也包含在 BEGIN/COMMIT 对中,以适应事务性 DDL。

创建过程还负责以正确的顺序发出 CREATE 语句;上面,FOREIGN KEY 约束依赖于现有的 user 表,因此首先创建 address 表。在更复杂的依赖场景中,FOREIGN KEY 约束也可以在事后使用 ALTER 应用于表。

MetaData 对象还具有 MetaData.drop_all() 方法,该方法将以相反的顺序发出 DROP 语句,因为它会发出 CREATE 来删除架构元素。

迁移工具通常是合适的:

总体而言, MetaData 的 CREATE / DROP 功能对于测试套件、小型和/或新应用程序以及使用短期数据库的应用程序非常有用。然而,对于应用程序数据库模式的长期管理,模式管理工具(例如基于 SQLAlchemy 构建的 Alembic)可能是更好的选择,因为它可以管理和编排随着时间的推移逐步更改固定数据库模式的过程,例如应用程序的设计发生变化。

4-3-6. 使用 ORM 声明式形式定义表元数据

制作 Table 对象的另一种方法?

前面的示例说明了 Table 对象的直接使用,它是 SQLAlchemy 在构造 SQL 表达式时最终如何引用数据库表的基础。如前所述,SQLAlchemy ORM 围绕 Table 声明过程(称为声明表)提供了一个外观。声明性表过程实现了与上一节中相同的目标,即构建 Table 对象,但也在该过程中为我们提供了其他称为 ORM 映射类的东西,或者简称为"映射类"。映射类是使用 ORM 时最常见的 SQL 基础单元,在现代 SQLAlchemy 中也可以非常有效地以 Core 为中心的使用。

使用声明性表的一些好处包括:

  • 设置列定义的更简洁和 Python 风格,其中 Python 类型可用于表示要在数据库中使用的 SQL 类型

  • 生成的映射类可用于形成 SQL 表达式,这些表达式在许多情况下维护由静态分析工具(例如 Mypy 和 IDE 类型检查器)获取的 PEP 484 类型信息

  • 允许同时声明表元数据和持久化/对象加载操作中使用的 ORM 映射类。

本节将说明使用声明性表构建的与上一节相同的 Table 元数据。

使用ORM时,我们声明 Table 元数据的过程通常与声明映射类的过程结合在一起。映射的类是我们想要创建的任何 Python 类,该类将具有链接到数据库表中的列的属性。虽然实现方式有多种,但最常见的样式称为声明式,它允许我们立即声明用户定义的类和 Table 元数据。

4-3-6-1. 建立声明性基础

使用 ORM 时, MetaData 集合仍然存在,但它本身与通常称为声明性基础的仅 ORM 构造相关联。获取新的声明性基础的最便捷方法是创建一个新类,该类是 SQLAlchemy DeclarativeBase 类的子类:

from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
    pass

上面的 Base 类就是我们所说的声明性基础类。当我们创建作为 Base 子类的新类并结合适当的类级指令时,它们将在类创建时被建立为新的 ORM 映射类,每个类通常(但不排他)引用到特定的 Table 对象。

声明性基础指的是自动为我们创建的 MetaData 集合(假设我们没有从外部提供集合)。此 MetaData 集合可通过 DeclarativeBase.metadata 类级属性访问。当我们创建新的映射类时,它们每个都将引用此 MetaData 集合中的 Table :

Base.metadata

声明性基础还引用了一个名为 registry 的集合,它是 SQLAlchemy ORM 中的中央"映射器配置"单元。虽然很少直接访问,但该对象是映射器配置过程的核心,因为一组 ORM 映射类将通过此注册表相互协调。与 MetaData 的情况一样,我们的声明性基础也为我们创建了一个 registry (同样带有传递我们自己的 registry 的选项),我们可以通过 DeclarativeBase.registry 类变量:

Base.registry

使用 registry 进行映射的其他方法:

DeclarativeBase 不是映射类的唯一方法,只是最常见的。 registry 还提供其他映射器配置模式,包括面向装饰器和命令式映射类的方法。还完全支持在映射时创建 Python 数据类。 ORM 映射类配置的参考文档包含了所有内容。

4-3-6-2. 声明映射类

建立 Base 类后,我们现在可以根据新类 User 和 user_account 和 address 表定义 ORM 映射类 Address 。我们在下面说明了最现代的声明式形式,它是使用特殊类型 Mapped 从 PEP 484 类型注释驱动的,该类型指示要映射为特定类型的属性:

from typing import List
from typing import Optional
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

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]] = mapped_column(String(100))
    addresses: Mapped[List["Address"]] = relationship(back_populates="user")
    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] = mapped_column(String(100))
    user_id = 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})"

上面的两个类 User 和 Address 现在被称为 ORM 映射类,可用于 ORM 持久化和查询操作,稍后将对此进行描述。有关这些类的详细信息包括:

  • 每个类都引用作为声明性映射过程的一部分生成的 Table 对象,该对象通过将字符串分配给 DeclarativeBase.tablename 属性来命名。创建类后,可以从 DeclarativeBase.table 属性获取生成的 Table 。

  • 如前所述,这种形式称为声明性表配置。几种替代声明样式之一将让我们直接构建 Table 对象,并将其直接分配给 DeclarativeBase.table 。这种风格称为带有命令表的声明式。

  • 为了指示 Table 中的列,我们使用 mapped_column() 构造,并结合基于 Mapped 类型的键入注释。该对象将生成应用于 Table 构造的 Column 对象。

  • 对于具有简单数据类型且没有其他选项的列,我们可以单独指示 Mapped 类型注释,使用简单的 Python 类型(例如 int 和 str 来表示 Integer 和 String 。在声明性映射过程中自定义 Python 类型的解释方式是非常开放的;请参阅使用带注释的声明表(mapped_column() 的类型注释形式)和自定义背景类型映射部分。

  • 可以根据 Optional[] 类型注释(或其等效项 | None 或 Union[, None] )的存在将列声明为"可为空"或"非空" 。 mapped_column.nullable 参数也可以显式使用(并且不必与注释的可选性匹配)。

  • 显式类型注释的使用是完全可选的。我们也可以使用不带注释的 mapped_column() 。使用此形式时,我们将根据每个 mapped_column() 构造中的需要使用更显式的类型对象,例如 Integer 和 String 以及 nullable=False 。

  • 两个附加属性 User.addresses 和 Address.user 定义了一种名为 relationship() 的不同类型的属性,其具有类似的注释感知配置样式,如图所示。 relationship() 构造在使用 ORM 相关对象中进行了更全面的讨论。

  • 如果我们不声明自己的方法,这些类会自动获得 init() 方法。此方法的默认形式接受所有属性名称作为可选关键字参数:

    sandy = User(name="sandy", fullname="Sandy Cheeks")
    

    要自动生成提供位置参数以及具有默认关键字值的参数的全功能 init () 方法,可以使用声明性数据类映射中引入的数据类功能。当然,也可以选择使用显式 init() 方法。

  • 添加 repr () 方法以便我们获得可读的字符串输出;这些方法不需要在这里。与 init () 的情况一样,可以使用数据类功能自动生成 repr() 方法。

4-3-6-3. 从 ORM 映射向数据库发送 DDL

由于我们的 ORM 映射类引用 MetaData 集合中包含的 Table 对象,因此在给定声明性基础的情况下发出 DDL 使用与之前在向数据库发出 DDL 中描述的相同过程。在我们的例子中,我们已经在 SQLite 数据库中生成了 user 和 address 表。如果我们还没有这样做,我们可以通过从 DeclarativeBase.metadata 访问集合来自由地使用与 ORM 声明性基类关联的 MetaData 来执行此操作。属性,然后像以前一样使用 MetaData.create_all() 。在这种情况下,运行 PRAGMA 语句,但不会生成新表,因为发现它们已经存在:

Base.metadata.create_all(engine)

输出结果如下,

2023-09-16 20:38:53,836 INFO sqlalchemy.engine.Engine select sys_context( 'userenv', 'current_schema' ) from dual
2023-09-16 20:38:53,837 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 20:38:53,841 INFO sqlalchemy.engine.Engine SELECT value FROM v$parameter WHERE name = 'compatible'
2023-09-16 20:38:53,842 INFO sqlalchemy.engine.Engine [raw sql] {}
2023-09-16 20:38:53,849 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 20:38:53,852 INFO sqlalchemy.engine.Engine SELECT tables_and_views.table_name 
FROM (SELECT a_tables.table_name AS table_name, a_tables.owner AS owner 
FROM all_tables a_tables UNION ALL SELECT a_views.view_name AS table_name, a_views.owner AS owner 
FROM all_views a_views) tables_and_views 
WHERE tables_and_views.table_name = :table_name AND tables_and_views.owner = :owner
2023-09-16 20:38:53,853 INFO sqlalchemy.engine.Engine [generated in 0.00053s] {'table_name': 'USER_ACCOUNT', 'owner': 'PDBADMIN'}
2023-09-16 20:38:53,855 INFO sqlalchemy.engine.Engine SELECT tables_and_views.table_name 
FROM (SELECT a_tables.table_name AS table_name, a_tables.owner AS owner 
FROM all_tables a_tables UNION ALL SELECT a_views.view_name AS table_name, a_views.owner AS owner 
FROM all_views a_views) tables_and_views 
WHERE tables_and_views.table_name = :table_name AND tables_and_views.owner = :owner
2023-09-16 20:38:53,855 INFO sqlalchemy.engine.Engine [cached since 0.003387s ago] {'table_name': 'ADDRESS', 'owner': 'PDBADMIN'}
2023-09-16 20:38:53,857 INFO sqlalchemy.engine.Engine 
CREATE TABLE user_account (
	id INTEGER NOT NULL, 
	name VARCHAR2(30 CHAR) NOT NULL, 
	fullname VARCHAR2(100 CHAR), 
	PRIMARY KEY (id)
)


2023-09-16 20:38:53,857 INFO sqlalchemy.engine.Engine [no key 0.00032s] {}
2023-09-16 20:38:53,868 INFO sqlalchemy.engine.Engine 
CREATE TABLE address (
	id INTEGER NOT NULL, 
	email_address VARCHAR2(100 CHAR) NOT NULL, 
	user_id INTEGER, 
	PRIMARY KEY (id), 
	FOREIGN KEY(user_id) REFERENCES user_account (id)
)


2023-09-16 20:38:53,869 INFO sqlalchemy.engine.Engine [no key 0.00051s] {}
2023-09-16 20:38:53,877 INFO sqlalchemy.engine.Engine COMMIT

4-3-7. 表反射

本节只是简单介绍表反射的相关主题,或者如何从现有数据库自动生成 Table 对象。想要继续编写查询的教程读者可以随意跳过本节。

为了完善有关使用表元数据的部分,我们将说明本节开头提到的另一个操作,即表反射。表反射是指通过读取数据库当前状态生成 Table 及相关对象的过程。在前面的部分中,我们已经在 Python 中声明了 Table 对象,然后我们可以选择向数据库发出 DDL 以生成这样的模式,而反射过程则相反地执行这两个步骤,从从现有数据库生成 Python 数据结构来表示该数据库中的模式。

Tip:

不要求必须使用反射才能将 SQLAlchemy 与预先存在的数据库一起使用。 SQLAlchemy 应用程序在 Python 中显式声明所有元数据是完全典型的,因此其结构与现有数据库相对应。元数据结构也不需要包括本地应用程序运行不需要的预先存在的数据库中的表、列或其他约束和构造。

作为反射的示例,我们将创建一个新的 Table 对象,它代表我们在本文档前面部分中手动创建的 some_table 对象。执行方式也有多种,但最基本的是构造一个 Table 对象,给定表的名称和它所属的 MetaData 集合,然后,不要指示单独的 Column 和 Constraint 对象,而是使用 Table.autoload_with 参数将其传递给目标 Engine :

some_table = Table("some_table", metadata_obj, autoload_with=engine)

输出结果如下,

2023-09-16 20:42:00,309 INFO sqlalchemy.engine.Engine SELECT a_tab_cols.table_name, a_tab_cols.column_name, a_tab_cols.data_type, a_tab_cols.char_length, a_tab_cols.data_precision, a_tab_cols.data_scale, a_tab_cols.nullable, a_tab_cols.data_default, a_col_comments.comments, a_tab_cols.virtual_column, a_tab_cols.default_on_null, CASE WHEN (a_tab_identity_cols.table_name IS NULL) THEN NULL ELSE a_tab_identity_cols.generation_type || :generation_type_1 || a_tab_identity_cols.identity_options END AS identity_options 
FROM all_tab_cols a_tab_cols LEFT OUTER JOIN all_col_comments a_col_comments ON a_tab_cols.table_name = a_col_comments.table_name AND a_tab_cols.column_name = a_col_comments.column_name AND a_tab_cols.owner = a_col_comments.owner LEFT OUTER JOIN all_tab_identity_cols a_tab_identity_cols ON a_tab_cols.table_name = a_tab_identity_cols.table_name AND a_tab_cols.column_name = a_tab_identity_cols.column_name AND a_tab_cols.owner = a_tab_identity_cols.owner 
WHERE a_tab_cols.table_name IN (:all_objects_1) AND a_tab_cols.hidden_column = :hidden_column_1 AND a_tab_cols.owner = :owner_1 ORDER BY a_tab_cols.table_name, a_tab_cols.column_id
2023-09-16 20:42:00,310 INFO sqlalchemy.engine.Engine [generated in 0.00106s] {'generation_type_1': ',', 'hidden_column_1': 'NO', 'owner_1': 'PDBADMIN', 'all_objects_1': 'SOME_TABLE'}
2023-09-16 20:42:00,464 INFO sqlalchemy.engine.Engine SELECT a_objects.object_name 
FROM all_objects a_objects 
WHERE a_objects.owner = :owner_1 AND a_objects.object_type IN (:object_type_1_1, :object_type_1_2) AND a_objects.object_name IN (:filter_names_1)
2023-09-16 20:42:00,464 INFO sqlalchemy.engine.Engine [generated in 0.00053s] {'owner_1': 'PDBADMIN', 'object_type_1_1': 'TABLE', 'object_type_1_2': 'VIEW', 'filter_names_1': 'SOME_TABLE'}
2023-09-16 20:42:00,505 INFO sqlalchemy.engine.Engine SELECT a_constraints.table_name, a_constraints.constraint_type, a_constraints.constraint_name, local.column_name AS local_column, remote.table_name AS remote_table, remote.column_name AS remote_column, remote.owner AS remote_owner, a_constraints.search_condition, a_constraints.delete_rule 
FROM all_constraints a_constraints JOIN all_cons_columns local ON local.owner = a_constraints.owner AND a_constraints.constraint_name = local.constraint_name LEFT OUTER JOIN all_cons_columns remote ON a_constraints.r_owner = remote.owner AND a_constraints.r_constraint_name = remote.constraint_name AND (remote.position IS NULL OR local.position = remote.position) 
WHERE a_constraints.owner = :owner_1 AND a_constraints.table_name IN (:all_objects_1) AND a_constraints.constraint_type IN (:constraint_type_1_1, :constraint_type_1_2, :constraint_type_1_3, :constraint_type_1_4) ORDER BY a_constraints.constraint_name, local.position
2023-09-16 20:42:00,505 INFO sqlalchemy.engine.Engine [generated in 0.00052s] {'owner_1': 'PDBADMIN', 'all_objects_1': 'SOME_TABLE', 'constraint_type_1_1': 'R', 'constraint_type_1_2': 'P', 'constraint_type_1_3': 'U', 'constraint_type_1_4': 'C'}
2023-09-16 20:42:01,043 INFO sqlalchemy.engine.Engine SELECT a_ind_columns.table_name, a_ind_columns.index_name, a_ind_columns.column_name, a_indexes.index_type, a_indexes.uniqueness, a_indexes.compression, a_indexes.prefix_length, a_ind_columns.descend, a_ind_expressions.column_expression 
FROM all_ind_columns a_ind_columns JOIN all_indexes a_indexes ON a_ind_columns.index_name = a_indexes.index_name AND a_ind_columns.index_owner = a_indexes.owner LEFT OUTER JOIN all_ind_expressions a_ind_expressions ON a_ind_expressions.index_name = a_ind_columns.index_name AND a_ind_expressions.index_owner = a_ind_columns.index_owner AND a_ind_expressions.column_position = a_ind_columns.column_position 
WHERE a_indexes.table_owner = :table_owner_1 AND a_indexes.table_name IN (:all_objects_1) ORDER BY a_ind_columns.index_name, a_ind_columns.column_position
2023-09-16 20:42:01,044 INFO sqlalchemy.engine.Engine [generated in 0.00056s] {'table_owner_1': 'PDBADMIN', 'all_objects_1': 'SOME_TABLE'}
2023-09-16 20:42:01,221 INFO sqlalchemy.engine.Engine SELECT tables_and_views.table_name, tables_and_views.comments 
FROM (SELECT a_tab_comments.table_name AS table_name, a_tab_comments.comments AS comments 
FROM all_tab_comments a_tab_comments 
WHERE a_tab_comments.owner = :owner_1 AND a_tab_comments.table_name NOT LIKE :table_name_1 UNION ALL SELECT a_mview_comments.mview_name AS table_name, a_mview_comments.comments AS comments 
FROM all_mview_comments a_mview_comments 
WHERE a_mview_comments.owner = :owner_2 AND a_mview_comments.mview_name NOT LIKE :mview_name_1) tables_and_views 
WHERE tables_and_views.table_name IN (:filter_names_1)
2023-09-16 20:42:01,222 INFO sqlalchemy.engine.Engine [generated in 0.00059s] {'owner_1': 'PDBADMIN', 'table_name_1': 'BIN$%', 'owner_2': 'PDBADMIN', 'mview_name_1': 'BIN$%', 'filter_names_1': 'SOME_TABLE'}
2023-09-16 20:42:01,324 INFO sqlalchemy.engine.Engine SELECT a_tables.table_name, a_tables.compression, a_tables.compress_for 
FROM all_tables a_tables 
WHERE a_tables.owner = :owner_1 AND a_tables.table_name IN (:filter_names_1)
2023-09-16 20:42:01,324 INFO sqlalchemy.engine.Engine [generated in 0.00093s] {'owner_1': 'PDBADMIN', 'filter_names_1': 'SOME_TABLE'}
2023-09-16 20:42:01,537 INFO sqlalchemy.engine.Engine SELECT a_views.view_name 
FROM all_views a_views 
WHERE a_views.owner = :owner_1
2023-09-16 20:42:01,538 INFO sqlalchemy.engine.Engine [generated in 0.00075s] {'owner_1': 'PDBADMIN'}
2023-09-16 20:42:01,559 INFO sqlalchemy.engine.Engine ROLLBACK

在该过程结束时, some_table 对象现在包含有关表中存在的 Column 对象的信息,并且该对象的使用方式与 Table 我们明确声明:

some_table

输出结果如下,

Table('some_table', MetaData(), Column('x', INTEGER(), table=<some_table>), Column('y', INTEGER(), table=<some_table>), schema=None)

现在,我们已经准备好了一个 Oracle 数据库,其中包含两个表,以及 Core 和 ORM 面向表的构造,我们可以通过 Connection 和/或 ORM Session使用它们。 在以下部分中,我们将说明如何使用这些结构创建、操作和选择数据。

4-4. 处理数据

在使用事务和 DBAPI 中,我们学习了如何与 Python DBAPI 及其事务状态交互的基础知识。然后,在使用数据库元数据中,我们学习了如何使用 MetaData 和相关对象在 SQLAlchemy 中表示数据库表、列和约束。在本节中,我们将结合上述两个概念来创建、选择和操作关系数据库中的数据。我们与数据库的交互始终以事务的形式进行,即使我们已将数据库驱动程序设置为在幕后使用自动提交。

本节的组成部分如下:

  • 使用 INSERT 语句 - 为了将一些数据输入数据库,我们介绍并演示了核心 Insert 构造。下一节使用 ORM 进行数据操作将描述从 ORM 角度进行的 INSERT。

  • 使用 SELECT 语句 - 本节将详细描述 Select 构造,它是 SQLAlchemy 中最常用的对象。 Select 构造为以 Core 和 ORM 为中心的应用程序发出 SELECT 语句,这两个用例将在此处进行描述。后面的"在查询中使用关系"部分以及"ORM 查询指南"中还介绍了其他 ORM 用例。

  • 使用 UPDATE 和 DELETE 语句 - 完善数据的 INSERT 和 SELECTion,本节将从核心角度描述 Update 和 Delete 结构的使用。 ORM 特定的 UPDATE 和 DELETE 在"使用 ORM 进行数据操作"部分中进行了类似的描述。

4-4-1. 使用 INSERT 语句

当使用 Core 以及使用 ORM 进行批量操作时,会使用 insert() 函数直接生成 SQL INSERT 语句 - 该函数生成一个 Insert 的新实例,它代表一个 INSERT SQL 语句,将新数据添加到表中。

本节详细介绍了生成单个 SQL INSERT 语句以向表中添加新行的核心方法。使用 ORM 时,我们通常使用另一个在此基础上运行的工具,称为工作单元,它将自动同时生成许多 INSERT 语句。然而,即使 ORM 为我们运行,了解 Core 如何处理数据创建和操作也非常有用。此外,ORM 支持直接使用 INSERT,使用称为批量/多行 INSERT、upsert、UPDATE 和 DELETE 的功能。

要直接跳到如何使用正常工作单元模式通过 ORM INSERT 行,请参阅使用 ORM 工作单元模式插入行。

4-4-1-1. insert() SQL 表达式结构

Insert 的简单示例同时说明了目标表和 VALUES 子句:

from sqlalchemy import insert
stmt = insert(user_table).values(id=3, name="spongebob", fullname="Spongebob Squarepants")

上面的 stmt 变量是 Insert 的实例。大多数 SQL 表达式都可以就地进行字符串化,作为查看所生成内容的一般形式的一种方法:

print(stmt)

输出结果如下,

INSERT INTO user_account (id, name, fullname) VALUES (:id, :name, :fullname)

字符串化形式是通过生成对象的 Compiled 形式来创建的,其中包括语句的特定于数据库的字符串 SQL 表示形式;我们可以使用 ClauseElement.compile() 方法直接获取这个对象:

compiled = stmt.compile()

我们的 Insert 构造是"参数化"构造的一个示例,如之前在发送参数中所示;要查看 name 和 fullname 绑定参数,这些参数也可从 Compiled 构造中获取:

compiled.params

输出结果如下,

{'id': 3, 'name': 'spongebob', 'fullname': 'Spongebob Squarepants'}
4-4-1-2. 执行语句

调用该语句,我们可以将一行插入到 user_table 中。 INSERT SQL 以及捆绑的参数可以在 SQL 日志记录中看到:

with engine.connect() as conn:
    result = conn.execute(stmt)
    conn.commit()

输出结果如下,

2023-09-16 21:09:58,974 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 21:09:58,974 INFO sqlalchemy.engine.Engine INSERT INTO user_account (id, name, fullname) VALUES (:id, :name, :fullname)
2023-09-16 21:09:58,974 INFO sqlalchemy.engine.Engine [generated in 0.00099s] {'id': 3, 'name': 'spongebob', 'fullname': 'Spongebob Squarepants'}
2023-09-16 21:09:58,977 INFO sqlalchemy.engine.Engine COMMIT

在上面的简单形式中,INSERT 语句不返回任何行,如果只插入一行,它通常会包含返回有关该行 INSERT 期间生成的列级默认值的信息的功能,大多数通常是整数主键值。在上述情况下,SQLite 数据库中的第一行通常会返回 1 作为第一个整数主键值,我们可以使用 CursorResult.inserted_primary_key 访问器获取该值:

result.inserted_primary_key

输出结果如下,

(3,)

Tip:

CursorResult.inserted_primary_key 返回一个元组,因为主键可能包含多个列。这称为复合主键。 CursorResult.inserted_primary_key 旨在始终包含刚刚插入的记录的完整主键,而不仅仅是"cursor.lastrowid"类型的值,并且还旨在无论是否"自动增量"都进行填充被使用,因此为了表达完整的主键,它是一个元组。

4-4-1-3. INSERT 通常会自动生成"values"子句

上面的示例使用 Insert.values() 方法显式创建 SQL INSERT 语句的 VALUES 子句。如果我们实际上不使用 Insert.values() 而只是打印出一个"空"语句,我们就会为表中的每一列获得一个 INSERT:

print(insert(user_table))

输出结果如下,

INSERT INTO user_account (id, name, fullname) VALUES (:id, :name, :fullname)

如果我们采用一个尚未调用 Insert.values() 的 Insert 构造并执行它而不是打印它,则该语句将根据我们传递给的参数编译为字符串 Connection.execute() 方法,并且仅包含与传递的参数相关的列。这实际上是使用 Insert 插入行而无需键入显式 VALUES 子句的常用方法。下面的示例说明了一次使用参数列表执行的两列 INSERT 语句:

with engine.connect() as conn:
    result = conn.execute(
        insert(user_table),
        [
            {"id": 4, "name": "sandy", "fullname": "Sandy Cheeks"},
            {"id": 5, "name": "patrick", "fullname": "Patrick Star"},
        ],
    )
    conn.commit()

输出结果如下,

2023-09-16 21:14:38,806 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 21:14:38,807 INFO sqlalchemy.engine.Engine INSERT INTO user_account (id, name, fullname) VALUES (:id, :name, :fullname)
2023-09-16 21:14:38,807 INFO sqlalchemy.engine.Engine [generated in 0.00084s] [{'id': 4, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}, {'id': 5, 'name': 'patrick', 'fullname': 'Patrick Star'}]
2023-09-16 21:14:38,816 INFO sqlalchemy.engine.Engine COMMIT

上面的执行采用"executemany"形式,首先在发送多个参数中进行说明,但与使用 text() 构造时不同,我们不必拼写任何 SQL。通过将字典或字典列表与 Insert 构造一起传递给 Connection.execute() 方法, Connection 确保传递的列名将被表达自动在 Insert 构造的 VALUES 子句中。

深层炼金术:

大家好,欢迎来到《深度炼金术》第一版。左边的人被称为炼金术士,你会注意到他们不是巫师,因为尖顶的帽子没有向上翘。 Alchemist 来描述通常更高级和/或棘手且通常不需要的东西,但无论出于何种原因,他们认为您应该了解 SQLAlchemy 可以做的事情。

在此版本中,为了在 address_table 中包含一些有趣的数据,下面是一个更高级的示例,说明如何同时显式使用 Insert.values() 方法包括从参数生成的附加值。使用下一节中介绍的 select() 构造构造标量子查询,并使用显式绑定参数名称来设置子查询中使用的参数,该参数名称是使用 bindparam() 构建。

这是一些更深层次的炼金术,以便我们可以添加相关行,而无需从 user_table 操作中将主键标识符获取到应用程序中。大多数炼金术士只会使用 ORM 来为我们处理类似的事情。

from sqlalchemy import select, bindparam
scalar_subq = (
   select(user_table.c.id)
   .where(user_table.c.name == bindparam("username"))
   .scalar_subquery()
)

with engine.connect() as conn:
   result = conn.execute(
       insert(address_table).values(user_id=scalar_subq),
       [
           {
               "username": "spongebob",
               "email_address": "spongebob@sqlalchemy.org",
               "id": 3
           },
           {"username": "sandy", "email_address": "sandy@sqlalchemy.org", "id": 4},
           {"username": "sandy", "email_address": "sandy@squirrelpower.org", "id": 5},
       ],
   )
   conn.commit()

这样,我们的表中就有了一些更有趣的数据,我们将在接下来的部分中使用它们。

输出结果如下,

2023-09-16 23:18:46,390 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 23:18:46,390 INFO sqlalchemy.engine.Engine INSERT INTO address (id, user_id, >email_address) VALUES (:id, (SELECT user_account.id 
FROM user_account 
WHERE user_account.name = :username), :email_address)
2023-09-16 23:18:46,391 INFO sqlalchemy.engine.Engine [generated in 0.00096s] [{'id': 3, >'username': 'spongebob', 'email_address': 'spongebob@sqlalchemy.org'}, {'id': 4, 'username': >'sandy', 'email_address': 'sandy@sqlalchemy.org'}, {'id': 5, 'username': 'sandy', 'email_address': >'sandy@squirrelpower.org'}]
2023-09-16 23:18:46,400 INFO sqlalchemy.engine.Engine COMMIT
4-4-1-4. 插入...返回

自动使用受支持后端的 RETURNING 子句,以便检索最后插入的主键值以及服务器默认值。然而,RETURNING 子句也可以使用 Insert.returning() 方法显式指定;在这种情况下,执行语句时返回的 Result 对象包含可以获取的行:

insert_stmt = insert(address_table).returning(
    address_table.c.id, address_table.c.email_address
)
print(insert_stmt)

输出结果如下,

INSERT INTO address (id, user_id, email_address) VALUES (:id, :user_id, :email_address) RETURNING address.id, address.email_address

它也可以与 Insert.from_select() 结合使用,如下例所示,该示例基于 INSERT...FROM SELECT 中所述的示例:

select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com")
insert_stmt = insert(address_table).from_select(
    ["user_id", "email_address"], select_stmt
)
print(insert_stmt.returning(address_table.c.id, address_table.c.email_address))

输出结果如下,

INSERT INTO address (user_id, email_address) SELECT user_account.id, user_account.name || :name_1 AS anon_1 
FROM user_account RETURNING address.id, address.email_address

Tip:

UPDATE 和 DELETE 语句也支持 RETURNING 功能,本教程稍后将介绍这些语句。

对于 INSERT 语句,RETURNING 功能既可用于单行语句,也可用于一次 INSERT 多行的语句。对带有 RETURNING 的多行 INSERT 的支持是特定于方言的,但是 SQLAlchemy 中包含的所有支持 RETURNING 的方言都受支持。有关此功能的背景信息,请参阅 INSERT 语句的"插入多个值"行为部分。

ORM 还支持带或不带 RETURNING 的批量 INSERT。请参阅 ORM Bulk INSERT 语句以获取参考文档。

4-4-1-5. 插入...从选择

Insert 的一个较少使用的功能,但为了完整性,这里 Insert 构造可以组成一个使用 Insert.from_select() 方法直接从 SELECT 获取行的 INSERT。此方法接受一个 select() 构造(将在下一节中讨论)以及实际 INSERT 中要定位的列名列表。在下面的示例中,行被添加到 address 表中,这些行源自 user_account 表中的行,为每个用户提供一个免费的电子邮件地址 aol.com

select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com")
insert_stmt = insert(address_table).from_select(
    ["user_id", "email_address"], select_stmt
)
print(insert_stmt)

输出结果如下,

INSERT INTO address (user_id, email_address) SELECT user_account.id, user_account.name || :name_1 AS anon_1 
FROM user_account

当人们想要将数据从数据库的其他部分直接复制到一组新的行中,而不实际从客户端获取和重新发送数据时,可以使用此构造。

也可以看看:
Insert - 在 SQL 表达式 API 文档中

4-4-2. 使用 SELECT 语句

对于 Core 和 ORM, select() 函数生成一个用于所有 SELECT 查询的 Select 构造。传递给 Core 中的 Connection.execute() 和 ORM 中的 Session.execute() 等方法,在当前事务中发出 SELECT 语句,并通过返回的 Result 对象获取结果行。

ORM 阅读器 - 此处的内容同样适用于 Core 和 ORM 使用,并且此处提到了基本的 ORM 变体用例。然而,还有更多 ORM 特定的功能可用;这些记录在 ORM 查询指南中。

4-4-2-1. select() SQL 表达式结构

select() 构造以与 insert() 相同的方式构建语句,使用生成方法,其中每个方法在对象上构建更多状态。与其他 SQL 结构一样,它可以就地字符串化:

from sqlalchemy import select
stmt = select(user_table).where(user_table.c.name == "spongebob")
print(stmt)

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account 
WHERE user_account.name = :name_1

同样与所有其他语句级 SQL 构造相同,为了实际运行该语句,我们将其传递给执行方法。由于 SELECT 语句返回行,我们始终可以迭代结果对象以获取 Row 对象:

with engine.connect() as conn:
    for row in conn.execute(stmt):
        print(row)

输出结果如下,

2023-09-16 21:30:37,111 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 21:30:37,112 INFO sqlalchemy.engine.Engine SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account 
WHERE user_account.name = :name_1
2023-09-16 21:30:37,112 INFO sqlalchemy.engine.Engine [generated in 0.00111s] {'name_1': 'spongebob'}
(3, 'spongebob', 'Spongebob Squarepants')
2023-09-16 21:30:37,117 INFO sqlalchemy.engine.Engine ROLLBACK

当使用 ORM 时,特别是使用针对 ORM 实体组成的 select() 构造时,我们将希望使用 Session 上的 Session.execute() 方法来执行它;使用这种方法,我们继续从结果中获取 Row 对象,但是这些行现在能够包含完整的实体,例如 User 类的实例,作为每个中的单独元素排:

stmt = select(User).where(User.name == "spongebob")
with Session(engine) as session:
    for row in session.execute(stmt):
        print(row)

输出结果如下,

2023-09-16 21:31:55,654 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 21:31:55,656 INFO sqlalchemy.engine.Engine SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account 
WHERE user_account.name = :name_1
2023-09-16 21:31:55,656 INFO sqlalchemy.engine.Engine [generated in 0.00042s] {'name_1': 'spongebob'}
(User(id=3, name='spongebob', fullname='Spongebob Squarepants'),)
2023-09-16 21:31:55,658 INFO sqlalchemy.engine.Engine ROLLBACK

从表中 select() 与 ORM 类

虽然无论我们调用 select(user_table) 还是 select(User) ,这些示例中生成的 SQL 看起来都是相同的,但在更一般的情况下,它们不一定呈现相同的内容,因为 ORM 映射的类可能会呈现相同的内容。映射到除表格之外的其他类型的"可选择项"。针对 ORM 实体的 select() 还指示应在结果中返回 ORM 映射的实例,而从 Table 对象中进行 SELECT 时情况并非如此。

以下部分将更详细地讨论 SELECT 构造。

4-4-2-2. 设置 COLUMNS 和 FROM 子句

select() 函数接受表示任意数量的 Column 和/或 Table 表达式的位置元素,以及各种兼容对象,这些对象被解析为要从中选择的 SQL 表达式列表,该列表将作为结果集中的列返回。这些元素还可以在更简单的情况下用于创建 FROM 子句,该子句是根据传递的列和类表表达式推断出来的:

print(select(user_table))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account

要使用核心方法从各个列中进行 SELECT,可以从 Table.c 访问器访问 Column 对象,并且可以直接发送; FROM 子句将被推断为由这些列表示的所有 Table 和其他 FromClause 对象的集合:

print(select(user_table.c.name, user_table.c.fullname))

输出结果如下,

SELECT user_account.name, user_account.fullname 
FROM user_account

或者,当使用任何 FromClause (例如 Table )的 FromClause.c 集合时,可以使用元组为 select() 指定多个列字符串名称:

print(select(user_table.c["name", "fullname"]))

输出结果如下,

SELECT user_account.name, user_account.fullname 
FROM user_account

或者,当使用任何 FromClause (例如 Table )的 FromClause.c 集合时,可以使用元组为 select() 指定多个列字符串名称:

print(select(user_table.c["name", "fullname"]))

输出结果如下,

SELECT user_account.name, user_account.fullname 
FROM user_account

2.0 版新增功能:向 :attr.FromClause.c 集合添加了元组访问器功能

4-4-2-3. 选择 ORM 实体和列

ORM 实体(例如我们的 User 类及其上的列映射属性(例如 User.name ))也参与表示表和列的 SQL 表达式语言系统。下面说明了从 User 实体进行 SELECT 的示例,它最终的呈现方式与我们直接使用 user_table 的方式相同:

print(select(User))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account

当使用 ORM Session.execute() 方法执行上述语句时,当我们从完整实体(例如 User )中进行选择时,与 user_table ,实体本身作为每行中的单个元素返回。也就是说,当我们从上述语句中获取行时,由于要获取的内容列表中只有 User 实体,因此我们会返回只有一个元素的 Row 对象,其中包含 User 类的实例:

row = session.execute(select(User)).first()
row

输出结果如下,

2023-09-16 22:31:59,892 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 22:31:59,893 INFO sqlalchemy.engine.Engine SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account
2023-09-16 22:31:59,893 INFO sqlalchemy.engine.Engine [generated in 0.00040s] {}
(User(id=3, name='spongebob', fullname='Spongebob Squarepants'),)

上面的 Row 只有一个元素,代表 User 实体:

row[0]

输出结果如下,

User(id=3, name='spongebob', fullname='Spongebob Squarepants')

强烈推荐的实现与上述相同结果的便捷方法是使用 Session.scalars() 方法直接执行语句;此方法将返回一个 ScalarResult 对象,该对象一次提供每行的第一个"列",在本例中为 User 类的实例:

user = session.scalars(select(User)).first()
user

输出结果如下,

2023-09-16 22:33:26,428 INFO sqlalchemy.engine.Engine SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account
2023-09-16 22:33:26,429 INFO sqlalchemy.engine.Engine [cached since 86.54s ago] {}
User(id=3, name='spongebob', fullname='Spongebob Squarepants')

或者,我们可以使用类绑定属性选择 ORM 实体的各个列作为结果行中的不同元素;当它们传递给诸如 select() 之类的构造时,它们将被解析为 Column 或由每个属性表示的其他 SQL 表达式:

print(select(User.name, User.fullname))

输出结果如下,

SELECT user_account.name, user_account.fullname 
FROM user_account

当我们使用 Session.execute() 调用此语句时,我们现在会收到每个值都有单独元素的行,每个元素对应于一个单独的列或其他 SQL 表达式:

row = session.execute(select(User.name, User.fullname)).first()
row

输出结果如下,

2023-09-16 22:35:27,303 INFO sqlalchemy.engine.Engine SELECT user_account.name, user_account.fullname 
FROM user_account
2023-09-16 22:35:27,303 INFO sqlalchemy.engine.Engine [generated in 0.00065s] {}
('spongebob', 'Spongebob Squarepants')

这些方法也可以混合使用,如下所示,我们选择 User 实体的 name 属性作为行的第一个元素,并将其与完整的 Address 第二个元素中的实体:

session.execute(
    select(User.name, Address).where(User.id == Address.user_id).order_by(Address.id)
).all()

输出结果如下,

2023-09-16 22:36:15,285 INFO sqlalchemy.engine.Engine SELECT user_account.name, address.id, address.email_address, address.user_id 
FROM user_account, address 
WHERE user_account.id = address.user_id ORDER BY address.id
2023-09-16 22:36:15,285 INFO sqlalchemy.engine.Engine [generated in 0.00083s] {}
[('spongebob', Address(id=3, email_address='spongebob@sqlalchemy.org')),
 ('sandy', Address(id=4, email_address='sandy@sqlalchemy.org')),
 ('sandy', Address(id=5, email_address='sandy@squirrelpower.org'))]

选择 ORM 实体和列的方法以及转换行的常用方法将在选择 ORM 实体和属性中进一步讨论。

4-4-2-4. 从带标签的 SQL 表达式中选择

ColumnElement.label() 方法以及 ORM 属性上可用的同名方法提供列或表达式的 SQL 标签,允许其在结果集中具有特定名称。当按名称引用结果行中的任意 SQL 表达式时,这会很有帮助:

from sqlalchemy import func, cast
stmt = select(
    ("Username: " + user_table.c.name).label("username"),
).order_by(user_table.c.name)
with engine.connect() as conn:
    for row in conn.execute(stmt):
        print(f"{row.username}")

输出结果如下,

2023-09-16 22:37:50,176 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 22:37:50,177 INFO sqlalchemy.engine.Engine SELECT :name_1 || user_account.name AS username 
FROM user_account ORDER BY user_account.name
2023-09-16 22:37:50,177 INFO sqlalchemy.engine.Engine [generated in 0.00085s] {'name_1': 'Username: '}
Username: patrick
Username: sandy
Username: spongebob
2023-09-16 22:37:50,180 INFO sqlalchemy.engine.Engine ROLLBACK
4-4-2-5. 使用文本列表达式进行选择

当我们使用 select() 函数构造一个 Select 对象时,我们通常会向它传递一系列 Table 和 Column 对象,这些对象是使用表元数据定义,或者使用 ORM 时,我们可能会发送表示表列的 ORM 映射属性。然而,有时也需要在语句中创建任意 SQL 块,例如常量字符串表达式,或者只是一些按字面编写速度更快的任意 SQL。

使用事务和 DBAPI 中引入的 text() 构造实际上可以直接嵌入到 Select 构造中,例如下面我们制造硬编码字符串文字 'some phrase' 并将其嵌入到 SELECT 语句中:

from sqlalchemy import text
stmt = select(text("'some phrase'"), user_table.c.name).order_by(user_table.c.name)
with engine.connect() as conn:
    print(conn.execute(stmt).all())

输出结果如下,

2023-09-16 22:38:53,506 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 22:38:53,506 INFO sqlalchemy.engine.Engine SELECT 'some phrase', user_account.name 
FROM user_account ORDER BY user_account.name
2023-09-16 22:38:53,507 INFO sqlalchemy.engine.Engine [generated in 0.00093s] {}
[('some phrase', 'patrick'), ('some phrase', 'sandy'), ('some phrase', 'spongebob')]
2023-09-16 22:38:53,510 INFO sqlalchemy.engine.Engine ROLLBACK

虽然 text() 构造可以在大多数地方使用来注入文字 SQL 短语,但我们实际上经常处理的是每个表示单个列表达式的文本单元。在这种常见情况下,我们可以使用 literal_column() 构造从文本片段中获得更多功能。该对象与 text() 类似,不同之处在于它不表示任何形式的任意 SQL,而是显式表示单个"列",然后可以在子查询和其他表达式中进行标记和引用:

from sqlalchemy import literal_column
stmt = select(literal_column("'some phrase'").label("p"), user_table.c.name).order_by(
    user_table.c.name
)
with engine.connect() as conn:
    for row in conn.execute(stmt):
        print(f"{row.p}, {row.name}")

输出结果如下,

2023-09-16 22:39:31,739 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 22:39:31,740 INFO sqlalchemy.engine.Engine SELECT 'some phrase' AS p, user_account.name 
FROM user_account ORDER BY user_account.name
2023-09-16 22:39:31,740 INFO sqlalchemy.engine.Engine [generated in 0.00102s] {}
some phrase, patrick
some phrase, sandy
some phrase, spongebob
2023-09-16 22:39:31,744 INFO sqlalchemy.engine.Engine ROLLBACK

请注意,在这两种情况下,当使用 text() 或 literal_column() 时,我们编写的是语法 SQL 表达式,而不是文字值。因此,我们必须包含我们希望呈现的 SQL 所需的任何引用或语法。

4-4-2-6. WHERE 子句

SQLAlchemy 允许我们将标准 Python 运算符与 Column 和类似对象结合使用来编写 SQL 表达式,例如 name = 'squidward' 或 user_id > 10 。对于布尔表达式,大多数 Python 运算符(例如 == 、 != 、 < 、 >= 等)会生成新的 SQL 表达式对象,而不是普通布尔 True / False 值:

print(user_table.c.name == "squidward")

输出结果如下,

user_account.name = :name_1

print(address_table.c.user_id > 10)

输出结果如下,

address.user_id > :user_id_1

我们可以使用这样的表达式通过将结果对象传递给 Select.where() 方法来生成 WHERE 子句:

print(select(user_table).where(user_table.c.name == "squidward"))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account 
WHERE user_account.name = :name_1

要生成由 AND 连接的多个表达式,可以调用 Select.where() 方法任意多次:

print(
    select(address_table.c.email_address)
    .where(user_table.c.name == "squidward")
    .where(address_table.c.user_id == user_table.c.id)
)

输出结果如下,

SELECT address.email_address 
FROM address, user_account 
WHERE user_account.name = :name_1 AND address.user_id = user_account.id

对 Select.where() 的单次调用也接受具有相同效果的多个表达式:

print(
    select(address_table.c.email_address).where(
        user_table.c.name == "squidward",
        address_table.c.user_id == user_table.c.id,
    )
)

输出结果如下,

SELECT address.email_address 
FROM address, user_account 
WHERE user_account.name = :name_1 AND address.user_id = user_account.id

"AND"和"OR"连词都可以直接使用 and_() 和 or_() 函数使用,如下图的 ORM 实体所示:

from sqlalchemy import and_, or_
print(
    select(Address.email_address).where(
        and_(
            or_(User.name == "squidward", User.name == "sandy"),
            Address.user_id == User.id,
        )
    )
)

输出结果如下,

SELECT address.email_address 
FROM address, user_account 
WHERE (user_account.name = :name_1 OR user_account.name = :name_2) AND address.user_id = user_account.id

对于与单个实体进行简单的"相等"比较,还有一种名为 Select.filter_by() 的流行方法,它接受与列键或 ORM 属性名称匹配的关键字参数。它将根据最左边的 FROM 子句或最后加入的实体进行过滤:

print(select(User).filter_by(name="spongebob", fullname="Spongebob Squarepants"))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account 
WHERE user_account.name = :name_1 AND user_account.fullname = :fullname_1
4-4-2-7. 显式 FROM 子句和 JOIN

如前所述,FROM 子句通常是根据我们在 columns 子句中设置的表达式以及 Select 的其他元素来推断的。

如果我们在 COLUMNS 子句中设置特定 Table 的单个列,它也会将该 Table 放入 FROM 子句中:

print(select(user_table.c.name))

输出结果如下,

SELECT user_account.name 
FROM user_account

如果我们要放置两个表中的列,那么我们会得到一个以逗号分隔的 FROM 子句:

print(select(user_table.c.name, address_table.c.email_address))

输出结果如下,

SELECT user_account.name, address.email_address 
FROM user_account, address

为了将这两个表连接在一起,我们通常在 Select 上使用两种方法之一。第一个是 Select.join_from() 方法,它允许我们显式指示 JOIN 的左侧和右侧:

print(
    select(user_table.c.name, address_table.c.email_address).join_from(
        user_table, address_table
    )
)

输出结果如下,

SELECT user_account.name, address.email_address 
FROM user_account JOIN address ON user_account.id = address.user_id

另一种是 Select.join() 方法,它只指示JOIN的右侧,推断左侧:

print(select(user_table.c.name, address_table.c.email_address).join(address_table))

输出结果如下,

SELECT user_account.name, address.email_address 
FROM user_account JOIN address ON user_account.id = address.user_id

ON 子句被推断:

当使用 Select.join_from() 或 Select.join() 时,我们可能会发现在简单的外键情况下,连接的 ON 子句也被推断出来。下一节将详细介绍这一点。

如果没有按照我们想要的方式从 columns 子句中推断出元素,我们还可以选择显式地将元素添加到 FROM 子句中。我们使用 Select.select_from() 方法来实现此目的,如下所示,我们将 user_table 建立为 FROM 子句中的第一个元素,并使用 Select.join() 建立 address_table

print(select(address_table.c.email_address).select_from(user_table).join(address_table))

输出结果如下,

SELECT address.email_address 
FROM user_account JOIN address ON user_account.id = address.user_id

我们可能想要使用 Select.select_from() 的另一个例子是,如果我们的 columns 子句没有足够的信息来为 FROM 子句提供。例如,要从常见 SQL 表达式 count(*) 中进行 SELECT,我们使用名为 sqlalchemy.sql.expression.func 的 SQLAlchemy 元素来生成 SQL count() 函数:

from sqlalchemy import func
print(select(func.count("*")).select_from(user_table))

输出结果如下,

from sqlalchemy import func
print(select(func.count("*")).select_from(user_table))

输出结果如下,

SELECT count(:count_2) AS count_1 
FROM user_account

也可以看看:

在 ORM 查询指南中设置连接中最左边的 FROM 子句包含有关 Select.select_from() 和 Select.join() 交互的附加示例和注释。

4-4-2-8. 设置 ON 子句

前面的 JOIN 示例说明了 Select 构造可以连接两个表并自动生成 ON 子句。在这些示例中会出现这种情况,因为 user_table 和 address_table Table 对象包含用于形成此 ON 子句的单个 ForeignKeyConstraint 定义。

如果连接的左右目标没有这样的约束,或者有多个约束,我们需要直接指定ON子句。 Select.join() 和 Select.join_from() 都接受 ON 子句的附加参数,该参数使用与我们在 WHERE 子句中看到的相同的 SQL 表达式机制来声明:

print(
    select(address_table.c.email_address)
    .select_from(user_table)
    .join(address_table, user_table.c.id == address_table.c.user_id)
)

输出结果如下,

SELECT address.email_address 
FROM user_account JOIN address ON user_account.id = address.user_id

ORM 提示 - 在使用使用 relationship() 构造的 ORM 实体时,还有另一种方法可以生成 ON 子句,就像上一节中声明映射类中设置的映射一样。这是一个完整的主题,在使用关系加入中详细介绍。

4-4-2-9. 外部连接和完整连接

Select.join() 和 Select.join_from() 方法都接受关键字参数 Select.join.isouter 和 Select.join.full ,它们将分别呈现 LEFT OUTER JOIN 和 FULL OUTER JOIN:

print(select(user_table).join(address_table, isouter=True))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account LEFT OUTER JOIN address ON user_account.id = address.user_id

print(select(user_table).join(address_table, full=True))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account FULL OUTER JOIN address ON user_account.id = address.user_id

还有一个方法 Select.outerjoin() 相当于使用 .join(..., isouter=True) 。

SQL 也有"RIGHT OUTER JOIN"。 SQLAlchemy 不会直接渲染它;相反,颠倒表的顺序并使用"LEFT OUTER JOIN"。

4-4-2-10. ORDER BY, GROUP BY, HAVING

SELECT SQL 语句包含一个名为 ORDER BY 的子句,该子句用于按给定顺序返回选定的行。

GROUP BY 子句的构造与 ORDER BY 子句类似,其目的是将所选行细分为可以调用聚合函数的特定组。 HAVING 子句通常与 GROUP BY 一起使用,其形式与 WHERE 子句类似,只不过它应用于组内使用的聚合函数。

ORDER BY

ORDER BY 子句是根据通常基于 Column 或类似对象的 SQL 表达式构造来构造的。 Select.order_by() 方法按位置接受一个或多个以下表达式:

print(select(user_table).order_by(user_table.c.name))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account ORDER BY user_account.name

升序/降序可通过 ColumnElement.asc() 和 ColumnElement.desc() 修饰符使用,这些修饰符也存在于 ORM 绑定属性中:

print(select(User).order_by(User.fullname.desc()))

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account ORDER BY user_account.fullname DESC

上述语句将生成按 user_account.fullname 列降序排序的行。

使用 GROUP BY / HAVING 聚合函数

在 SQL 中,聚合函数允许将多行的列表达式聚合在一起以生成单个结果。示例包括计数、计算平均值以及查找一组值中的最大值或最小值。

SQLAlchemy 使用名为 func 的命名空间以开放式方式提供 SQL 函数。这是一个特殊的构造函数对象,当给定特定 SQL 函数的名称时,它将创建 Function 的新实例,该函数可以具有任何名称,以及传递给该函数的零个或多个参数,这些参数是与所有其他情况一样,SQL 表达式构造。例如,要针对 user_account.id 列呈现 SQL COUNT() 函数,我们调用 count() 名称:

from sqlalchemy import func
count_fn = func.count(user_table.c.id)
print(count_fn)

输出结果如下,

count(user_account.id)

本教程后面的使用 SQL 函数更详细地描述了 SQL 函数。

在 SQL 中使用聚合函数时,GROUP BY 子句至关重要,因为它允许将行划分为组,其中聚合函数将单独应用于每个组。当在 SELECT 语句的 COLUMNS 子句中请求非聚合列时,SQL 要求所有这些列都受 GROUP BY 子句的约束,直接或间接基于主键关联。然后,HAVING 子句的使用方式与 WHERE 子句类似,只不过它根据聚合值而不是直接行内容来过滤行。

SQLAlchemy 使用 Select.group_by() 和 Select.having() 方法提供这两个子句。下面我们将说明如何为拥有多个地址的用户选择用户名字段以及地址计数:

with engine.connect() as conn:
    result = conn.execute(
        select(User.name, func.count(Address.id).label("count"))
        .join(Address)
        .group_by(User.name)
        .having(func.count(Address.id) > 1)
    )
    print(result.all())

输出结果如下,

2023-09-16 22:57:09,039 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 22:57:09,040 INFO sqlalchemy.engine.Engine SELECT user_account.name, count(address.id) AS count 
FROM user_account JOIN address ON user_account.id = address.user_id GROUP BY user_account.name 
HAVING count(address.id) > :count_1
2023-09-16 22:57:09,040 INFO sqlalchemy.engine.Engine [generated in 0.00087s] {'count_1': 1}
[('sandy', 2)]

2023-09-16 22:57:09,051 INFO sqlalchemy.engine.Engine ROLLBACK

按标签排序或分组

一项重要的技术(特别是在某些数据库后端上)是能够对 columns 子句中已声明的表达式进行 ORDER BY 或 GROUP BY,而无需在 ORDER BY 或 GROUP BY 子句中重新声明表达式,而是使用列COLUMNS 子句中的名称或标记名称。通过将名称的字符串文本传递给 Select.order_by() 或 Select.group_by() 方法,可以使用此形式。传递的文本并不直接渲染;相反,为 columns 子句中的表达式指定的名称,并在上下文中呈现为该表达式名称,如果未找到匹配项,则会引发错误。一元修饰符 asc() 和 desc() 也可以这种形式使用:

from sqlalchemy import func, desc
stmt = (
    select(Address.user_id, func.count(Address.id).label("num_addresses"))
    .group_by("user_id")
    .order_by("user_id", desc("num_addresses"))
)
print(stmt)

输出结果如下,

SELECT address.user_id, count(address.id) AS num_addresses 
FROM address GROUP BY address.user_id ORDER BY address.user_id, num_addresses DESC
4-4-2-11. 使用别名

现在我们正在从多个表中进行选择并使用联接,我们很快就会遇到需要在语句的 FROM 子句中多次引用同一个表的情况。我们使用 SQL 别名来实现这一点,它是一种为表或子查询提供替代名称的语法,可以在语句中引用它。

在 SQLAlchemy 表达式语言中,这些"名称"由称为 Alias 构造的 FromClause 对象表示,该对象是使用 FromClause.alias() 方法在 Core 中构造的。 Alias 构造就像 Table 构造一样,它也具有 Alias.c 集合中的 Column 对象的命名空间。例如,下面的 SELECT 语句返回所有唯一的用户名对:

user_alias_1 = user_table.alias()
user_alias_2 = user_table.alias()
print(
    select(user_alias_1.c.name, user_alias_2.c.name).join_from(
        user_alias_1, user_alias_2, user_alias_1.c.id > user_alias_2.c.id
    )
)

输出结果如下,

SELECT user_account_1.name, user_account_2.name AS name_1 
FROM user_account AS user_account_1 JOIN user_account AS user_account_2 ON user_account_1.id > user_account_2.id

ORM 实体别名

FromClause.alias() 方法的 ORM 等效项是 ORM aliased() 函数,它可以应用于诸如 User 和 Address 之类的实体。这会在内部生成一个与原始映射的 Table 对象相对应的 Alias 对象,同时保持 ORM 功能。下面的 SELECT 从 User 实体中选择包含两个特定电子邮件地址的所有对象:

from sqlalchemy.orm import aliased
address_alias_1 = aliased(Address)
address_alias_2 = aliased(Address)
print(
    select(User)
    .join_from(User, address_alias_1)
    .where(address_alias_1.email_address == "patrick@aol.com")
    .join_from(User, address_alias_2)
    .where(address_alias_2.email_address == "patrick@gmail.com")
)

输出结果如下,

SELECT user_account.id, user_account.name, user_account.fullname 
FROM user_account JOIN address AS address_1 ON user_account.id = address_1.user_id JOIN address AS address_2 ON user_account.id = address_2.user_id 
WHERE address_1.email_address = :email_address_1 AND address_2.email_address = :email_address_2

正如设置 ON 子句中提到的,ORM 提供了另一种使用 relationship() 构造进行连接的方法。上述使用别名的示例是在使用关系连接别名目标之间使用 relationship() 进行演示的。

4-4-2-12. 子查询和 CTEs

SQL 中的子查询是一个 SELECT 语句,它在括号内呈现并放置在封闭语句的上下文中,通常是 SELECT 语句,但不一定。

本节将介绍所谓的"非标量"子查询,它通常放置在封闭的 SELECT 的 FROM 子句中。我们还将介绍公共表表达式或 CTE,它的使用方式与子查询类似,但包含其他功能。

SQLAlchemy 使用 Subquery 对象表示子查询,使用 CTE 表示 CTE,通常从 Select.subquery() 和 Select.cte() 方法获取,分别。任一对象都可以用作较大 select() 构造内的 FROM 元素。

我们可以构造一个 Subquery ,它将从 address 表中选择行的聚合计数(聚合函数和 GROUP BY 之前在使用 GROUP BY / HAVING 的聚合函数中介绍过):

subq = (
    select(func.count(address_table.c.id).label("count"), address_table.c.user_id)
    .group_by(address_table.c.user_id)
    .subquery()
)

单独对子查询进行字符串化,而不将其嵌入另一个 Select 或其他语句中,会生成不带任何括号的纯 SELECT 语句:

print(subq)

输出结果如下,

SELECT count(address.id) AS count, address.user_id 
FROM address GROUP BY address.user_id

Subquery 对象的行为与任何其他 FROM 对象(例如 Table )类似,值得注意的是它包含它选择的列的 Subquery.c 命名空间。我们可以使用此命名空间来引用 user_id 列以及我们的自定义标记 count 表达式:

print(select(subq.c.user_id, subq.c.count))

输出结果如下,

SELECT anon_1.user_id, anon_1.count 
FROM (SELECT count(address.id) AS count, address.user_id AS user_id 
FROM address GROUP BY address.user_id) AS anon_1

通过选择 subq 对象中包含的行,我们可以将该对象应用于更大的 Select ,该 Select 会将数据连接到 user_account 表:

stmt = select(user_table.c.name, user_table.c.fullname, subq.c.count).join_from(
    user_table, subq
)

print(stmt)

输出结果如下,

SELECT user_account.name, user_account.fullname, anon_1.count 
FROM user_account JOIN (SELECT count(address.id) AS count, address.user_id AS user_id 
FROM address GROUP BY address.user_id) AS anon_1 ON user_account.id = anon_1.user_id

为了从 user_account 加入到 address ,我们使用了 Select.join_from() 方法。如前所述,该连接的 ON 子句再次根据外键约束推断出来。即使 SQL 子查询本身没有任何约束,SQLAlchemy 也可以通过确定 subq.c.user_id 列派生自 address_table.c.user_id 列来对列上表示的约束进行操作,该列确实表达了外键关系返回到 user_table.c.id 列,然后用于生成 ON 子句。

通用表表达式 (CTEs)

SQLAlchemy 中 CTE 构造的用法实际上与 Subquery 构造的使用方式相同。通过更改 Select.subquery() 方法的调用以使用 Select.cte() 代替,我们可以以相同的方式将结果对象用作 FROM 元素,但呈现的 SQL 是非常不同的公用表表达式语法:

subq = (
    select(func.count(address_table.c.id).label("count"), address_table.c.user_id)
    .group_by(address_table.c.user_id)
    .cte()
)

stmt = select(user_table.c.name, user_table.c.fullname, subq.c.count).join_from(
    user_table, subq
)

print(stmt)

输出结果如下,

WITH anon_1 AS 
(SELECT count(address.id) AS count, address.user_id AS user_id 
FROM address GROUP BY address.user_id)
 SELECT user_account.name, user_account.fullname, anon_1.count 
FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id

CTE 构造还具有以"递归"样式使用的能力,并且在更复杂的情况下可以由 INSERT、UPDATE 或 DELETE 语句的 RETURNING 子句组成。 CTE 的文档字符串包含有关这些附加模式的详细信息。

在这两种情况下,子查询和 CTE 都是在 SQL 级别使用"匿名"名称命名的。在Python代码中,我们根本不需要提供这些名称。 Subquery 或 CTE 实例的对象标识在渲染时充当对象的语法标识。将在 SQL 中呈现的名称可以通过将其作为 Select.subquery() 或 Select.cte() 方法的第一个参数传递来提供。

也可以看看:

Select.subquery() - 有关子查询的更多详细信息

Select.cte() - CTE 示例,包括如何使用 RECURSIVE 以及面向 DML 的 CTE

ORM 实体子查询/CTEs

在 ORM 中, aliased() 构造可用于将 ORM 实体(例如我们的 User 或 Address 类)与任何 FromClause 表示行源的概念。前面的 ORM 实体别名部分说明了如何使用 aliased() 将映射的类与其映射的 Table 的 Alias 关联起来。在这里,我们说明 aliased() 对 Subquery 以及针对 Select 构造生成的 CTE 执行相同的操作,最终派生来自同一个映射的 Table 。

下面是将 aliased() 应用于 Subquery 构造的示例,以便可以从其行中提取 ORM 实体。结果显示一系列 User 和 Address 对象,其中每个 Address 对象的数据最终来自针对 address 的子查询表而不是直接该表:

subq = select(Address).where(~Address.email_address.like("%@aol.com")).subquery()
address_subq = aliased(Address, subq)
stmt = (
    select(User, address_subq)
    .join_from(User, address_subq)
    .order_by(User.id, address_subq.id)
)
with Session(engine) as session:
    for user, address in session.execute(stmt):
        print(f"{user} {address}")

输出结果如下,

2023-09-16 23:06:09,195 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 23:06:09,198 INFO sqlalchemy.engine.Engine SELECT user_account.id, user_account.name, user_account.fullname, anon_1.id AS id_1, anon_1.email_address, anon_1.user_id 
FROM user_account JOIN (SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id 
FROM address 
WHERE address.email_address NOT LIKE :email_address_1) anon_1 ON user_account.id = anon_1.user_id ORDER BY user_account.id, anon_1.id
2023-09-16 23:06:09,198 INFO sqlalchemy.engine.Engine [generated in 0.00071s] {'email_address_1': '%@aol.com'}
2023-09-16 23:06:09,204 INFO sqlalchemy.engine.Engine ROLLBACK

下面是另一个示例,除了使用 CTE 构造之外,它完全相同:

cte_obj = select(Address).where(~Address.email_address.like("%@aol.com")).cte()
address_cte = aliased(Address, cte_obj)
stmt = (
    select(User, address_cte)
    .join_from(User, address_cte)
    .order_by(User.id, address_cte.id)
)
with Session(engine) as session:
    for user, address in session.execute(stmt):
        print(f"{user} {address}")

输出结果如下,

2023-09-16 23:06:50,019 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 23:06:50,021 INFO sqlalchemy.engine.Engine WITH anon_1 AS 
(SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id 
FROM address 
WHERE address.email_address NOT LIKE :email_address_1)
 SELECT user_account.id, user_account.name, user_account.fullname, anon_1.id AS id_1, anon_1.email_address, anon_1.user_id 
FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id ORDER BY user_account.id, anon_1.id
2023-09-16 23:06:50,021 INFO sqlalchemy.engine.Engine [generated in 0.00045s] {'email_address_1': '%@aol.com'}
2023-09-16 23:06:50,025 INFO sqlalchemy.engine.Engine ROLLBACK
4-4-2-13. 标量和相关子查询

标量子查询是精确返回零或一行和一列的子查询。然后,该子查询将在封闭的 SELECT 语句的 COLUMNS 或 WHERE 子句中使用,并且与常规子查询不同,它不在 FROM 子句中使用。相关子查询是引用封闭 SELECT 语句中的表的标量子查询。

SQLAlchemy 使用 ScalarSelect 构造表示标量子查询,该构造是 ColumnElement 表达式层次结构的一部分,与由 Subquery 构造表示的常规子查询形成对比,位于 FromClause 层次结构中。

标量子查询通常(但不一定)与聚合函数一起使用,之前在使用 GROUP BY / HAVING 的聚合函数中介绍过。标量子查询通过使用 Select.scalar_subquery() 方法显式指示,如下所示。当它本身进行字符串化时,它是默认的字符串形式,呈现为从两个表中进行选择的普通 SELECT 语句:

subq = (
    select(func.count(address_table.c.id))
    .where(user_table.c.id == address_table.c.user_id)
    .scalar_subquery()
)
print(subq)

输出结果如下,

(SELECT count(address.id) AS count_1 
FROM address, user_account 
WHERE user_account.id = address.user_id)

上面的 subq 对象现在属于 ColumnElement SQL 表达式层次结构,因为它可以像任何其他列表达式一样使用:

print(subq == 5)

输出结果如下,

(SELECT count(address.id) AS count_1 
FROM address, user_account 
WHERE user_account.id = address.user_id) = :param_1

尽管标量子查询本身在其 FROM 子句中进行字符串化时会同时呈现 user_account 和 address ,但当将其嵌入到处理以下内容的封闭 select() 构造中时, user_account 表、 user_account 表自动关联,这意味着它不会在子查询的 FROM 子句中呈现:

stmt = select(user_table.c.name, subq.label("address_count"))
print(stmt)

输出结果如下,

SELECT user_account.name, (SELECT count(address.id) AS count_1 
FROM address 
WHERE user_account.id = address.user_id) AS address_count 
FROM user_account

简单的相关子查询通常会做所需的正确事情。然而,在相关性不明确的情况下,SQLAlchemy 会让我们知道需要更清晰的说明:

stmt = (
    select(
        user_table.c.name,
        address_table.c.email_address,
        subq.label("address_count"),
    )
    .join_from(user_table, address_table)
    .order_by(user_table.c.id, address_table.c.id)
)
print(stmt)

输出结果如下,

InvalidRequestError: Select statement '<sqlalchemy.sql.selectable.Select object at 0x00000246786AEF50>' returned no FROM clauses due to auto-correlation; specify correlate(<tables>) to control correlation manually.

要指定 user_table 是我们寻求关联的对象,我们使用 ScalarSelect.correlate() 或 ScalarSelect.correlate_except() 方法指定这一点:

subq = (
    select(func.count(address_table.c.id))
    .where(user_table.c.id == address_table.c.user_id)
    .scalar_subquery()
    .correlate(user_table)
)

然后,该语句可以像其他任何列一样返回该列的数据:

with engine.connect() as conn:
    result = conn.execute(
        select(
            user_table.c.name,
            address_table.c.email_address,
            subq.label("address_count"),
        )
        .join_from(user_table, address_table)
        .order_by(user_table.c.id, address_table.c.id)
    )
    print(result.all())

输出结果如下,

2023-09-16 23:13:51,030 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-09-16 23:13:51,030 INFO sqlalchemy.engine.Engine SELECT user_account.name, address.email_address, (SELECT count(address.id) AS count_1 
FROM address 
WHERE user_account.id = address.user_id) AS address_count 
FROM user_account JOIN address ON user_account.id = address.user_id ORDER BY user_account.id, address.id
2023-09-16 23:13:51,031 INFO sqlalchemy.engine.Engine [generated in 0.00117s] {}
[('spongebob', 'spongebob@sqlalchemy.org', 1), ('sandy', 'sandy@sqlalchemy.org', 2), ('sandy', 'sandy@squirrelpower.org', 2)]

2023-09-16 23:13:51,039 INFO sqlalchemy.engine.Engine ROLLBACK

未完待续!!!

https://docs.sqlalchemy.org/en/20/tutorial/data_select.html => LATERAL correlation

相关推荐
云和数据.ChenGuang41 分钟前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys1 小时前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi1 小时前
SQL注入的那些面试题总结
数据库·sql
建投数据2 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi3 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀3 小时前
Redis梳理
数据库·redis·缓存
独行soc3 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天4 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺4 小时前
分布式系统架构:服务容错
数据库·架构
独行soc5 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘