跳到内容

数据库到代码 (ORMs)

在这里,我将告诉您 SQLModel 如何与数据库交互,为什么您可能想要使用它(或使用类似的工具),以及它与 SQL 的关系。

代码中的内联 SQL

让我们检查一下这个简单的 SQL 查询示例,以获取 hero 表中的所有数据

SELECT *
FROM hero;

该 SQL 查询将返回表

idnamesecret_nameageteam_id
1DeadpondDive Wilsonnull2
2Spider-BoyPedro Parqueadornull1
3Rusty-ManTommy Sharp481

这种 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

的结果将是此表(单行)

idnamesecret_nameageteam_id
2Spider-BoyPedro Parqueadornull1

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 的一个笑话

Exploits of a Mom

SQL 与 SQLModel

使用 SQLModel,您可以使用 Python 类和对象与数据库交互,而不是直接编写 SQL 语句。

例如,您可以使用以下代码向数据库请求 ID 为 2 的相同 hero

user_id = input("Type the user ID: ")

session.exec(
    select(Hero).where(Hero.id == user_id)
).all()

如果用户提供此 ID

2

...结果将是此表(单行)

idnamesecret_nameageteam_id
2Spider-BoyPedro Parqueadornull1

防止 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 表。🎉

信息

当然,还有其他方法可以在不使用像 SQLModel 这样的工具的情况下进行 SQL 数据净化,但这仍然是您默认获得的一个不错的功能。

编辑器支持

再次检查上面的 Python 代码片段。

因为我们正在使用标准 Python 类和对象,所以您的编辑器将能够为您提供自动完成、内联错误等功能。

例如,假设您想查询数据库以根据秘密身份查找 hero。

也许您不记得列是如何命名的。也许是

  • 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()

ORMs 和 SQL

SQLModel(当然还有 SQLAlchemy)这样的库,它们在使用类和对象的情况下在 SQL 和代码之间进行转换,被称为 ORMs

ORM 表示 对象关系映射器

这是一个非常常见的术语,但它也来自非常技术性和学术性的概念 👩‍🎓

  • 对象:指的是使用类和实例的代码,通常称为“面向对象编程”,这就是“对象”部分的原因。

例如,这个类是“对象”面向对象编程的一部分

class Hero(SQLModel):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None
  • 关系:指的是 SQL 数据库。记住,它们也称为 关系数据库,因为每个表也称为“关系”?这就是“关系”的来源。

例如,这个 关系 或表

idnamesecret_nameageteam_id
1DeadpondDive Wilsonnull2
2Spider-BoyPedro Parqueadornull1
3Rusty-ManTommy Sharp481
  • 映射器:这来自数学,当存在可以将一组事物转换为另一组事物的工具时,这称为“映射函数”。这就是 映射器 的来源。

Squares to Triangles Mapper

我们还可以用 Python 编写一个 映射函数,将小写字母集转换为大写字母集,如下所示

def map_lower_to_upper(value: str):
    return value.upper()

实际上这是一个简单的想法,却有一个非常学术和数学化的名称。😅

因此,ORM 是一个库,它在 SQL 和代码之间,以及代码和 SQL 之间进行转换。所有这些都使用类和对象。

除了 SQLModel 之外,还有许多可用的 ORM,您可以在 替代方案、灵感和比较 中阅读有关其中一些的更多信息

SQL 表名

技术背景

对于 SQL 纯粹主义者来说,这有点枯燥的背景知识。请随意跳过本节。😉

当使用纯 SQL 时,通常以复数形式命名表。因此,表将被命名为 heroes 而不是 hero,因为它可能包含多行,每行包含一个 hero。

然而,SQLModel 和许多其他类似工具可以从您的代码自动生成表名,您将在本教程后面看到。

但是,此名称将从类名派生而来。对于类使用单数名称(例如 class Hero,而不是 class Heroes)是很常见的做法。为 class Hero 这样的类使用单数名称也使您的代码更直观。

您将比内部表名更多地看到您自己的代码,因此可能最好保留代码/类约定而不是 SQL 约定。

因此,为了保持一致性,我将继续使用 SQLModel 将生成的相同表名。

提示

您也可以覆盖表名。您可以在高级用户指南中阅读相关内容。