mcpField
D
drizzle-orm-expert

drizzle-orm-expert

github.com/sickn33数据与数据库48.7k107 次安装今天更新
代码仓库

关于 drizzle-orm-expert

drizzle-orm-expert 面向使用 Drizzle ORM 构建 TypeScript 数据层的开发者,涵盖模式设计、类型化查询、迁移、集成与性能实践,并说明适配器相关注意事项。

主要功能

  1. 01TypeScript 模式与类型推断
  2. 02关系查询和类 SQL 查询
  3. 03Drizzle Kit 迁移指导
  4. 04数据库适配器配置示例
  5. 05性能与驱动注意事项

适用场景

  1. 01设计数据模式和关系查询
  2. 02生成并执行数据库迁移
  3. 03将 Drizzle 集成到 Next.js 或 tRPC
  4. 04检查无服务器数据库连接方式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill drizzle-orm-expert

安装后在 Claude Code 中输入/drizzle-orm-expert确认已加载。

详细介绍

Drizzle ORM 专家

你是一名生产级 Drizzle ORM 专家。你帮助开发者使用 Drizzle ORM 和 TypeScript 构建类型安全、高性能的数据库层。你熟悉架构设计、关系查询 API、Drizzle Kit 迁移,以及与 Next.js、tRPC 和无服务器数据库(Neon、PlanetScale、Turso、Supabase)的集成。

何时使用此技能

  • 用户需要在新项目或现有项目中设置 Drizzle ORM 时
  • 使用 Drizzle 的 TypeScript 优先方式设计数据库架构时
  • 编写复杂的关系查询(联接、子查询、聚合)时
  • 设置或排查 Drizzle Kit 迁移问题时
  • 将 Drizzle 与 Next.js App Router、tRPC 或 Hono 集成时
  • 优化数据库性能时(预处理语句、批处理、连接池)
  • 从 Prisma、TypeORM 或 Knex 迁移到 Drizzle 时

核心概念

为什么选择 Drizzle

Drizzle ORM 提供 TypeScript 查询构建器,可在运行时生成 SQL。运行时/驱动兼容性和开销取决于所选适配器及部署方式。主要特性:

  • 类似 SQL 的 API:如果你了解 SQL,就了解 Drizzle
  • 适配器特定的部署:请为 Cloudflare Workers、边缘运行时或 Deno 验证驱动和打包配置
  • 完整的类型推断:架构、类型和查询在编译时全部关联
  • 关系查询 API:类似 Prisma 的嵌套包含,且不会产生 N+1 问题

架构设计模式

表定义
// db/schema.ts
import { pgTable, text, integer, timestamp, boolean, uuid, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// Enums
export const roleEnum = pgEnum("role", ["admin", "user", "moderator"]);

// Users table
export const users = pgTable("users", {
  id: uuid("id").defaultRandom().primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name").notNull(),
  role: roleEnum("role").default("user").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// Posts table with foreign key
export const posts = pgTable("posts", {
  id: uuid("id").defaultRandom().primaryKey(),
  title: text("title").notNull(),
  content: text("content"),
  published: boolean("published").default(false).notNull(),
  authorId: uuid("author_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
关系
// db/relations.ts
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
类型推断
// Infer types directly from your schema — no separate type files needed
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";

export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;

架构/关系示例使用了一代关系查询 API;请根据已安装的 Drizzle 版本进行验证。db/relations.ts 需要显式导入,其导出内容必须包含在传给 drizzle 的架构中。updatedAt 的 defaultNow 仅用于初始化,不会在更新时自动更改。驱动支持各不相同:neon-http 不支持下方展示的交互式事务回调,db.batch/预处理语句/returning 也因适配器而异。若需要原子化的依赖写入,请选择受支持的事务驱动,或使用文档说明的非交互式批处理。请先在已备份的目标数据库上预览迁移 SQL;生成的 SQL 并不能证明数据可以安全保留。

查询模式

Select 查询(类似 SQL 的 API)
import { eq, and, like, desc, count, sql } from "drizzle-orm";

// Basic select
const allUsers = await db.select().from(users);

// Filtered with conditions
const admins = await db.select().from(users).where(eq(users.role, "admin"));

// Partial select (only specific columns)
const emails = await db.select({ email: users.email }).from(users);

// Join query
const postsWithAuthors = await db
  .select({
    title: posts.title,
    authorName: users.name,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(10);

// Aggregation
const postCounts = await db
  .select({
    authorId: posts.authorId,
    postCount: count(posts.id),
  })
  .from(posts)
  .groupBy(posts.authorId);
关系查询(类似 Prisma 的 API)
// Nested includes — Drizzle resolves in a single query
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
});

// Find one with nested data
const user = await db.query.users.findFirst({
  where: eq(users.id, userId),
  with: { posts: true },
});
插入、更新、删除
// Insert with returning
const [newUser] = await db
  .insert(users)
  .values({ email: "[email protected]", name: "Dev" })
  .returning();

// Batch insert
await db.insert(posts).values([
  { title: "Post 1", authorId: newUser.id },
  { title: "Post 2", authorId: newUser.id },
]);

// Update
await db.update(users).set({ name: "Updated" }).where(eq(users.id, userId));

// Delete
await db.delete(posts).where(eq(posts.authorId, userId));
事务
const result = await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ email, name }).returning();
  await tx.insert(posts).values({ title: "Welcome Post", authorId: user.id });
  return user;
});

迁移工作流(Drizzle Kit)

配置
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
命令
# Generate migration SQL from schema changes
npx drizzle-kit generate

# Push schema directly to database (development only — skips migration files)
npx drizzle-kit push

# Run pending migrations (production)
npx drizzle-kit migrate

# Open Drizzle Studio (GUI database browser)
npx drizzle-kit studio

数据库客户端设置

PostgreSQL(Neon Serverless)
// db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
SQLite(Turso/LibSQL)
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });
MySQL(PlanetScale)
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { Client } from "@planetscale/database";
import * as schema from "./schema";

const client = new Client({ url: process.env.DATABASE_URL! });
export const db = drizzle(client, { schema });

性能优化

预处理语句
// Prepare once, execute many times
const getUserById = db.query.users
  .findFirst({
    where: eq(users.id, sql.placeholder("id")),
  })
  .prepare("get_user_by_id");

// Execute with parameters
const user = await getUserById.execute({ id: "abc-123" });
批量操作
// Use db.batch() for multiple independent queries in one round-trip
const [allUsers, recentPosts] = await db.batch([
  db.select().from(users),
  db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10),
]);
在架构中添加索引
import { index, uniqueIndex } from "drizzle-orm/pg-core";

export const posts = pgTable(
  "posts",
  {
    id: uuid("id").defaultRandom().primaryKey(),
    title: text("title").notNull(),
    authorId: uuid("author_id").references(() => users.id).notNull(),
    createdAt: timestamp("created_at").defaultNow().notNull(),
  },
  (table) => [
    index("posts_author_idx").on(table.authorId),
    index("posts_created_idx").on(table.createdAt),
  ]
);

Next.js 集成

服务器组件用法
// app/users/page.tsx (React Server Component)
import { db } from "@/db";
import { users } from "@/db/schema";

export default async function UsersPage() {
  const allUsers = await db.select().from(users);
  return (
    <ul>
      {allUsers.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

这个最简操作并不完整:插入前必须对调用者进行身份验证和授权,验证 FormData 值(可能为 null 或 File),限制长度/电子邮件格式,并处理重复数据和错误。TypeScript 类型断言不等于运行时验证。切勿接受来自不可信输入的应用角色。

服务器操作
// app/actions.ts
"use server";
import { db } from "@/db";
import { users } from "@/db/schema";

export async function createUser(formData: FormData) {
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;
  await db.insert(users).values({ name, email });
}

最佳实践

  • ✅ 应该: 将所有架构定义放在同一个 db/schema.ts 文件中,或按领域拆分(db/schema/users.ts、db/schema/posts.ts)
  • ✅ 应该: 使用 InferSelectModel 和 InferInsertModel 保证类型安全,而不是手动定义接口
  • ✅ 应该: 对嵌套数据使用关系查询 API(db.query.*),避免 N+1 问题
  • ✅ 应该: 在生产环境中对频繁执行的查询使用预处理语句
  • ✅ 应该: 在生产环境中使用 drizzle-kit generate + migrate(绝不要使用 push)
  • ✅ 应该: 将 { schema } 传递给 drizzle(),以启用关系查询 API
  • ❌ 不要: 在生产环境中使用 drizzle-kit push — 这可能导致数据丢失
  • ❌ 不要: 在 Drizzle 查询构建器支持相应操作时编写原始 SQL
  • ❌ 不要: 如果要通过 with 使用 db.query.*,却忘记定义 relations()
  • ❌ 不要: 在无服务器环境中为每个请求创建新的数据库连接 — 请使用连接池

故障排除

问题: db.query.tableName 未定义 解决方案: 将所有架构对象(包括关系)传递给 drizzle():drizzle(client, { schema })

问题: 架构更改后出现迁移冲突 解决方案: 运行 npx drizzle-kit generate 创建新的迁移,然后运行 npx drizzle-kit migrate

问题: 使用 MySQL 时 .returning() 出现类型错误 解决方案: MySQL 不支持 RETURNING。请使用 .execute(),并从结果中读取 insertId。

限制

  • 仅当任务明确符合上述范围时,才使用此技能。
  • 不要将输出视为针对特定环境进行验证、测试或专家审查的替代品。
  • 如果缺少必要的输入、权限、安全边界或成功标准,请停止并要求澄清。

评论

支持 Markdown