我正在为 Deno 构建一个 ORM!
我该如何搭建它?
结论
谢谢你,法迪尔,太棒了!
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
各位开发者们,大家好!希望你们一切都好。
我只是想告诉你,我目前正在做一个开源项目,我觉得这个项目非常棒。
我正在为今年最热门但尚不成熟的技术之一——Deno——构建一个 ORM 库。
它叫棉花,但别问我为什么会想到这个名字,我也不知道。
我对这个项目感到非常兴奋,我想和你们分享我的经验。
说实话,我启动这个项目并不是因为我能做到。见鬼,我甚至不知道怎么写一条原始的 SQL 查询语句来连接两个不同的表。
过去几年,我感觉自己对开源社区没什么贡献,尽管我一直都在使用开源软件。对此我感到有点愧疚。
所以,我决定为我目前非常喜欢的项目 Deno 做贡献。
我该如何搭建它?
构建这个项目最难的部分在于,网上没有任何教程教你“如何构建 ORM”。因此,我学习这方面知识的唯一途径就是阅读 GitHub 上数百行现有的 ORM 代码,例如TypeORM(TypeScript)、Eloquent(PHP)和ActiveRecord(Ruby)。
我终于明白了,这些巨大的工具可以分为三个主要部分。
- 查询生成器
- 数据库适配器
- 模型
我知道,它们大多数都有自己独特而复杂的结构,但至少这是我可以从中开始的。
查询生成器
查询构建器是一个主要任务是构建原始 SQL 查询的类。它是所有 ORM 的基础,因此也是最需要理解的部分之一。
你可能见过这种代码。例如,在Laravel中,这段代码会被转换成可执行的 SQL 查询语句,从而大大简化开发过程。
DB::table('users')->where('name', 'John')->first();
// SELECT * FROM users WHERE name = 'John' LIMIT 1;
SQL很复杂,作为开发人员,我们不想处理复杂的东西。让库来帮我们完成这些繁重的工作就好。
编写查询构建器是我为这个项目做的第一件事。当然,这并非易事。为了实现这个功能,我需要学习很多 SQL 知识。但总的来说,我非常享受这个过程。
Cotton 的查询构建器最初版本只能构建带有WHEREANDLIMIT约束的查询,这非常糟糕。但这项功能发展迅速,随着时间的推移,Cotton 正在实现越来越多的约束。
事实证明,编写查询构建器本身并没有我想象的那么难。难点在于如何实现所有可用的查询约束。不过对我来说,这只是时间问题,我们迟早会实现的。
queryBuilder在 Cotton 中,一旦使用该函数连接到数据库,即可访问查询构建器connect。
const db = await connect({
type: 'sqlite',
database: './db.sqlite3'
})
// SELECT * FROM users WHERE email = 'a@b.com' LIMIT 5;
await db.queryBuilder('users')
.where('email', 'a@b.com')
.limit(5)
.execute()
如果您不想执行查询,只想获取原始 SQL 语句,可以使用该toSQL方法。
// SELECT * FROM users WHERE email = 'a@b.com' LIMIT 5;
const sql: string = db.queryBuilder('users')
.where('email', 'a@b.com')
.limit(5)
.toSQL()
数据库适配器
数据库适配器允许您使用相同的代码结构来操作不同类型的数据库。有时,您可能希望在本地计算机上使用SQLite开发应用程序,因为它设置起来非常方便。但是,当您将应用程序部署到生产环境时,您可能需要使用能够处理更复杂数据的数据库,例如PostgreSQL或MySQL。
即使使用相同的查询语言 SQL,每个数据库都有其独特的连接和执行查询的方式。
所以数据库适配器的作用就是帮你处理这件事。你只需要编写一次代码,就可以在任何地方使用它,无需做任何修改。
目前,Cotton 支持三种主流数据库:MySQL、PostgreSQL 和 SQLite。接下来,我将重点介绍这三种数据库,然后再深入探讨 Oracle 等其他数据库。
为了在 Cotton 中实现此功能,我正在使用 Deno 的一些现有数据库驱动程序。
- SQLite3 (通过sqlite)
- MySQL 和 MariaDB (通过deno_mysql)
- PostgresQL (通过postgres)
由于这些驱动程序具有不同的 API 集,我编写了一个适配器类,可以处理常见的数据库操作,例如执行connectSQL语句和从数据库中获取记录。disconnectexecutequery
export abstract class BaseAdapter {
/**
* Run SQL query and get the result
*
* @param query SQL query to run (ex: "SELECT * FROM users;")
* @param values Bind values to query to prevent SQL injection
*/
public abstract query<T>(query: string, values?: any[]): Promise<T[]>;
/**
* Execute SQL statement and save changes to database
*
* @param query SQL query to run (ex: "INSERT INTO users (email) VALUES ('a@b.com');")
* @param values Bind values to query to prevent SQL injection
*/
public abstract execute(query: string, values?: any[]): Promise<void>;
/**
* Connect database
*/
public abstract connect(): Promise<void>;
/**
* Disconnect database
*/
public abstract disconnect(): Promise<void>;
}
每个数据库适配器都需要遵循这个BaseAdapter类,这使得我们能够使用同一套 API。因此,我最终创建了三个实现了相同方法的数据库驱动程序类。每个类看起来都差不多:
// Mysql
const adapter = new MysqlAdapter({
database: 'test',
password: 'test',
hostname: '127.0.0.1'
// other config...
})
// Postgres
const adapter = new PostgresAdapter({
database: 'test',
password: 'test',
hostname: '127.0.0.1'
// other config...
})
// Sqlite
const adapter = new SqliteAdapter({
database: './db.sqlite3'
// other config...
})
由于这些适配器都遵循同一个抽象类,我们可以使用相同的方法与不同类型的数据库进行交互。
await adapter.execute(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email VARCHAR(255),
);
`)
现在,我可以创建一个名为 `helper` 的辅助函数connect,只需传递一个参数即可自动决定使用哪个适配器。
const db = await connect({
type: "sqlite", // available type: 'mysql', 'postgres', and 'sqlite'
database: "./db.sqlite3",
// other...
});
模型
最后,ORM 的最后一个组成部分是模型。它构建在查询构建器之上,也是开发人员大部分时间都在处理的部分。
如果您已经熟悉 MVC 模式,那么模型(Model)就是 MVC 中的 M。模型负责处理对单个数据库表的 CRUD 操作。每个 ORM 框架对模型的实现方式都不同,以下是一些示例。
ActiveRecord
# Post model
class Post < ApplicationRecord
end
# Fetch all posts
Post.all
雄辩的 ORM
// Post model
class Post extends Model {}
// Fetch all posts
Post::all();
TypeORM
// Post model
@Entity()
class Post extends BaseEntity {
@Field()
public title: string;
@Field()
public content: string;
}
// Fetch all posts
const posts = await Post.find()
一旦我构建了 MVP 查询构建器和 MVP 数据库适配器,我就可以将它们结合起来,通过单个对象执行 CRUD 操作。
以下是我的做法:
class User extends Model {
static tableName = 'users';
email: string;
}
db.addModel(User)
const users = await User.find()
首先,您需要创建一个继承自该类的模型Model。您需要为该模型指定表名。此外,还有其他可选配置,例如primaryKey设置默认主键字段。
然后,使用方法注册模型类addModel。这样,你的模型就可以向数据库执行 SQL 查询。最后,你可以users通过这个模型对表执行 CRUD 操作。
结论
虽然听起来内容很多,但实际上还有很多关键功能我尚未实现。例如模式迁移、缓存,甚至像模型和查询构建器这样的基本功能都还没有完成。
我会尽量让你了解这个有趣项目的进展。如果你也渴望学习新知识,那就让我们一起改进这个项目吧!
任何捐助都意义重大。
每个人都会写出糟糕的代码,但只要我们不断互相改进,好事就会发生。
最后,你对此有何看法?请在下方评论区留言!
文章来源:https://dev.to/rahmanfadhil/im-building-an-orm-for-deno-4jm1