Nest + Drizzle 实战小博客系统

114 阅读7分钟

项目初始化

使用脚手架搭建项目

连接数据库pg 我们使用docker搭建数据库环境

项目文件根目录下方创建,docker-compose.yaml

services:
  postgres:
    # 拉取最新版本的 postgres 镜像
    image: postgres 
    # 隐射容器的 5432 端口到主机的 5432 端口
    ports:
      - 5432:5432
    # 设置容器的环境变量
    environment:
      POSTGRES_PASSWORD: 123456
      POSTGRES_DB: nestjs-drizzle

使用GUI连接数据库

image.png转存失败,建议直接上传图片文件

项目安装依赖

pnpm add drizzle-orm pg

pnpm add -D drizzle-kit @types/pg

连接数据库

修改.env文件 用于配置连接

APP_NAME=nest-cli 
PORT=8900
CORS=false

# 数据库配置
DATABASE_URL=postgres://postgres:123456@localhost:5432/nestjs-drizzle

初始化

生成module文件

nest g mo database/drizzle/database --flat
// database/drizzle/database.module.ts
import { Global, Module } from '@nestjs/common'
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'
import { ConfigService } from '@nestjs/config'
import { Pool } from 'pg'
import * as schema from './schemas'

export const Drizzle = Symbol('drizzle-connection')
export type DrizzleDB = NodePgDatabase<typeof schema>

@Global()
@Module({
  providers: [
    {
      provide: Drizzle,
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        const pool = new Pool({
          // 数据库/Redis/JWT 密钥等“缺了就不能跑”的配置,全用 getOrThrow
          // 可选配置才用普通 .get() 并给默认值
          connectionString: configService.getOrThrow('DATABASE_URL'),
        })
        return drizzle(pool, { schema }) as DrizzleDB
      },
    },
  ],
  exports: [Drizzle],
})
export class DatabaseModule {}

创建表

创建schema文件 /drizzle/schemas

index.ts

// schema/index.ts
export * from './user.schema'

user.schema.ts

import { pgTable, serial, text } from 'drizzle-orm/pg-core'

export const userSchema = pgTable('user', {
  id: serial('id').primaryKey(),
  email: text('email').unique().notNull(),
  password: text('password').notNull(),
})

迁移

在根目录下创建 drizzle.config.ts 文件,运行 Drizzle 迁移命令等。

  • drizzle-kit generate: 根据 schema 变化生成迁移文件。
  • drizzle-kit migrate: 执行生成的迁移文件,将数据库更新到最新的 schema 状态。
pnpm add dotenv
import { defineConfig } from 'drizzle-kit'
import { config } from 'dotenv'

config({ path: './.env' })
export default defineConfig({
  // 指定schema路径
  schema: './src/database/drizzle/schemas/**.schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
})

使用

/user.service.ts

import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { userSchema } from '@/database/drizzle/schemas'
import { Inject, Injectable } from '@nestjs/common'

@Injectable()
export class UserService {
  constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}

  findAll() {
    // 查询所有用户
    return this.db.select().from(userSchema)
  }
}

也可以这样写

this.db.query.userSchema.findMany()

第一种写法类似 sql 写法 第二种写法更加简便,建议简单的可以使用 query 语法

一对多模型

一个用户可以有多个帖子,多个帖子拥有一个用户

新建帖子模块(posts)

nest g mo modules/posts --no-spec

nest g co modules/posts --no-spec

nest g s modules/posts --no-spec

抽离 时间 公共模块

// global.schema.ts
import { timestamp } from 'drizzle-orm/pg-core'

export const baseSchema = {
  createAt: timestamp('create_at').defaultNow().notNull(),
  updateAt: timestamp('update_at').defaultNow().notNull(),
}

posts 帖子数据模型

// schemas/posts.schema.ts
import { boolean } from 'drizzle-orm/pg-core'
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { baseSchema } from './global.schema'
import { integer } from 'drizzle-orm/pg-core'
import { userSchema } from './user.schema'
import { relations } from 'drizzle-orm'

export const postsSchema = pgTable('posts', {
  id: serial('id').primaryKey(),
  content: text('content'),
  published: boolean('published').default(false),
  ...baseSchema,
  userId: integer('user_id').references(() => userSchema.id),
})

// 多对一(posts 多条 → user 一条)
export const postsRelations = relations(postsSchema, ({ one }) => ({
  // 给这条关系起个名 user
  user: one(userSchema, {
    fields: [postsSchema.userId], // 外键列(存在于 posts 表)
    references: [userSchema.id], // 被引用列(存在于 user 表)
  }),
}))

user 数据模型补充 对应关系

import { relations } from 'drizzle-orm'
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { postsSchema } from './posts.schema'

export const userSchema = pgTable('user', {
  id: serial('id').primaryKey(),
  email: text('email').unique().notNull(),
  password: text('password').notNull(),
})

//user 一条 → posts 多条)
export const userRelations = relations(userSchema, ({ many }) => ({
  posts: many(postsSchema),
}))

查询使用

import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { postsSchema } from '@/database/drizzle/schemas'
import { Inject, Injectable } from '@nestjs/common'

@Injectable()
export class PostsService {
  constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}

  findPostsAll() {
	  // 关系用户表所有内容
    return this.db.query.postsSchema.findMany({ with: { user: true } })
  }

  async createPosts(posts: typeof postsSchema.$inferInsert) {
    await this.db.insert(postsSchema).values(posts)
  }
}

一对一模型

一个用户拥有一份档案信息

/schemas/profile.schema.ts

import { pgTable, serial, integer, text } from 'drizzle-orm/pg-core'
import { userSchema } from './user.schema'
import { relations } from 'drizzle-orm'

export const profileSchema = pgTable('profile', {
  id: serial('id').primaryKey(),
  age: integer('age').default(0),
  biography: text('biography'),
  userId: integer('user_id').references(() => userSchema.id),
})

// 一对一关联用户表 
export const profileRelations = relations(profileSchema, ({ one }) => ({
  user: one(userSchema, {
    fields: [profileSchema.userId],
    references: [userSchema.id],
  }),
}))

/schemas/user.schema.ts

import { relations } from 'drizzle-orm'
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { postsSchema } from './posts.schema'
import { profileSchema } from './profile.schema'

export const userSchema = pgTable('user', {
  id: serial('id').primaryKey(),
  email: text('email').unique().notNull(),
  password: text('password').notNull(),
})

//user 一条 → posts 多条)
export const userRelations = relations(userSchema, ({ many, one }) => ({
  posts: many(postsSchema),
  profile: one(profileSchema), // 关联档案信息表
}))

导出 使用,记得导出schema文件 !!!

import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { userSchema } from '@/database/drizzle/schemas'
import { profileSchema } from '@/database/drizzle/schemas/profile.schema'
import { Inject, Injectable } from '@nestjs/common'

@Injectable()
export class UserService {
  constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}

  findAll() {
    // return this.db.select().from(userSchema)
    // 关联查询 两个表进行数据回显
    return this.db.query.userSchema.findMany({ with: { profile: true, posts: true } })
  }

  async createProfile(profile: typeof profileSchema.$inferInsert) {
    await this.db.insert(profileSchema).values(profile)
  }
}

多对多模型

分类 一个文章可以拥有多分类 一个分类也可以拥有多个文章

nest g mo modules/categories --no-spec

nest g co modules/categories --no-spec

nest g s modules/categories --no-spec

多对多 就是中间建立一张关系表 对应两个表的id

然后两个表分别取映射 many 到这张中间关系表去进行连接

/schemas/categories.schema.ts

import { pgTable, text, serial, integer } from 'drizzle-orm/pg-core'
import { postsSchema } from './posts.schema'
import { primaryKey } from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'

// 分类表
export const categoriesSchema = pgTable('categories', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
})

// 分类表关系
export const categoriesSchemaRelations = relations(categoriesSchema, ({ many }) => ({
  postsToCategories: many(postsToCategoriesSchema),
}))

// 文章分类关联表
export const postsToCategoriesSchema = pgTable(
  'posts_to_categories',
  {
    postsId: integer('posts_id')
      .notNull()
      .references(() => postsSchema.id),
    categoriesId: integer('categories_id')
      .notNull()
      .references(() => categoriesSchema.id),
  },
  (t) => [primaryKey({ columns: [t.postsId, t.categoriesId] })],
)

// 文章分类关联表关系
export const postsToCategoriesSchemaRelations = relations(postsToCategoriesSchema, ({ one }) => ({
  posts: one(postsSchema, {
    fields: [postsToCategoriesSchema.postsId],
    references: [postsSchema.id],
  }),
  categories: one(categoriesSchema, {
    fields: [postsToCategoriesSchema.categoriesId],
    references: [categoriesSchema.id],
  }),
}))

/schemas/posts.schema.ts

import { boolean } from 'drizzle-orm/pg-core'
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { baseSchema } from './global.schema'
import { integer } from 'drizzle-orm/pg-core'
import { userSchema } from './user.schema'
import { relations } from 'drizzle-orm'
import { postsToCategoriesSchema } from './categories.schema'

export const postsSchema = pgTable('posts', {
  id: serial('id').primaryKey(),
  content: text('content'),
  published: boolean('published').default(false),
  ...baseSchema,
  userId: integer('user_id').references(() => userSchema.id),
})

// 多对一(posts 多条 → user 一条)
export const postsRelations = relations(postsSchema, ({ one, many }) => ({
  // 给这条关系起个名 user
  user: one(userSchema, {
    fields: [postsSchema.userId], // 外键列(存在于 posts 表)
    references: [userSchema.id], // 被引用列(存在于 user 表)
  }),
  postsToCategories: many(postsToCategoriesSchema), // 映射关系表
}))

使用 这样就会返回关系表中的id

  findPostsAll() {
    return this.db.query.postsSchema.findMany({ with: { user: true, postsToCategories: true } })
  }

eq 根据id进行查询

根据id 去进行比对数据表查询

// 语法
where: eq(postsSchema.id, postsId)

完整示例:

	// controller 层
	@Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findPosts(id)
  }

	// service 层
  async findPosts(postsId: number) {
    return this.db.query.postsSchema.findFirst({
      where: eq(postsSchema.id, postsId),
      with: { user: true, postsToCategories: true },
    })
  }

更新操作update

传递 帖子id 并更新该条数据,如果传入的id 不存在数据库将会返回一个空数组

  async updatePost(postsId: number, posts: typeof postsSchema.$inferInsert) {
    // returning 表示返回更新后的数据
    const updatedPosts = await this.db.update(postsSchema)
	    .set(posts)
		  .where(eq(postsSchema.id, postsId))
		  .returning()
		  
    if (updatedPosts.length === 0) {
      throw new NotFoundException('帖子不存在')
    }
    return updatedPosts[0]
  }

数据库事务

创建帖子的时候可以同步创建分类标签,如果帖子创建失败那么创建的标签也会被回滚

核心代码:

  async createPosts(posts: typeof postsSchema.$inferInsert, categories?: string) {
  // transaction 声明事务 tx 表示数据操作 所有再事务里面 需要对数据库进行操作的 都需要使用 tx
  // 都使用 tx 进行操作
    await this.db.transaction(async (tx) => {
      const postsArr = await tx.insert(postsSchema).values(posts).returning({ id: postsSchema.id })
      if (categories) {
        const { id } = await this.categoriesService.create({ name: categories }, tx)
        await this.categoriesService.addCategory({ postsId: postsArr[0].id, categoriesId: id }, tx)
      }
    })
  }
  async create(categories: typeof categoriesSchema.$inferInsert, tx?: DrizzleDB) {
    const category = await (tx || this.db)
      .insert(categoriesSchema)
      .values(categories)
      .returning({ id: categoriesSchema.id })
    return category[0]
  }

  async addCategory(postsToCategories: typeof postsToCategoriesSchema.$inferInsert, tx?: DrizzleDB) {
    await (tx || this.db).insert(postsToCategoriesSchema).values(postsToCategories)
  }

实现增删改查

  • Select 查
  • Insert 创建
  • Update 更新
  • Delete 删除
  • Filters 查询条件,如 where 中的各种条件的用法说明
  • Utils 工具,主要是查数量
  • Joins 关联
  • Magic sql operator 自定义 sql 语句

基础增删改查:

db.insert(usersTable).values(data);
db.select().from(usersTable);
db.select().from(usersTable).where(eq(usersTable.id, id));
db.update(postsTable).set(data).where(eq(postsTable.id, id));
db.delete(usersTable).where(eq(usersTable.id, id));

查询近24小时文章(灵活拼接 sql 语句):

db
.select({
  id: postsTable.id,
  title: postsTable.title,
})
.from(postsTable)
.where(between(postsTable.createdAt, sql`now() - interval '1 day'`, sql`now()`))
.orderBy(asc(postsTable.title), asc(postsTable.id))
.limit(pageSize)
.offset((page - 1) * pageSize);

分页+关联查询+排序,其中关联通过getTableColumns查询主表所有字段,以及自定义一个关联查询的字段:

db.select({
  ...getTableColumns(usersTable),
  postsCount: count(postsTable.id),
})
.from(usersTable)
.leftJoin(postsTable, eq(usersTable.id, postsTable.userId))
.groupBy(usersTable.id)
.orderBy(asc(usersTable.id))
.limit(pageSize)
.offset((page - 1) * pageSize);

join 关联查询返回的结果无法自动映射,可手动增加转换:

const rows = db.select({
    user: users,
    pet: pets,
  }).from(users).leftJoin(pets, eq(users.id, pets.ownerId)).all();
const result = rows.reduce<Record<number, { user: User; pets: Pet[] }>>(
  (acc, row) => {
    const user = row.user;
    const pet = row.pet;
    if (!acc[user.id]) {
      acc[user.id] = { user, pets: [] };
    }
    if (pet) {
      acc[user.id].pets.push(pet);
    }
    return acc;
  },
  {}
);

分页查询+total 总数计算 使用事务保持数据更新一致性

 // 使用 事务 (快照)保证数据的一致性
    return this.db.transaction(async (tx) => {
      const userList = await tx
        .select()
        .from(userSchema)
        .limit(size)
        .offset((current - 1) * size)
      // 统计总数
      const [countNumber] = await this.db.select({ count: count() }).from(userSchema)
      const pages = Math.ceil(countNumber.count / size)
      return { current, data: userList, total: countNumber.count, pages }
    })
  • 基础实现
@Injectable()
export class UserService {
    constructor(private readonly drizzle: DrizzleService) {}

    async create(createUserDto: CreateUserDto) {
        const result = await this.drizzle.db.insert(schema.user).values(createUserDto).returning();

        return result;
    }

    async findOne(id: number) {
        const [user] = await this.drizzle.db.select().from(schema.user).where(eq(schema.user.id, id));
        return user;
    }

    async update(id: number, updateUserDto: UpdateUserDto) {
        const user = this.drizzle.db
            .update(schema.user)
            .set(updateUserDto)
            .where(eq(schema.user.id, id));
        return user;
    }

    async remove(id: number) {
        const [user] = await this.drizzle.db
            .delete(schema.user)
            .where(eq(schema.user.id, id))
            .returning();
        return user;
    }
}

package.json 中的脚本


  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
  • db:generate 声明时或后续 Schema 更改时基于 Drizzle Schema 生成 SQL 迁移
  • db:migrate 运行迁移后,Drizzle Kit 会将成功应用的迁移记录保存到数据库中。
  • db:push 允许您直接将您的架构和后续架构更改推送到数据库
  • db:studio 本地数据库可视化面板

说明:

阶段命令
第一次初始化pnpm db:generate + pnpm db:migrate
开发中快速迭代pnpm db:push(结构稳定后再 generate→migrate)
上线前确保 generate→migrate 把最新迁移包打进去
调试数据pnpm db:studio 随时开

初始化执行一次 generate→migrate

此后 每次新建表或是改表的数据结构 就执行 push,上线前再执行 generate→migrate即可