跳到内容
Sutando
Esc
↑↓导航↵打开⌘J预览

Node.js Active Record ORM 完整指南

Active Record 是最直观的数据库交互模式之一。如果你用过 Laravel 的 Eloquent 或 Ruby on Rails,你就已经知道它有多高效。在本指南中,我们将探讨什么是 Active Record、它与其他 ORM 模式的区别,以及如何在 Node.js 中使用 Sutando…


Active Record 是最直观的数据库交互模式之一。如果你用过 Laravel 的 Eloquent 或 Ruby on Rails,你就已经知道它有多高效。在本指南中,我们将探讨什么是 Active Record、它与其他 ORM 模式的区别,以及如何在 Node.js 中使用 Sutando 来实践这一模式。

什么是 Active Record 模式?

Active Record 是一种设计模式,每个模型类对应一张数据库表,每个实例代表一行记录。模型本身负责持久化——你直接在实例上调用 save()、delete()、update() 等方法。

const user = new User();
user.name = 'Alice';
user.email = 'alice@example.com';
await user.save(); // INSERT INTO users ...

这与 Data Mapper 模式(如 Prisma、TypeORM 的 EntityRepository 或 Java 的 Hibernate)有本质区别——后者由独立的映射器/仓库层来处理数据库操作:

// Data Mapper 风格(如 Prisma)
const user = await prisma.user.create({
  data: { name: 'Alice', email: 'alice@example.com' }
});

Active Record vs Data Mapper:哪个更好?

没有绝对的“更好”,各有取舍:

维度 Active Record Data Mapper
简洁性 高——方法直接在模型上 中等——需要独立的仓库层
可测试性 好——可以 mock 模型方法 优秀——可以注入仓库
耦合度 模型知道数据库的存在 模型不感知持久化层
学习曲线 低,适合初学者 较高——概念更多
最适合 快速开发、CRUD 应用 复杂领域、企业级应用

对于大多数 Node.js 项目——尤其是 API、SaaS 产品和原型开发——Active Record 的简洁性是巨大优势。你写的代码更少,迭代更快,认知负担更低。

Node.js 中的 Active Record 实现

Node.js 生态有几个 Active Record 实现:

1. Sutando ORM

Sutando 是 Laravel Eloquent 在 Node.js 中最忠实的移植。它支持 MySQL、PostgreSQL 和 SQLite,功能包括:

  • 流畅的查询构造器,支持 where、orderBy、with(预加载)
  • 模型关联:hasMany、belongsTo、hasOne
  • 软删除、模型事件/钩子、全局作用域
  • 数据库迁移和 Schema 构造器
import { Model } from 'sutando';

class User extends Model {
  table = 'users';
  
  relationPosts() {
    return this.hasMany(Post);
  }
}

// 查询
const users = await User.query().where('active', true).get();

// 创建
const user = new User();
user.name = 'Alice';
await user.save();

// 关联
const user = await User.query().with('posts').find(1);

2. AdonisJS Lucid

Lucid 是 AdonisJS 框架内置的 ORM,同样是 Active Record 风格,也受 Eloquent 启发。但它与 AdonisJS 生态紧密耦合——无法在独立的 Express 或 Fastify 项目中使用。

3. TypeORM(Active Record 模式)

TypeORM 同时支持 Active Record 和 Data Mapper 两种模式。在 Active Record 模式下,模型继承 BaseEntity:

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm';

@Entity()
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column()
  name: string;
}

const user = new User();
user.name = 'Alice';
await user.save();

TypeORM 可以用,但它严重依赖装饰器,而且因维护问题和 TypeScript 装饰器的不稳定性受到批评。

用 Active Record 构建 CRUD 应用

让我们用 Sutando 构建一个简单的博客 API,演示 Active Record 的实际用法。

安装

npm install sutando mysql2 express

数据库配置

import { sutando } from 'sutando';

sutando.addConnection({
  client: 'mysql2',
  connection: {
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '',
    database: 'blog'
  }
});

定义模型

import { Model } from 'sutando';

class Post extends Model {
  table = 'posts';
  
  relationAuthor() {
    return this.belongsTo(User, 'user_id');
  }
  
  relationComments() {
    return this.hasMany(Comment);
  }
}

class Comment extends Model {
  table = 'comments';
  
  relationPost() {
    return this.belongsTo(Post);
  }
}

class User extends Model {
  table = 'users';
  
  relationPosts() {
    return this.hasMany(Post, 'user_id');
  }
}

CRUD 操作

import express from 'express';
const app = express();
app.use(express.json());

// 创建
app.post('/posts', async (req, res) => {
  const post = new Post();
  post.title = req.body.title;
  post.content = req.body.content;
  post.user_id = req.body.userId;
  await post.save();
  res.json(post);
});

// 查询列表(带预加载)
app.get('/posts', async (req, res) => {
  const posts = await Post.query()
    .with('author', 'comments')
    .orderBy('created_at', 'desc')
    .limit(20)
    .get();
  res.json(posts);
});

// 查询单条
app.get('/posts/:id', async (req, res) => {
  const post = await Post.query().with('author', 'comments').find(req.params.id);
  if (!post) return res.status(404).json({ error: 'Not found' });
  res.json(post);
});

// 更新
app.put('/posts/:id', async (req, res) => {
  const post = await Post.query().find(req.params.id);
  if (!post) return res.status(404).json({ error: 'Not found' });
  post.title = req.body.title;
  post.content = req.body.content;
  await post.save();
  res.json(post);
});

// 删除
app.delete('/posts/:id', async (req, res) => {
  const post = await Post.query().find(req.params.id);
  if (!post) return res.status(404).json({ error: 'Not found' });
  await post.delete();
  res.json({ success: true });
});

app.listen(3000);

高级 Active Record 功能

预加载

Active Record 最强大的功能之一是预加载——在单次查询中加载关联,避免 N+1 问题:

// 不好:N+1 查询
const users = await User.query().get();
for (const user of users) {
  const posts = await user.getRelated('posts'); // 每个用户一次查询
}

// 好:总共 2 次查询
const users = await User.query().with('posts').get();

查询作用域

作用域让你定义可复用的查询约束:

class Post extends Model {
  scopePublished(query) {
    return query.where('published', true);
  }
}

// 使用
const posts = await Post.query().published().get();

模型事件

在模型生命周期中插入自定义逻辑:

Post.creating(async (post) => {
  post.slug = post.title.toLowerCase().replace(/\s+/g, '-');
});

Post.deleting(async (post) => {
  await post.related('comments').delete(); // 级联删除
});

何时选择 Active Record

Active Record 适合以下场景:

  • 你希望快速开发,尽量少写样板代码
  • 你的应用以 CRUD 为主
  • 你从 Laravel、Rails 等框架迁移过来
  • 你重视代码的可读性和自文档化
  • 你在构建原型或 MVP

考虑 Data Mapper 的场景:

  • 你有复杂的领域逻辑,需要与持久化层解耦
  • 你在构建大型企业应用,需要严格关注点分离
  • 你需要轻松替换持久化层

总结

Active Record 仍然是 Node.js 中最高产的数据库交互模式之一。通过 Sutando,你可以获得忠实的 Eloquent 风格体验——框架无关、轻量且直观。无论你是从 Laravel 迁移还是开始新的 Node.js 项目,Active Record + Sutando 都能让你专注于业务逻辑,而不是与 ORM 搏斗。

准备好试试了吗?查看 Sutando 文档 或运行 npm install sutando 立即开始。