Chapter 04

基础 CRUD

insert/select/update/delete 四件套——API 故意长得像 SQL,学会就会用,类型全自动推断。

4.1 insert:新增行

import { db } from "./db";
import { users } from "./schema";

// 插入一行
await db.insert(users).values({
  name: "Alice",
  email: "alice@example.com",
});

// 批量插入
await db.insert(users).values([
  { name: "Bob", email: "bob@example.com" },
  { name: "Carol", email: "carol@example.com" },
]);

returning:拿到刚插入的行

// PG/SQLite 支持 RETURNING
const [inserted] = await db
  .insert(users)
  .values({ name: "Dan", email: "dan@x.com" })
  .returning();
console.log(inserted.id);   // 新生成的 id

// 只取部分字段
const rows = await db.insert(users)
  .values([{ name: "E", email: "e@x.com" }])
  .returning({ id: users.id, email: users.email });
MySQL 不支持 RETURNING

MySQL 8.x 没有 RETURNING 子句——Drizzle 的 mysql2 版本 .returning() 被禁用。想拿 id 用 result.lastInsertId 或插入后单独查询。

4.2 select:查询

// 全表所有列
const all = await db.select().from(users);

// partial select:只要某些列
const names = await db
  .select({ id: users.id, name: users.name })
  .from(users);

// where
import { eq, gt, and } from "drizzle-orm";
const adults = await db
  .select()
  .from(users)
  .where(gt(users.age, 18));

// limit / offset / orderBy
import { desc } from "drizzle-orm";
const top10 = await db
  .select()
  .from(users)
  .orderBy(desc(users.createdAt))
  .limit(10)
  .offset(20);

4.3 类型推断实战

const rows = await db.select().from(users);
// rows: { id: number; name: string; email: string; age: number | null; ... }[]

const partial = await db.select({ id: users.id, name: users.name }).from(users);
// partial: { id: number; name: string }[]  —— 只含你选的字段

4.4 update

import { eq } from "drizzle-orm";

await db
  .update(users)
  .set({ name: "Alice Renamed", age: 31 })
  .where(eq(users.id, 1));

// 用现有列值计算新值
import { sql } from "drizzle-orm";
await db
  .update(posts)
  .set({ viewCount: sql`${posts.viewCount} + 1` })
  .where(eq(posts.id, id));

// returning
const [updated] = await db
  .update(users).set({ age: 40 })
  .where(eq(users.id, 1))
  .returning();
永远写 where!

db.update(users).set({ age: 0 }) 不加 where 会把全表所有行的 age 改成 0。Drizzle 不给你安全网——SQL 是什么语义就是什么语义。Code review 时尤其留意 update/delete 调用链是否缺 where。

4.5 delete

await db.delete(users).where(eq(users.id, 1));

// returning 删除的行
const deleted = await db
  .delete(users)
  .where(lt(users.createdAt, new Date("2020-01-01")))
  .returning();

4.6 onConflict / upsert

插入时若唯一键冲突,怎么办?三种策略:

Postgres / SQLite:onConflictDoUpdate

await db.insert(users)
  .values({ email: "alice@x.com", name: "Alice", age: 31 })
  .onConflictDoUpdate({
    target: users.email,       // 冲突检测的列(唯一约束)
    set: {
      name: "Alice",
      age: 31,
      // 用 excluded 引用"将要插入但被阻止的那行"
      updatedAt: sql`now()`,
    },
    // 可选:只在满足条件时才更新
    // where: gt(users.age, 0),
  });

onConflictDoNothing

await db.insert(users)
  .values({ email: "alice@x.com", name: "Alice" })
  .onConflictDoNothing({ target: users.email });

MySQL:onDuplicateKeyUpdate

await db.insert(users)
  .values({ email: "alice@x.com", name: "Alice" })
  .onDuplicateKeyUpdate({
    set: { name: "Alice" },
  });

4.7 批量插入性能

// Drizzle 会生成一条多值 INSERT,比循环单条快几十倍
const items = Array.from({ length: 1000 }, (_, i) => ({
  name: `u${i}`,
  email: `u${i}@x.com`,
}));
await db.insert(users).values(items);

超大批量(> 10k 行)建议分批 1000-5000 一次,避免单条 SQL 过长。

4.8 prepared statement(预编译)

同一条查询要跑很多次时,预编译省重复解析:

import { sql } from "drizzle-orm";

const findByEmail = db
  .select().from(users)
  .where(eq(users.email, sql.placeholder("email")))
  .prepare("find_user_by_email");   // 给准备语句命名(PG)

const u1 = await findByEmail.execute({ email: "a@x.com" });
const u2 = await findByEmail.execute({ email: "b@x.com" });

4.9 $returning / $inferInsert 类型玩法

type NewUser = typeof users.$inferInsert;

async function createUser(data: NewUser) {
  const [u] = await db.insert(users).values(data).returning();
  return u;
}

// 参数类型自动不含 id/createdAt(可选),返回类型是 User 完整对象

4.10 小结