发布于 2026-01-06 7 阅读
0

我正在为 Deno 构建一个 ORM!我是如何构建的?结论 谢谢 Fadhil,太棒了!DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

我正在为 Deno 构建一个 ORM!

我该如何搭建它?

结论

谢谢你,法迪尔,太棒了!

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

各位开发者们,大家好!希望你们一切都好。

我只是想告诉你,我目前正在做一个开源项目,我觉得这个项目非常棒。

我正在为今年最热门但尚不成熟的技术之一——Deno——构建一个 ORM 库。

它叫棉花,但别问我为什么会想到这个名字,我也不知道。

我对这个项目感到非常兴奋,我想和你们分享我的经验。

说实话,我启动这个项目并不是因为我能做到。见鬼,我甚至不知道怎么写一条原始的 SQL 查询语句来连接两个不同的表。

过去几年,我感觉自己对开源社区没什么贡献,尽管我一直都在使用开源软件。对此我感到有点愧疚。

所以,我决定为我目前非常喜欢的项目 Deno 做贡献。

我该如何搭建它?

构建这个项目最难的部分在于,网上没有任何教程教你“如何构建 ORM”。因此,我学习这方面知识的唯一途径就是阅读 GitHub 上数百行现有的 ORM 代码,例如TypeORM(TypeScript)、Eloquent(PHP)和ActiveRecord(Ruby)。

我终于明白了,这些巨大的工具可以分为三个主要部分。

  1. 查询生成器
  2. 数据库适配器
  3. 模型

我知道,它们大多数都有自己独特而复杂的结构,但至少这是我可以从中开始的。

查询生成器

查询构建器是一个主要任务是构建原始 SQL 查询的类。它是所有 ORM 的基础,因此也是最需要理解的部分之一。

你可能见过这种代码。例如,在Laravel中,这段代码会被转换成可执行的 SQL 查询语句,从而大大简化开发过程。

DB::table('users')->where('name', 'John')->first();
// SELECT * FROM users WHERE name = 'John' LIMIT 1;
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

如果您不想执行查询,只想获取原始 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()
Enter fullscreen mode Exit fullscreen mode

数据库适配器

数据库适配器允许您使用相同的代码结构来操作不同类型的数据库。有时,您可能希望在本地计算机上使用SQLite开发应用程序,因为它设置起来非常方便。但是,当您将应用程序部署到生产环境时,您可能需要使用能够处理更复杂数据的数据库,例如PostgreSQLMySQL

即使使用相同的查询语言 SQL,每个数据库都有其独特的连接和执行查询的方式。

所以数据库适配器的作用就是帮你处理这件事。你只需要编写一次代码,就可以在任何地方使用它,无需做任何修改。

目前,Cotton 支持三种主流数据库:MySQL、PostgreSQL 和 SQLite。接下来,我将重点介绍这三种数据库,然后再深入探讨 Oracle 等其他数据库。

为了在 Cotton 中实现此功能,我正在使用 Deno 的一些现有数据库驱动程序。

由于这些驱动程序具有不同的 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>;
}
Enter fullscreen mode Exit fullscreen mode

每个数据库适配器都需要遵循这个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...
})
Enter fullscreen mode Exit fullscreen mode

由于这些适配器都遵循同一个抽象类,我们可以使用相同的方法与不同类型的数据库进行交互。

await adapter.execute(`
  CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email VARCHAR(255),
  );
`)
Enter fullscreen mode Exit fullscreen mode

现在,我可以创建一个名为 `helper` 的辅助函数connect,只需传递一个参数即可自动决定使用哪个适配器。

const db = await connect({
  type: "sqlite", // available type: 'mysql', 'postgres', and 'sqlite'
  database: "./db.sqlite3",
  // other...
});
Enter fullscreen mode Exit fullscreen mode

模型

最后,ORM 的最后一个组成部分是模型。它构建在查询构建器之上,也是开发人员大部分时间都在处理的部分。

如果您已经熟悉 MVC 模式,那么模型(Model)就是 MVC 中的 M。模型负责处理对单个数据库表的 CRUD 操作。每个 ORM 框架对模型的实现方式都不同,以下是一些示例。

ActiveRecord

# Post model
class Post < ApplicationRecord
end

# Fetch all posts
Post.all
Enter fullscreen mode Exit fullscreen mode

雄辩的 ORM

// Post model
class Post extends Model {}

// Fetch all posts
Post::all();
Enter fullscreen mode Exit fullscreen mode

TypeORM

// Post model
@Entity()
class Post extends BaseEntity {
  @Field()
  public title: string;

  @Field()
  public content: string;
}

// Fetch all posts
const posts = await Post.find()
Enter fullscreen mode Exit fullscreen mode

一旦我构建了 MVP 查询构建器和 MVP 数据库适配器,我就可以将它们结合起来,通过单个对象执行 CRUD 操作。

以下是我的做法:

class User extends Model {
  static tableName = 'users';

  email: string;
}

db.addModel(User)

const users = await User.find()
Enter fullscreen mode Exit fullscreen mode

首先,您需要创建一个继承自该类的模型Model。您需要为该模型指定表名。此外,还有其他可选配置,例如primaryKey设置默认主键字段。

然后,使用方法注册模型类addModel。这样,你的模型就可以向数据库执行 SQL 查询。最后,你可以users通过这个模型对表执行 CRUD 操作。

结论

虽然听起来内容很多,但实际上还有很多关键功能我尚未实现。例如模式迁移、缓存,甚至像模型和查询构建器这样的基本功能都还没有完成。

我会尽量让你了解这个有趣项目的进展。如果你也渴望学习新知识,那就让我们一起改进这个项目吧!

任何捐助都意义重大。

每个人都会写出糟糕的代码,但只要我们不断互相改进,好事就会发生。

最后,你对此有何看法?请在下方评论区留言!

文章来源:https://dev.to/rahmanfadhil/im-building-an-orm-for-deno-4jm1