数据库到代码(ORM)¶
在这里我将告诉你 SQLModel 如何与数据库交互,为什么你会想使用它(或使用类似的工具),以及它与 SQL 的关系。
代码中的内联 SQL¶
我们来看看一个简单的 SQL 查询示例,用于从 `hero` 表中获取所有数据
SELECT *
FROM hero;
该 SQL 查询将返回表
| id | name | secret_name | age | team_id |
|---|---|---|---|---|
| 1 | 死侍 | 戴夫·威尔逊 | 空 | 2 |
| 2 | 蜘蛛男孩 | 佩德罗·帕尔克多 | 空 | 1 |
| 3 | 锈人 | 汤米·夏普 | 48 | 1 |
这种 SQL 语言有一个小小的注意事项。它并非设计用于与 Python 等编程语言中的普通代码混合使用。🚨
因此,如果你正在使用 Python,最简单的选择是将 SQL 代码放入字符串中,然后直接将该字符串发送到数据库。
statement = "SELECT * FROM hero;"
results = database.execute(statement)
但在那种情况下,你将无法获得编辑器支持、内联错误、自动补全等功能。因为对于编辑器来说,SQL 语句只是一串文本。如果你有错误,编辑器将无法提供帮助。😔
更重要的是,在大多数情况下,你会发送带有修改和参数的 SQL 字符串。例如,获取 *特定项目 ID*、*日期范围* 等数据。
在大多数情况下,你的代码用于查询或修改数据库中数据的参数,以某种方式来自外部用户。
例如,查看此 SQL 查询
SELECT *
FROM hero
WHERE id = 2;
它使用的是 ID 参数 `2`。这个数字 `2` 可能以某种方式来自用户输入。
用户可能以某种方式告诉你的应用程序
嘿,我想获取 ID 为的英雄
2
结果将是这张表(只有一行)
| id | name | secret_name | age | team_id |
|---|---|---|---|---|
| 2 | 蜘蛛男孩 | 佩德罗·帕尔克多 | 空 | 1 |
SQL 注入¶
但假设你的代码接收外部用户提供的任何内容,并在发送到数据库之前将其放入 SQL 字符串中。类似于这样
# Never do this! 🚨 Continue reading.
user_id = input("Type the user ID: ")
statement = f"SELECT * FROM hero WHERE id = {user_id};"
results = database.execute(statement)
如果外部用户实际上是攻击者,他们可能会向你发送恶意 SQL 字符串,执行一些可怕的操作,例如删除所有记录。这被称为“SQL 注入”。
例如,想象一下这个新的攻击者用户说
嘿,我想获取 ID 为的英雄
2; DROP TABLE hero
然后上面接收用户输入并将其放入 SQL 的代码实际上会向数据库发送这个
SELECT * FROM hero WHERE id = 2; DROP TABLE hero;
检查末尾添加的部分。那是另一个完整的 SQL 语句
DROP TABLE hero;
这就是你在 SQL 中告诉数据库删除整个 `hero` 表的方式。
不!我们丢失了 `hero` 表中的所有数据!💥😱
SQL 清理¶
确保外部用户发送的任何内容在 SQL 字符串中都是安全使用的过程称为清理。
它在 SQLModel 中默认提供(得益于 SQLAlchemy)。许多其他类似的工具也会提供该功能以及许多其他功能。
现在你准备好听 xkcd 的笑话了

SQL 与 SQLModel¶
使用 SQLModel,你无需直接编写 SQL 语句,而是使用 Python 类和对象与数据库交互。
例如,你可以使用以下代码向数据库请求 ID 为 `2` 的相同英雄
user_id = input("Type the user ID: ")
session.exec(
select(Hero).where(Hero.id == user_id)
).all()
如果用户提供此 ID
2
...结果将是这张表(只有一行)
| id | name | secret_name | age | team_id |
|---|---|---|---|---|
| 2 | 蜘蛛男孩 | 佩德罗·帕尔克多 | 空 | 1 |
防止 SQL 注入¶
如果用户是攻击者并尝试将此作为“ID”发送
2; DROP TABLE hero
那么 SQLModel 将其转换为字面字符串 `“2; DROP TABLE hero”`。
然后,它将告诉 SQL 数据库尝试查找具有该确切 ID 的记录,而不是注入攻击。
最终 SQL 语句的区别很微妙,但它完全改变了含义
SELECT * FROM hero WHERE id = "2; DROP TABLE hero;";
提示
请注意双引号 (`"`) 将其变为字符串,而不是更原始的 SQL。
数据库将找不到任何具有该 ID 的记录
"2; DROP TABLE hero;"
然后数据库将发送一个空表作为结果,因为它没有找到任何具有该 ID 的记录。
然后你的代码将继续执行,并平静地告诉用户它找不到任何东西。
但我们从未删除 `hero` 表。🎉
信息
当然,还有其他方法可以进行 SQL 数据清理,而无需使用 SQLModel 这样的工具,但它仍然是你默认获得的一个不错的功能。
编辑器支持¶
再次查看上面的 Python 片段。
因为我们使用的是标准 Python 类和对象,你的编辑器将能够为你提供自动补全、内联错误等。
例如,假设你想查询数据库以根据秘密身份查找英雄。
也许你不记得你给列起了什么名字。也许是
secret_identity?
...或者是不是
secretidentity?
...或者
private_name?secret_name?secretname?
如果你在代码中的 SQL 字符串中键入这些内容,你的编辑器将无法帮助你
statement = "SELECT * FROM hero WHERE secret_identity = 'Dive Wilson';"
results = database.execute(statement)
...你的编辑器会将其视为一个长字符串,其中包含一些文本,并且无法自动补全或检测 `secret_identity` 中的错误。
但是如果你使用普通的 Python 类和对象,你的编辑器将能够帮助你
database.execute(
select(Hero).where(Hero.secret_name == "Dive Wilson")
).all()

ORM 与 SQL¶
像 SQLModel(当然还有 SQLAlchemy)这类在 SQL 和带有类和对象的代码之间进行转换的库被称为 ORM。
ORM 意为 对象关系映射器。
这是一个非常常见的术语,但它也来源于相当技术性和学术性的概念 👩🎓
- 对象(Object):指带有类和实例的代码,通常称为“面向对象编程”,这就是“Object”部分的来源。
例如,这个类就是面向对象编程的一部分
class Hero(SQLModel):
id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
age: int | None = None
- 关系(Relational):指 SQL 数据库。还记得它们也被称为关系型数据库吗,因为每个表也被称为一个“关系”?这就是“关系”的来源。
例如,这个关系或表
| id | name | secret_name | age | team_id |
|---|---|---|---|---|
| 1 | 死侍 | 戴夫·威尔逊 | 空 | 2 |
| 2 | 蜘蛛男孩 | 佩德罗·帕尔克多 | 空 | 1 |
| 3 | 锈人 | 汤米·夏普 | 48 | 1 |
- 映射器(Mapper):这来自数学,当有东西可以将某些集合转换为另一个集合时,这被称为“映射函数”。这就是映射器的来源。
我们也可以在 Python 中编写一个映射函数,将小写字母集转换为大写字母集,像这样
def map_lower_to_upper(value: str):
return value.upper()
这实际上是一个简单的想法,却有一个非常学术和数学化的名字。😅
所以,ORM 是一个在 SQL 和代码之间,以及代码和 SQL 之间进行转换的库。所有这些都使用类和对象。
除了 SQLModel 之外,还有许多可用的 ORM,你可以在替代方案、灵感和比较中阅读更多关于它们的信息
SQL 表名¶
技术背景
这对 SQL 纯粹主义者来说有点无聊的背景知识。随意跳过此部分。😉
在使用纯 SQL 时,通常会将表名设为复数。因此,表名会是 `heroes` 而不是 `hero`,因为它可能包含多行,每行一个英雄。
然而,SQLModel 和许多其他类似的工具可以从你的代码自动生成表名,你将在稍后的教程中看到。
但是这个名字将从类名派生。并且使用单数名称作为类名是常见的做法(例如 `class Hero`,而不是 `class Heroes`)。使用像 `class Hero` 这样的单数名称也使你的代码更直观。
你将看到你自己的代码比内部表名多得多,所以最好保持代码/类约定,而不是 SQL 约定。
因此,为了保持一致性,我将继续使用 SQLModel 会生成的相同表名。
提示
你也可以覆盖表名。你可以在高级用户指南中阅读相关内容。