为啥使用 Drizzle Orm 而不是 TypeOrm?
- 类型安全性更强
- Drizzle ORM 提供了完全类型安全的查询构建器
- 所有的查询都在编译时进行类型检查,减少运行时错误
- 性能更优
- Drizzle ORM 是一个轻量级的 ORM,没有太多额外的抽象层
- 查询性能比 TypeORM 更好,因为它更接近原生 SQL
- 内存占用更小,启动时间更快
- 更好的开发体验
- 不需要装饰器,减少了代码的复杂性
- 更好的 IDE 支持和自动补全
- 迁移工具更简单易用
- 同时支持类似 SQL 和关系型的查询语法。
从 npm 下载量对比,可以看出来 drizzle-orm 相对较新,而且包体积差不多是 TypeOrm 的 1/3。
最戳我的是它的学习曲线较低,因为支持类似SQL的语法。
而且它的官网首页 [Developers love Drizzle ORM] 部分特别幽默,恨也是一种爱是吧。
ok,开始搭配 nest.js 学习 drizzle-orm。为了简便起见,使用 postgres,因为太多公司提供 postgres free tier,直接远程连接就不用本地运行数据库服务了。
接下来会创建一个类似博客的数据库项目,有用户表 (users),用户个人主页表 (profileInfo),博客表 (posts),评论表 (comments),用户分组表 (groups),用户和用户分组关联表 (usersToGroups)。
学习表与表之间的关联关系,如何造模拟数据,最后使用 nest.js 进行 crud。
初始化
初始化 nestjs 项目:
nest new learn-drizzle
安装依赖:
npm i drizzle-orm pg
npm i -D drizzle-kit @types/pg # 迁移的时候要使用 drizzle-kit
为drizzle orm新增一个module,用于配置数据库的连接。
nest g module /database/drizzle
使用 Neon 的免费 postgres 数据库。
将neon的连接串存放在 .env 文件中:
# 连接串示例
POSTGRES_URL = 'postgresql://neondb_owner:npg_O5N@ep-silent-hazer.ap-southeast-1.aws.neon.tech/neondb?sslmode=require'
安装 npm i @nestjs/config 读取 .env 文件,然后在 app.module.ts 中引入 ConfigModule
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
在 drizzle.module.ts文件中配置和初始化Drizzle ORM的数据库连接:
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
export const Drizzle = Symbol('drizzle-connection');
@Module({
providers: [
{
provide: Drizzle,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const databaseUrl = configService.get<string>('POSTGRES_URL');
const pool = new Pool({
connectionString: databaseUrl,
});
return drizzle(pool);
},
},
],
exports: [Drizzle],
})
export class DrizzleModule {}
注意这里的 drizzle 函数来自 drizzle-orm/node-postgres
创建表
新建 database/drizzle/schema/users.schema.ts 文件,里面新增一个 users 表:
// schema/user.schema.ts
import { pgTable, serial, text } from "drizzle-orm/pg-core";
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
password: text('password').notNull(),
})
在 schema 文件夹中新建 index.ts 文件,用于导出全部的 xxx.schema.ts 文件:
// schema/index.ts
export * from './users.schema'
// ... 后续添加的schema文件通过这里统一导出
然后在 drizzle.module.ts 中引入:
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schemas';
export const Drizzle = Symbol('drizzle-connection');
export type DrizzleDB = NodePgDatabase<typeof schema>;
@Module({
providers: [
{
provide: Drizzle,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const databaseUrl = configService.get<string>('POSTGRES_URL');
const pool = new Pool({
connectionString: databaseUrl,
});
return drizzle(pool, { schema }) as DrizzleDB;
},
},
],
exports: [Drizzle],
})
export class DrizzleModule {}
useFactory的最后一行 drizzle(pool, { schema }) as DrizzleDB 将普通的 PostgreSQL 连接池包装成 Drizzle ORM 实例。
迁移
使用 Drizzle ORM 时,修改数据库表结构需要进行迁移操作。
在根目录下创建 drizzle.config.ts 文件,运行 Drizzle 迁移命令等。
drizzle-kit generate: 根据 schema 变化生成迁移文件。drizzle-kit migrate: 执行生成的迁移文件,将数据库更新到最新的 schema 状态。
在 drizzle.config.ts 中,不能使用 NestJS 的依赖注入系统,所以需要直接使用 dotenv 包来读取环境变量文件。
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';
// 指定 .env 文件路径
config({ path: './.env' });
export default defineConfig({
schema: './src/database/drizzle/schema/**.schema.ts', // 指定schema路径
dialect: 'postgresql',
dbCredentials: {
url: process.env.POSTGRES_URL,
},
});
生成数据库表迁移文件:
npx drizzle-kit generate
推送到数据库中:
npx drizzle-kit migrate
一对一关系
把 users 当成主表,再创建 profileInfo 作为从表。 profileInfo 的 userId 是外键,关联到 users 表的 id 字段。
// 新增 schema/profileInfo.schema.ts
import { integer, jsonb, pgTable, serial, text } from 'drizzle-orm/pg-core';
import { users } from './users.schema';
export const profileInfo = pgTable('profileInfo', {
id: serial('id').primaryKey(),
metadata: jsonb('metadata'),
userId: integer('userId').references(() => users.id), // 外键!
});
// schema/index.ts 导出profileInfo
export * from './users.schema'
export * from './profileInfo.schema'
从 drizzle-orm/pg-core 导入的几个 postgres 类型:
- integer : 用于定义整数类型的列
- jsonb : 用于定义 PostgreSQL 的 JSONB 类型列
- serial : 用于定义自增的序列类型
- text : 用于定义文本类型的列
默认的外键约束 是
No action。PostgreSQL 提供了以下几种外键约束行为,可以在定义外键时使用ON DELETE和ON UPDATE子句来指定:
NO ACTION: 这是 默认 的外键约束行为。如果父表中被引用的行有任何子表中的行引用它,那么删除或更新父表中的该行将导致错误。事务会被回滚。NO ACTION和RESTRICT非常相似,主要区别在于NO ACTION会在检查约束时才执行检查(如果约束是可延迟的),而RESTRICT会立即检查。但在不可延迟的约束下,它们的行为实际上是一样的。RESTRICT: 与NO ACTION类似,如果父表中被引用的行有任何子表中的行引用它,那么删除或更新父表中的该行将导致错误,并且事务会被回滚。RESTRICT会立即执行检查。CASCADE: 当父表中被引用的行被删除时,子表中所有引用该行的行也会被自动删除。当父表中被引用的主键或唯一键被更新时,子表中所有引用该键的外键列的值也会自动更新为新的值。SET NULL: 当父表中被引用的行被删除或更新时,子表中引用该行的外键列的值会被设置为NULL。前提是外键列允许存储NULL值。SET DEFAULT: 当父表中被引用的行被删除或更新时,子表中引用该行的外键列的值会被设置为该列的默认值。前提是外键列定义了默认值。
在 drizzle 中也可以自定义外键约束行为:
userId: integer('userId').references(() => users.id, {
onDelete: 'cascade', // 当删除用户时级联删除相关的 profileInfo
onUpdate: 'cascade' // 当更新用户 id 时级联更新相关的 profileInfo
}),
现在生成数据库表,然后推送到远程服务器:
npx drizzle-kit generate
npx drizzle-kit migrate
多对一/一对多关系
user 可以发布多个 post,一个 user 对于多个 post。
每个 post 可以对应多个评论,但每条评论只能属于属于某一个特定的用户。
新增 posts 表:
// schema/posts.schema.ts
import { integer, pgTable, serial, text } from "drizzle-orm/pg-core";
import { users } from "./users.schema";
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('authorId').references(()=>users.id) // 外键!
})
新增 comments 表:
// schema/comments.schema.ts
import { integer, pgTable, serial, text } from "drizzle-orm/pg-core";
import { users } from "./users.schema";
import { posts } from "./posts.schema";
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
text: text('title').notNull(),
authorId: integer('authorId').references(() => users.id), // 外键!
postId:integer('postId').references(()=> posts.id) // 外键!
})
// schema/index.ts
export * from './users.schema'
export * from './profileInfo.schema'
export * from './posts.schema'
export * from './comments.schema'
然后同样进行生成 sql 代码后进行迁移:
npx drizzle-kit generate
npx drizzle-kit migrate
运行 npx drizzle-kit studio 可启动本地的 drizzle studio 服务器,在 https://local.drizzle.studio 网页会自动请求本地的 drizzle studio 服务器。关于 drizzle studio 的介绍,点击查看。
多对多关系
每个小组可以包含多个 user,而每个 user 可以加入多个 小组。group 和 user 之间就是多对多关系。在关系型数据库中,可以通过建立第 3 张表来关联 user 和group,实现多对多关系。
新增 groups 表和关联表 usersToGroups :
// schema/groups.schema.ts
import { integer, serial, text } from 'drizzle-orm/pg-core';
import { pgTable } from 'drizzle-orm/pg-core';
import { users } from './users.schema';
export const groups = pgTable('groups', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
// 关联表
export const usersToGroups = pgTable('usersToGroups', {
userId: integer('userId').references(() => users.id),
groupId: integer('groupId').references(() => groups.id),
});
usersToGroups 是一个关联表,用于建立用户和群组之间的多对多关系:
- usersId : 外键,关联到users表的id
- groupId : 外键,关联到groups表的id
在 usersToGroups 表中应该怎样设置主键?常见的作法是设置复合主键(composite primary key),将 usersId 和 groupId 一起作为主键,原因如下:
- 在多对多关系中,一个用户可以属于多个组,一个组可以包含多个用户,使用复合主键可以确保同一个用户不会重复加入同一个组
- 复合主键可以自动创建索引,提高查询性能
import { integer, primaryKey, serial, text } from 'drizzle-orm/pg-core';
import { pgTable } from 'drizzle-orm/pg-core';
import { users } from './users.schema';
export const groups = pgTable('groups', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
// 关联表
export const usersToGroups = pgTable(
'usersToGroups',
{
userId: integer('userId').references(() => users.id),
groupId: integer('groupId').references(() => groups.id),
},
(table) => [primaryKey({ columns: [table.userId, table.groupId] })],
);
最后在 index.ts 文件中导出:
// schema/index.ts
export * from './users.schema'
export * from './profileInfo.schema'
export * from './posts.schema'
export * from './comments.schema'
export * from './groups.schema'
造数据
现在数据库表中没有任何数据,我们可以造一下假数据。
新增 seed.ts 文件,由于这个文件和 nestjs 服务器无关,无法使用 nestjs 中的 drizzle 实例,所以手动创建 drizzleDB 实例:
// drizzle/seed.ts
import { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { config } from 'dotenv';
import * as schema from './schema';
config({ path: './.env' });
const pool = new Pool({
connectionString: process.env.POSTGRES_URL,
});
const drizzleDB = drizzle(pool, { schema }) as NodePgDatabase<typeof schema>;
使用 npm install drizzle-seed 库来造看起来真实的假数据,比如创建 50 个假用户。
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { config } from 'dotenv';
import * as schema from './schema';
import { reset, seed } from 'drizzle-seed';
config({ path: './.env.development' });
const pool = new Pool({
connectionString: process.env.POSTGRES_URL,
});
const drizzleDB = drizzle(pool, { schema }) as NodePgDatabase<typeof schema>;
async function main() {
await seed(
drizzleDB,
{ users: schema.users},
{ seed: 12345 },
).refine((funcs) => ({
users: {
count: 50,
columns: {
email: funcs.email(),
name: funcs.fullName(),
password: funcs.default({ defaultValue: '' }),
},
},
}));
}
main()
.then(() => {
console.log('数据填充完成');
process.exit(0);
})
.catch((e) => {
console.error('数据填充失败:', e);
process.exit(1);
});
然后在 package.json 增加一条运行 seed.ts 的命令。
{
"scripts":{
"db:seed": "ts-node ./src/database/drizzle/seed.ts "
}
}
通过利用伪随机数生成器(pseudorandom number generator , pRNG),可以确保每次生成的数据一致的。这里我传了 {seed: 12345},如果下次也传入12345后生成的用户和本次是一致的。
执行 npm run db:seed,再刷新数据库就能看到已经填充了50条数据。
接下来创建 50 个用户和 50 个 posts,并且将 post 的外键指向 user。只需要在父表(即 users 表)中使用 with: { posts: 1},代表每生成一条 user 记录也生成一条 post。注意这里不是 with: { posts: 50},如果这样配置将生成 2500 条 posts。
import { reset, seed } from 'drizzle-seed';
async function main() {
await reset(drizzleDB, schema);
await seed(
drizzleDB,
{ users: schema.users, posts: schema.posts },
{ seed: 12345 },
).refine((funcs) => ({
users: {
count: 50,
columns: {
email: funcs.email(),
name: funcs.fullName(),
password: funcs.default({ defaultValue: '' }),
},
with: {
posts: 1, // 每生成一条 user 记录也生成一条 post
},
posts: {
columns: {
title: funcs.loremIpsum({
sentencesCount: 1,
}),
content: funcs.loremIpsum({
sentencesCount: 10,
}),
},
},
},
}));
}
注意 seed 函数的参数:第一个参数是数据库连接对象,第二个参数指参与填充的数据库表。这里 users表和 posts 表都需要参与填充,所以把它们两个都放入第二个参数对象中。
在这里先运行了 reset 函数,因为我前面已经插入了50个假用户,需要先 TRUNCATE 清空。重新运行后,会发现 users 生成的数据和先前一致,而且 posts 表的外键字段指向了 users 表!
接下来再生成评论,创建小组,生成用户的 profileInfo,以及分配用户加入小组:
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { config } from 'dotenv';
import * as schema from './schema';
import { reset, seed } from 'drizzle-seed';
config({ path: './.env.development' });
const pool = new Pool({
connectionString: process.env.POSTGRES_URL,
});
const drizzleDB = drizzle(pool, { schema }) as NodePgDatabase<typeof schema>;
async function main() {
await reset(drizzleDB, schema);
await seed(
drizzleDB,
{
users: schema.users,
posts: schema.posts,
profileInfo: schema.profileInfo,
groups: schema.groups,
comments: schema.comments,
usersToGroups: schema.usersToGroups,
},
{ seed: 12345 },
).refine((funcs) => ({
users: {
count: 50,
columns: {
email: funcs.email(),
name: funcs.fullName(),
password: funcs.default({ defaultValue: '' }),
},
with: {
posts: 1,
profileInfo: 1,
comments: 1,
usersToGroups: 1,
},
},
posts: {
columns: {
title: funcs.loremIpsum({
sentencesCount: 1,
}),
content: funcs.loremIpsum({
sentencesCount: 10,
}),
},
with: {
comments: 1,
},
},
groups: {
count: 2,
columns: {
name: funcs.valuesFromArray({
values: ['前端', '后端'],
}),
},
usersToGroups: 1,
},
comments: {
count: 1,
columns: {
text: funcs.loremIpsum({
sentencesCount: 10,
}),
},
},
}));
}
main()
.then(() => {
console.log('数据填充完成');
process.exit(0);
})
.catch((e) => {
console.error('数据填充失败:', e);
process.exit(1);
});
此时,可以把 seed 的第二个参数用 schema 直接代替,因为现在已经将所有表的填充都配置好了。
最后的生成结果:
CRUD
现在终于有数据了,可以开始进行增删改查了。
新建一个 crud 模块 nest g res modules/post
注入DrizzleDB
如何注入 DrizzleDB 呢? 通过 @Inject(Drizzle) 来注入数据库连接。
// post.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Drizzle, type DrizzleDB } from 'src/database/drizzle/drizzle.module';
@Injectable()
export class PostService {
constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}
}
如果没有把 DrizzleModule 设置为全局模块,则还需要在 post.module.ts 导入它:
对于数据库服务一般需要在多个模块中都需要使用,建议使用全局模块方式。
查询
现在给 get 请求返回数据:
// post.service.ts
import { posts } from './../../database/drizzle/schema/posts.schema';
import { Inject, Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Drizzle, type DrizzleDB } from 'src/database/drizzle/drizzle.module';
@Injectable()
export class PostService {
constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}
findAll() {
return this.db.select().from(posts);
}
}
除了 select api 还可以使用 query api 查询数据,这种方法更加类似于 ORM(Object-Relational Mapping) 的方法。ORM(对象关系映射)让你可以使用面向对象的方式来操作数据库,而不是直接写 SQL 查询语句。
findAll() {
return this.db.query.posts.findMany();
}
联表查询
如果想要查询 post 的时候,同时能够返回发布 post 的 user 信息呢?
首先,用 select 语法:
findAll() {
return this.db
.select({
id: posts.id,
title: posts.title,
content: posts.content,
author: {
id: users.id,
name: users.name,
email: users.email,
},
})
.from(posts)
.leftJoin(users, eq(posts.authorId,users.id));
}
非常类似于 sql 查询的语法!
也可以使用 query 的语法:
findAll() {
return this.db.query.posts.findMany({
with: {
author: {
columns: {
id: true,
name: true,
email: true,
},
},
},
});
}
不过直接这样使用会报错 TypeError: Cannot read properties of undefined (reading 'referencedTable')。
我们需要在 schema 中定义关联关系,使用 one 关系(因为一个帖子只能有一个作者),在关系定义中,通过 fields 字段指定 posts 表中的外键字段 (authorId),通过 references 字段指定 users 表中被引用的字段 (id)
// posts.schema.ts
import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core';
import { users } from './users.schema';
import { relations } from 'drizzle-orm';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('authorId').references(() => users.id),
});
export const postsRelations = relations(posts, ({ one,many }) => ({
// 定义与 users 表的关联关系
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
// 与comments表的关联关系。
// 外键在comments那里,所以这里不用使用fields和references指出关联关系
comments: many(comments)
}));
不管哪种方法,最终返回的结果都是一样的:
对于这种简单的查询,我觉得最好还是使用 query 的语法,因为需要明确用定义关系类型,IDE 能够保证类型安全。
再以 users 表为例,写出它和其它表的关系:
import { relations } from 'drizzle-orm';
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
import { comments } from './comments.schema';
import { posts } from './posts.schema';
import { profileInfo } from './profileInfo.schema';
import { usersToGroups } from './groups.schema';
export const users = pgTable('users', {
id: serial('id').primaryKey(), //
name: text('name').notNull(),
email: text('email').notNull(),
password: text('password').notNull(),
});
// 写出全部与 users 表的关联关系
export const usersRelations = relations(users, ({ one, many }) => ({
comments: many(comments),
posts: many(posts),
profile: one(profileInfo), // 一对一关联 但不需要指定关联字段,因为外键在 profileInfo 表中
usersToGroups: many(usersToGroups),
}));
再写出 usersToGroups 与其它表的关系:
import { integer, primaryKey, serial, text } from 'drizzle-orm/pg-core';
import { pgTable } from 'drizzle-orm/pg-core';
import { users } from './users.schema';
import { relations } from 'drizzle-orm';
export const groups = pgTable('groups', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
// 关联表
export const usersToGroups = pgTable(
'usersToGroups',
{
userId: integer('userId').references(() => users.id),
groupId: integer('groupId').references(() => groups.id),
},
(table) => [primaryKey({ columns: [table.userId, table.groupId] })],
);
export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
user: one(users, {
fields: [usersToGroups.userId],
references: [users.id],
}),
group: one(groups, {
fields: [usersToGroups.groupId],
references: [groups.id],
}),
}))
现在就可以用 query 语法查询某个 post 对应的作者属于哪个 group:
findAll() {
return this.db.query.posts.findMany({
with: {
author: {
with: {
usersToGroups: {
with: {
group: {
columns: {
name: true,
},
},
},
},
},
},
},
});
}
最后,把剩下的几个表补齐关系配置:
// comments 表
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
text: text('title').notNull(),
authorId: integer('authorId').references(() => users.id),
postId: integer('postId').references(() => posts.id),
});
export const commentsRelations = relations(comments, ({ one }) => ({
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
}));
// profileInfo表
export const profileInfo表 = pgTable('profileInfo', {
id: serial('id').primaryKey(),
metadata: jsonb('metadata'),
userId: integer('userId').references(() => users.id),
});
export const profileInfoRelations = relations(profileInfo, ({ one }) => ({
user: one(users, {
fields: [profileInfo.userId],
references: [users.id],
}),
}));
//
条件查询
比如查询 id 为 1的用户发布的 post:
import { eq } from 'drizzle-orm';
findAll() {
return this.db.query.posts.findMany({
where: eq(posts.authorId, 1),
with: {
author: {},
},
});
}
改删
用 nestjs-cli 自动给 post.service.ts 生成的 update 方法来修改一下 post
update(id: number) {
return this.db
.update(posts)
.set({
title: 'hello',
content: 'hi',
})
.where(eq(posts.id, id))
.returning();
}
调用 update 的 controller 方法是一个 patch请求。
现在来调用试试,可以看到成功返回了修改后的结果
同样,也可以实现删除:
remove(id: number) {
return this.db.delete(posts).where(eq(posts.id, id));
}
总结
简要介绍了如何在 nest.js 中使用 drizzle-orm,包括创建表,迁移,造数据和简单的 crud。