ORM 白皮书
白皮书
ORM 白皮书大纲
- [x] 查询生成器
- [x] 模型
- [x] 模式
- [x] 语法类
- [x] 关系
目前本项目还在开发中尚未包含于 Masonite 。 本文旨在作为学习文档,以帮助希望为该项目做出贡献的人们。 本文将在 ORM 项目添加概念或修改概念时对其进行更新。它仍然是一个年轻的项目,因此概念可能会发生变化,但基础已经建立,您在此处阅读的基础可能不会改变。您可以在 Masonite ORM Repo 为本项目作贡献。
流程
我首先会在较高层次讨论流程 (flow) ,然后再分别讨论每个部分。
首先,一切都要从模型 (model) 开始。 你将会在应用中导入模型并在其上运行一些 (模型) 方法。 这些方法中的大多数仅返回一个查询构建器 (query builder) 的实例来启动流程。
举例,
user = User
user #== <class User>
user.where('id', 1) #== <masonite.orm.Querybuilder object>
由于它返回查询构建器,因此我们可以简单地在多个方法上构建此类并进行链接:
user.where('id', 1).where('active', 1) #== <masonite.orm.Querybuilder object>
最后,当我们构建查询完成后将调用 a .get()
:
user.select('id').where('id', 1).where('active', 1).get() #== <masonite.orm.Collection object>
当你调用 get 方法,构建生成器将你 ( 使用 1 select 语句, 2 where 语句 ) 构建的所有东西传给语法类。 语法类负责循环遍历这3条语句,并将它们编译为将要运行的查询语句。 因此,语法类将编译成如下查询语句:
SELECT `id` FROM `users` WHERE `id` = '1' AND `active` = 1
一旦我们得到查询语句,就可以将查询语句传递给连接类 (connection class),该连接类将连接到MySQL数据库向其发送查询任务。
然后,我们将从查询中得到字典并“水合 (hydrate) ”原始模型。当您对模型进行水合处理时,简而言之就是我们将字典结果处理成模型,因此当我们访问诸如user.name
之类的内容时,我们将获得用户的名称。可以将其视为将字典加载到稍后要获取的类中。
语法类
语法类是负责将类属性编译为SQL语句的类。然后将SQL语句提供给连接类以进行数据库调用并返回结果。同样,语法类仅负责将查询内容编译为字符串。
语法类将同时负责SQL和Qmark。 SQL看起来像这样:
SELECT * FROM `users` where `age` = '18'
Qmark是这样的:
SELECT * FROM `users` where `age` = '?'
然后,将使用诸如(18,)
之类的绑定元组将 Qmark 查询 (Qmark queries) 传递给连接类。这有助于防止SQL注入。
任何值都应该可以被 qmarked . 这是通过在语法类内部使用'?'
替换具体的值,然后将该值添加到绑定 (bindings) 中来实现的。
语法类实际上是一个抽象类。所有繁重的工作都在 BaseGrammar
类内部完成。子类(例如 MySQLGrammar
和 PostgresGrammar
等)实际上只包含sql字符串的格式。
几乎所有的 SQL 基本上都是相同的,但格式略有不同。
例如,对于具有限制的 select 语句,MySQL 具有以下格式:
SELECT * from `users` LIMIT 1
但是 Microsoft SQL Server 具有以下功能:
SELECT TOP 1 * from `users`
注意,SQL 本质上是相同的,但是 limit 语句在 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:
def limit_string(self):
return "LIMIT {limit}"
Microsoft:
def limit_string(self):
return "TOP {limit}"
现在,我们已将差异抽象到自己的类中。当我们编译字符串时,一切都准备就绪:
self.select_format().format(
columns="*",
table="`users`",
limit="1"
)
让我们删除一些抽象,以便可以更底层地看到它的作用:
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 结构的数据库,每个语法可以对抽象进行更改。
格式字符串
子语法类有很多这样的语句,用于获取较小的内容,例如表格。
子语法类中的大多数方法实际上就是这些字符串。
MySQL 表的格式为:
`users`
Microsoft 是这样的:
[users]
同样,我们在语法类上也有完全一样的东西:
table = self.table_string().format(table=table)
对于 MySQL 来说,那个抽象的看起来像这样:
table = "`{table}`".format(table=table)
对于 Microsoft 来说是这样:
table = "[{table}]".format(table=table)
其中有很多。列语句是完全一样的。
编译方法
有很多以 _compile
开头的方法,所以让我们解释一下它们是什么。
现在,语法之间的所有差异都被抽象到子语法类中,所有繁重的清单都可以在父语法类 BaseGrammar
类中完成。
这个 BaseGrammar
类负责执行以上部分中的实际编译。因此,此类实际上只有一堆类,例如_compile_wheres
,_compile_selects
等。
该类的核心实际上在于 _compile_select
,_compile_create
方法。大多数其他 _compile
方法实际上只是用于执行上述字符串内插。
看看上面用于编译查询语句的抽象格式是什么。
让我们首先返回完整版本:
"SELECT {columns} FROM {table} {limit}".format(
columns="*",
table="`users`",
limit="LIMIT 1"
)
#== 'SELECT * FROM `users` LIMIT 1'
现在该方法真正的样子是:
"SELECT {columns} FROM {table} {wheres} {limit}".format(
columns=self._compile_columns(),
table=self._compile_from(),
limit=self._compile_limit()
wheres=self._compile_wheres
)
#== 'SELECT * FROM `users` LIMIT 1'
请注意,我们有一堆 _compile
方法,但它们主要用于编译 select,create 或 alter 语句。
模型和查询生成器
模型和查询生成器实际上是携手并进的。在几乎所有情况下,模型上的单个方法都会将所有内容传递给 QueryBuilder
类。
要知道模型实际上只是 QueryBuilder
的一个小型代理。模型上的大多数方法都只是简单地调用QueryBuilder
,因此我们将重点放在 QueryBuilder
上。
模型类唯一要做的就是包含一些小设置,如表名、进行数据库调用后的属性(查询结果)以及其他一些小设置,如要使用的连接和语法。
不过,了解类 (cls
) 和对象实例之间的区别很重要。请务必阅读下面的部分。
CLS
由于稍后再实例化模型,因此通常使用 cls
变量。此 cls
变量是模型的类,而不是模型的实例。如果您不知道,则可能要研究类和实例之间的区别,但要知道的一件事是:
在类上设置的所有属性将分布到所有实例。
这意味着我们不能做诸如在模型类上设置查询结果之类的事情,因为这样做会达不到目的。每个模型都需要有自己的结果。
我们这样做的真正原因实际上只是为了视觉效果。我想这样做:
User.find(1)
并不是:
User().find(1)
第二种方法会使事情变得容易一些,但我愿意再多麻烦一点,以确保 ORM 看起来真的干净。
因此,在使用模型和查询生成器时请记住这一点
查询生成器
这个 QueryBuilder
类负责构建查询,因此它将具有大量属性,这些属性最终将传递给 grammar 类并编译为 SQL。然后,该 SQL 将被传递给 connection 类,并将进行数据库调用以返回结果。
这确实是 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>
最后,当您调用 .get()
之类的方法时,它将返回结果集合。
user = User.where('age', 18).where('name', 'Joe').limit(1).get()
#== <masonite.orm.Collection object>
如果您调用 first()
,它将返回单个模型:
user = User.where('age', 18).where('name', 'Joe').limit(1).first()
#== <app.User object>
因此,我们再次使用 QueryBuilder
来构建一个查询,然后执行它。
表达式类
有几种不同的类可以帮助从 grammar 类中编译 SQL。这些实际上只是具有不同属性的各种类。 这些类是:
QueryExpression
- 用于编译 where 语句HavingExpression
- 用于编译 Having 语句JoinExpression
- 用于编译 Join 语句UpdateExpression
- 用于编译 Update 语句SubSelectExpression
- 用于编译子查询语句. 子查询可以放在 where 语句中,使复杂的 where 语句更强大SubGroupExpression
- 用于传递给一个 callable,以便稍后执行。这对于子查询同样有用,但只是可调用的抽象层
这些仅在建立查询的不同部分时使用。 当在语法类上运行 _compile_wheres
、 _compile_update
和其他方法时,这些方法只会使获取所需数据变得更加简单,而不会因
不太通用,使困难的用例难以编写代码。
QueryBuilder -> Grammar
更清楚地说,一旦我们构建完查询,然后调用 .get()
或 .first()
,所有的 wheres、selects、group_by's 等都会被传递给正确的 grammar 类,比如 MySQLGrammar
,然后它会编译成一个 SQL 字符串。
QueryBuilder -> Connection
然后,该 SQL 字符串与 grammar 类的绑定一起发送到 connection 类。 然后,我们得到字典形式的结果。 但是,我们不想使用大量的字典,我们希望使用更多的模型。
QueryBuilder Hydrating
返回响应时的 QueryBuilder
对象也负责使您的模型水合。水合真的只是一个花哨的词,用来用数据填充虚拟模型。我们真的不想在我们的项目中使用字典,所以我们将字典响应放入一个模型中,然后返回该模型。现在我们有了一个比简单的字典有用得多的类。
有时我们有几个结果 (一个字典列表),我们只需循环该列表,然后用每个字典填充一个不同的模型。因此,如果我们有 5 个结果,我们循环遍历每个结果,并构建一个包含 5 个水合模型的集合。为此,我们调用 .hydrate()
方法,该方法创建一个新实例,并将该实例与字典合并。
关系
关系有点神奇。 我们需要做一些 Python 类管理的魔术,以弄清楚关系类的内在魔力。例如,我们有这样的关系:
class User:
@belongs_to('local_key', 'foreign_key')
def profile(self):
return Profile
这是足够纯真的,但是当您访问如下内容时,我们希望:
user = User.find(1)
user.profile.city
可以工作。我知道这很奇怪。我们如何将属性添加到:
- 在方法中找到正确的模型
- 构建查询
- 找到要提取的正确外键
- 返回完全水合的模型,随时可以使用
为此,我们使用 __get__
魔术方法来做一些装饰器和属性访问魔术,只要访问属性,就会调用该方法。 然后,我们可以劫持该钩子并返回我们需要的任何东西。 在这种情况下,是完全水合的模型。
关系类
它对解释关系类很有用。
我们有一个 BaseRelationship
类,它确实包含了实际装饰器工作所需的所有魔力。
然后,我们有一个 BelongsTo
关系(它是在 __init __.py
文件中作为 belongs_to
导入的,因此这是装饰器中名称更改的地方),它具有一个简单的 apply_query
方法,使用查询模型QueryBuilder
返回连接所需的查询。 这里我们有 foreign
和 owner
变量。 foreign
是关系类(在这种情况下为 Profile
),而 owner
是当前模型(在这种情况下为 User
)。
应用查询,并从查询构建器返回字典或列表形式的结果(对于单个结果,它将是一个字典,如果返回多个结果,则它将是一个列表)。然后,正常的过程就会顺其自然。如果是字典,它将返回一个水合模型,如果返回列表,它将返回一个水合模型集合。
模式类
The Schema class is responsible for the creation and altering of tables so will have a slightly different syntax for building a normal Query Builder class. Here we don't have things like where
and limit
. Instead of have things in the format of:
Schema 类负责表的创建和更改,因此用于构建普通的 Query Builder 类的语法会稍有不同。这里没有 where
和 limit
之类的东西。而不是采用以下格式:
CREATE TABLE `table` (
`name` VARCHAR(255)
)
因此,格式略有不同,但是我们可以执行与上述完全相同的字符串插值。我不再赘述如何做,但是知道我们又有了这样的事情:
def create_start(self):
return "CREATE TABLE {table}"
这一部分可能会再次被抽象为使用像 select 这样的完整语句,但现在它被分解得更多一些。
Classes
现在让我们来讨论每个类在这里是如何与其他类进行对话的。
Schema -> Blueprint
Schema 类负责指定表和(或)要使用的连接。 然后它将把这些信息传递给 Blueprint
类,这实际上与 Model
和 QueryBuilder
之间的关系相同。 Schema 类还负责设置 create
或 alter
模式。如果您分别使用 Schema.create('users')
或 Schema.table('users')
,则设置此项。
Blueprint
类类似于 QueryBuilder
类,因为两者都只是构建了一堆要执行的列。一个仅用于获取数据,另一个用于更改或创建表。
Schema 类将 blueprint 类称为上下文管理器。
blueprint 类将以以下格式构建:
Schema.table('users') as blueprint:
blueprint.string('name')
blueprint.integer('age')
注意,我们正在构建一个 blueprint 类。
Blueprint -> Column
blueprint 类传递给它的信息,并使用 Column
类构建一个 columns 列表。它们存储在列的元组的属性中,以便稍后由 grammar 类传递并编译为 SQL。
blueprint._columns
#== (<masonite.orm.Column object>, <masonite.orm.Column object>,)
然后,通过 Schema 类将 blueprint 类设置为 create 或 Alter。
编译
最后,我们需要编译查询,该查询只需执行blueprint即可完成。要编译sql()
,该查询将生成create
或Alter
查询,具体取决于最初由模式
以前上课。
最后,我们需要编译查询,只需通过执行 blueprint.to_sql()
即可完成,这将根据之前 Schema
类最初设置的内容构建 create
或 alter
查询。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。