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 类内部完成。子类(例如 MySQLGrammarPostgresGrammar 等)实际上只包含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 返回连接所需的查询。 这里我们有 foreignowner 变量。 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 类的语法会稍有不同。这里没有 wherelimit 之类的东西。而不是采用以下格式:

CREATE TABLE `table` (
    `name` VARCHAR(255)
)

因此,格式略有不同,但是我们可以执行与上述完全相同的字符串插值。我不再赘述如何做,但是知道我们又有了这样的事情:

def create_start(self):
    return "CREATE TABLE {table}"

这一部分可能会再次被抽象为使用像 select 这样的完整语句,但现在它被分解得更多一些。

Classes

现在让我们来讨论每个类在这里是如何与其他类进行对话的。

Schema -> Blueprint

Schema 类负责指定表和(或)要使用的连接。 然后它将把这些信息传递给 Blueprint 类,这实际上与 ModelQueryBuilder 之间的关系相同。 Schema 类还负责设置 createalter模式。如果您分别使用 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(),该查询将生成createAlter查询,具体取决于最初由模式以前上课。
最后,我们需要编译查询,只需通过执行 blueprint.to_sql() 即可完成,这将根据之前 Schema 类最初设置的内容构建 createalter 查询。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:4
讨论数量: 0
发起讨论 只看当前版本


暂无话题~