ORM 白皮书
ORM 白皮书
大纲
你可以在 MASONITE ORM 存储库 为项目做出贡献
工作流程
我会先从高层次讨论整体的流程,然后可以分别讨论每个部分。
你可以从几条不同的路径开始。并非一切都从模型级别开始。你可以直接使用查询构建器类来构建你的查询。查询构建器类正是这样:构建查询的类。因此,你将与这个类进行交互(稍后会详细介绍),它会为类设置诸如 wheres、limits、selects 等内容,然后将所有这些传递给构建查询。
Model 模型
首先我们来谈谈 Model
的流程。 Model
可能是大多数人大部分时间都会使用的。 Model
基本上是表格周围的包装实体。所以 1 个表可能等于 1 个模型。 users
表将具有 User
模型,而 articles
表将具有 Article
模型。
Model
的有趣之处在于它只是 QueryBuilder
类的一个壳。大多数时候,你在 Model
上调用某些东西实际上只是立即构建一个查询构建器类并传递其余的调用。理解这一点很重要:
例如,
user = User
user #== <class User>
user.where('id', 1) #== <masonite.orm.Querybuilder object>
由于它返回一个查询构建器,我们可以简单地构建这个类并链上一大堆方法:
user.where('id', 1).where('active', 1) #== <masonite.orm.Querybuilder object>
最后,当我们完成构建查询时,我们将调用一个 .get()
,它基本上是一个执行命令:
user.select('id').where('id', 1).where('active', 1).get() #== <masonite.orm.Collection object>
当你调用 get
时,查询构建器将传递你构建的所有内容(1 个选择,2 个 where 语句)并将它们传递给 Grammar
类。 Grammar
类负责循环 3 条语句并将它们编译成将运行的 SQL 查询。因此,Grammar
类将编译一个如下所示的查询:
SELECT `id` FROM `users` WHERE `id` = '1' AND `active` = 1
如果它需要构建一个 Qmark 查询(带有问号的查询,它将被查询绑定替换以防止 SQL 注入),那么它将如下所示:
SELECT `id` FROM `users` WHERE `id` = '?' AND `active` = '?'
并带有 2 个查询绑定:
(1,1)
一旦我们得到查询,我们就可以将查询传递给连接类,该类将连接到 MySQL 数据库以发送查询。
然后,我们将从查询中取回一个字典并与原始模型叠加。当你对模型进行叠加时,这仅意味着我们将字典结果设置到模型中,因此当我们访问诸如 user.name
之类的内容时,我们将获得用户的名称。可以将其视为将字典加载到类中,以便以后在访问和设置期间使用。
语法类
语法类是负责将属性编译成 SQL 语句的类。语法类用于 DML 语句(选择、插入、更新和删除)。语法不用于 DDL 语句(create and alter)。然后,SQL 语句将返回给它的任何名称(如QueryBuilder
类),然后传递给连接类以进行数据库调用并返回结果。同样,语法类只负责将查询编译成字符串。只需获取传递给它的属性并循环它们并将它们编译成查询。
语法类将负责 SQL 和 Qmark。同样,SQL 看起来像这样:
SELECT * FROM `users` where `age` = '18'
Qmark 是这样的:
SELECT * FROM `users` where `age` = '?'
然后,Qmark 查询将被传递给带有绑定元组的连接类,例如 (18,)
。这有助于防止 SQL 注入攻击。 所有传递给连接类的查询都应该是 qmark 查询。编译 SQL 确实是为了在开发时进行调试。将直接 SQL 传递到连接类可能会使查询对 SQL 注入开放。
任何值都应该能够被标记。这是在语法类中通过用 '?'
替换值然后将值添加到绑定来完成的。语法类知道它应该通过在整个语法类中传递 qmark 布尔变量来进行 qmark。
语法类也确实是一个抽象。所有繁重的工作都在 BaseGrammar
类中完成。子类(如 MySQLGrammar
和 PostgresGrammar
等)实际上只包含 sql 字符串的格式。
目前,每种支持的语法都有 2 个不同的语法类。一种用于普通查询,一种用于模式查询。他们可能是一个大类,但类会很大,很难维持这样一个对所有事情负责的父类。这也使得首先构建用于查询(选择、更新、删除等)的语法,然后再支持模式构建变得更加困难。
几乎所有 SQL 都基本相同,但某些语法的格式或位置略有不同。这就是为什么我们使用的这种结构如此强大并且以后易于扩展或修复的原因。
例如,MySQL 对带有限制的 select 语句具有以下格式:
SELECT * from `users` LIMIT 1
但是 Microsoft SQL Server 有这个:
SELECT TOP 1 * from `users`
请注意,SQL 基本相同,但限制语句位于 SQL 中的不同位置。
我们可以通过指定一般:选择、插入、更新和删除格式来实现这一点,这样我们以后可以更好地组织和交换放置。我们通过使用 Python 关键字字符串插值来做到这一点。例如,让我们分解为更底层的方式来了解如何实现这一点:
下面是 MySQL 语法类的 select 语句结构。为了解释起见,我将对此进行简化,但只知道这还包含连接的格式,以 {joins}
、{group_by}
的形式分组:
MySQL:
def select_format(self):
return "SELECT {columns} FROM {table} {limit}"
Microsoft SQL:
def select_format(self):
return "SELECT {limit} {columns} FROM {table}"
只需更改此字符串中的顺序,我们就可以替换生成的 SQL 语句的格式。最后一步是准确更改单词的含义。
同样,MySQL 是 LIMIT X
,Microsoft 是 TOP X
。我们可以通过在自己的方法中指定差异来实现这一点。请记住,这些都在语法类的子类中。 Mysql 在 MySQLGrammar
中,Microsoft 在 MSSQLGrammar
中
MySQL:
# MySQLGrammar
def limit_string(self):
return "LIMIT {limit}"
而 Microsoft 的:
# MSSQLGrammar
def limit_string(self):
return "TOP {limit}"
现在我们已经将这些差异抽象为它们自己的类和类方法。现在,当我们编译字符串时,一切都准备就绪。此代码片段位于BaseGrammar
类(它调用我们在上面构建的支持的语法类)中。
# 一切都完全抽象成它自己的类和类方法。
sql = self.select_format().format(
columns=self.process_columns(),
table=self.process_table(),
limit=self.process_limit()
)
让我们删除抽象并稍微分解变量,以便我们可以看到更底层的内容:
MySQL:
"SELECT {columns} FROM {table} {limit}".format(
columns="*",
table="`users`",
limit="LIMIT 1"
)
#== 'SELECT * FROM `users` LIMIT 1'
Microsoft:
"SELECT {limit} {columns} FROM {table} ".format(
columns="*",
table="`users`",
limit="TOP 1"
)
#== 'SELECT TOP 1 * FROM `users`'
因此请注意,对于具有不同 SQL 结构的数据库,可以根据每种语法更改抽象。你只需要更改字符串返回方法的响应和 select_format
方法的结构
格式化字符串
子语法类有一大堆这样的语句来获得像表格式这样的细节实现。
子语法类中的大多数方法实际上就是这些字符串。
MySQL 表的格式如下:
`users`
Postgres 和 SQLite 表的格式如下:
"users"
来自 Microsoft 的数据库表的格式如下:
[users]
所以我们在语法类上也有完全相同的东西,如下所示:
table = self.table_string().format(table=table)
对于 MySQL,未抽象的代码看起来像这样:
# MySQL
table = "`{table}`".format(table=table)
对于 Microsoft 的数据库:
# MSSQL
table = "[{table}]".format(table=table)
语法类中有一大堆这些方法,用于各种各样的事情。数据库之间可能存在的任何差异都被抽象到这些方法中。
编译方法
There are a whole bunch of methods that begin with process_
or _compile_
so let's explain what those are.
这里有一整套以 PROCESS_
或 _COMPILE_
开头的方法,所以让我们解释一下它们是什么。
现在,所有语法之间的差异都被抽象到子语法类中,所有繁重的列表都可以在父语法类的 BaseGrammar
类中完成,这实际上是为所有语法编译查询的引擎。
这个 BaseGrammar
类负责执行上一节中的实际编译。所以这个类实际上只有一堆类,如 process_wheres
、process_selects
等。这些是更多的支持方法,可以帮助处理 _compile_
方法的 sql 字符串。
还有一些以 _compile_
开头的方法。这些是负责编译实际的相应查询。这个类的核心实际上在于_compile_select
、_compile_create
、_compile_update
、_compile_delete
方法。
让我们先带回未抽象的版本:
def _compile_select(self):
"SELECT {columns} FROM {table} {limit}".format(
columns="*",
table="`users`",
limit="LIMIT 1"
)
#== 'SELECT * FROM `users` LIMIT 1'
现在让我们开始抽象,直到我们得到类中真正的内容。
现在有了支持的 _compile
方法,该方法看起来会是什么样子:
def _compile_select(self):
"SELECT {columns} FROM {table} {wheres} {limit}".format(
columns=self.process_columns(),
table=self.process_from(),
limit=self.process_limit()
wheres=self.process_wheres
)
#== 'SELECT * FROM `users` LIMIT 1'
所以请注意,我们有许多 _compile
方法,但它们主要是为了支持 select、create 或 alter 语句的主要编译。
现在,最终该类中的方法看起来是这样的:
def _compile_select(self):
self.select_format().format(
columns=self.process_columns(),
table=self.process_from(),
limit=self.process_limit()
wheres=self.process_wheres
)
#== 'SELECT * FROM `users` LIMIT 1'
模型和查询构建器
模型和查询构建器紧密相连。在几乎所有情况下,模型上的单个方法都会立即将所有内容传递给 QueryBuilder
类。
只要知道模型实际上只是 QueryBuilder
的一个小代理即可。模型上的大多数方法只是调用了 QueryBuilder
,因此我们将重点关注 QueryBuilder
。
模型类唯一的作用就是包含一些小的设置,比如表名、数据库调用后的属性(查询结果
)以及一些其他小的设置,如要使用的连接和语法。
不过,了解类(cls
)和对象实例之间的差异非常重要。请务必阅读下面的章节。
元类
在模型中,我们使用的一个较为复杂的魔法技巧是在Model
类(所有User
和Article
模型将继承的基类)上设置一个元类。这实际上在首次调用方法之间创建了一个中间件。由于在处理不同的类实例和类类时完成所有操作非常困难,因此在继续之前,最好先捕获调用并将其转换为实例。
这很难解释,但让我们看看它真正解决了什么:
我们可以对所有模型都这样做:
class User(Model):
pass
然后执行模型调用:
result = User().where('...')
但它看起来不如这样简洁:
result = User.where('...')
(同样为了与 Orator
的向后兼容性,如果我们不支持它,那将是一个巨大的变化)。
因此,如果您查看 Model.py
类,我们有一个继承的元类(如果您查看文件,您会注意到),它实际上做了一些神奇的事,实际上在调用任何方法之前实例化了该类。这类似于您可以绑定到的任何普通的 Python 钩子,如 __getattr__
。
这使得处理 cls
和 self
更加容易。尽管在某些特殊用例中我们需要直接处理 cls
,这就是为什么您会在某些模型方法上看到一些 @classmethod
装饰器。
经过(Pass Through)
我们提到该模型只是构造了一个查询构建器,并且基本上将所有内容都传递给了 QueryBuilder
类。
但问题是,当你调用类似 User.where(..)
的方法时,它会调用 User
类的 where
方法。由于模型类实际上没有 where
方法,因此它将挂接到模型类上的 __getattr__
。从那里我们捕获了一套位于模型的 __passthrough__
属性中的不同的方法,并将其传递给查询构建器。理解这一点很重要。
查询构建器
QueryBuilder
类负责构建查询,因此它会有一整套属性,这些属性最终将传递给 Grammar
类并编译为 SQL。然后,该 SQL 将被传递给 Connection
类,并执行数据库调用以返回结果。
QueryBuilder
类实际上是 ORM
的核心,它真的需要做到完美,具有最多的功能并且需要最多的时间来构建和完善。
例如,当你在模型上调用 where
时,它会将信息传递给查询构建器并返回 QueryBuilder
类。
user = User.where('age', 18)
#== <masonite.orm.QueryBuilder object>
所有其他调用都将在该查询构建器对象上完成:
user = User.where('age', 18).where('name', 'Joe').limit(1)
#== <masonite.orm.QueryBuilder object x100>
最后,当你调用像 .get()
这样的方法时,它会返回一个结果集合。
user = User.where('age', 18).where('name', 'Joe').limit(1).get()
#== <masonite.orm.Collection object x101>
如果你调用 first()
它将返回单个模型:
user = User.where('age', 18).where('name', 'Joe').limit(1).first()
#== <app.User object x100>
因此,我们再次使用 QueryBuilder
来构建查询,然后再执行它。
表达式类
有几个不同的类将有助于从 Grammar
类编译 SQL。这些实际上只是具有不同属性的各种类。它们是仅供内部使用的类,用于更好地编译 BaseGrammar
类内部的内容,因为我们使用了诸如 isinstance
检查和属性条件之类的东西。在开发应用程序时,你不会直接使用这些。这些类是:
QueryExpression
- 用于编译where
语句。HavingExpression
- 用于编译Having
语句。JoinExpression
- 用于编译Join
语句。UpdateExpression
- 用于编译Update
语句。SubSelectExpression
- 用于编译子查询。子查询可以放在where
语句中,使复杂的where
语句更强大。SubGroupExpression
- 用于传递到可调用对象中以便稍后执行。这对于子查询也很有用,但这只是对可调用对象的抽象层。
这些仅在构建查询的不同部分时使用。当在语法类上运行 _compile_wheres
、 _compile_update
和其他方法时,这些方法只是使获取所需数据变得更加简单,并且不会过于通用,不会使复杂的用例难以编码。
类如何相互交互
Model -> QueryBuilder
一旦访问,模型会将其上设置的任何内容直接传递给查询构建器。之后的所有调用都将基于一个新的查询构建器类。所有查询构建都将在此类上完成。
QueryBuilder -> Grammar
更清楚地说,一旦我们完成了查询的构建,然后调用 .get()
或 .first()
,所有的 wheres
、 selects
、group_by
等都会被传递到正确的语法类,如 MySQLGrammar
,然后将编译成 SQL 字符串。
QueryBuilder -> Connection
然后从语法类返回的那个 SQL 字符串与来自语法类的绑定一起发送到连接类。然后我们得到字典形式的结果。不过,我们不想使用一堆字典,我们希望使用更多模型。
混合模型(QueryBuilder Hydrating)
如果传入了模型,则返回响应时的 QueryBuilder
对象还负责混合模型。如果没有模型传入初始化程序,那么它将只返回一个字典或列表。Hydrating
实际上只是用数据填充虚拟模型的一个奇特词。我们真的不想在我们的项目中使用字典,所以我们采用字典响应并将其推入模型并返回模型。现在我们有了一个比简单的字典更有用的类。
有时我们会有多个结果(一个字典列表),我们只需遍历该字典,然后为每个字典填充不同的模型。所以如果我们有 5 个结果,我们会遍历每个结果并建立一个包含 5 个填充后的模型集合。我们通过调用 .hydrate()
方法来做到这一点,并将该实例与字典相结合。
关联
关联仍在改进中,可能发生变化
关联有点神奇,它使用了很多基础的 Python 魔法技巧来实现。我们需要一些 Python 类来管理这些魔法技巧以确定关联类的特性。例如我们有这样的关系:
class User:
@belongs_to('local_key', 'foreign_key')
def profile(self):
return Profile
这很无辜,但是当您访问这样的内容时,我们希望:
user = User.find(1)
user.profile.city
但是我们也希望可以扩展这种关系:
user = User.find(1)
user.profile().city
因此,我们需要同时访问并调用该属性。我知道这很奇怪。我们如何附加属性:
- 在方法中找到正确的模型
- 构建查询
- 找到要获取的正确的外键
- 返回准备就绪的完成填充的模型
- 但当你调用它时,执行 wheres 并返回查询构建器。
为此,我们使用 __get__
魔术方法进行属性访问,每当访问属性时都会调用该方法。然后我们可以接管这个钩子并返回我们需要的任何东西。在本例中,是一个完成填充的模型或查询构建器。
关联类
解释关联类很有用。
我们有一个 BaseRelationship
类,它实际上包含了我们实际装饰器工作所需的所有魔法技巧。
然后我们有一个 BelongsTo
关联(它是在 __init__.py
文件中导入的 as belongs_to
,所以这是装饰器中名称更改的来源),它有一个简单的 apply_query
方法,使用模型 QueryBuilder
返回连接所需的查询。这里我们有 foreign
和 owner
变量。 foreign
是关联类(在本例中为 Profile
)和 owner
是当前模型(在本例中为 User
)。
查询被应用并以字典或列表的形式从查询构建器返回结果(对于一个结果,它将是一个字典,如果返回多个,它将是一个列表)。然后正常的过程会顺其自然。如果是字典,它将返回一个混合模型,如果返回一个列表,它将返回一个混合模型的集合。
架构类
Schema
类负责创建和更改表,因此构建普通查询构建器类的语法略有不同。这里我们没有像 where
和 limit
这样的词。不是采用以下格式:
CREATE TABLE `table` (
`name` VARCHAR(255)
)
类
现在我们来讨论一下这三个基本类的每个类是如何相互通信的。
Schema -> Blueprint
Schema
类负责指定要使用的表和/或连接。然后它会将这些信息传递给 Blueprint
类,这实际上与 Model
和 QueryBuilder
之间的关系相同。 Schema
类还负责设置 create
或 alter
模式。如果你分别使用 Schema.create('users')
或 Schema.table('users')
,则会设置此项。
正因为如此,才有了平台类。 SQLitePlatform
, MySQLPlatform
等。 这些类有 _create_sql and compile_alter_sql 方法。 这些方法采用单个表类,与BluePrint类构建的表类相同。
这个表类具有 added_columns , removed_indexes 等,我们可以使用这些方法来构建我们的 ALTER 和 CREATE 语句。
例如, Postgres 要求一次运行一条用于添加列的 ALTER 语句。 因此我们不能使用一个ALTER 查询添加多个列,所以我们需要遍历所有的 Table.added_columns 并为每一列创建多个更改查询。
编译
最后,我们需要通过编译执行 blueprint.to_sql()
来完成查询。这将根据由 Schema
类之前设置的最初内容构建 create
或 alter
查询。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。