Drizzle ORM v1 (Relations Query v2) 指南

SkillDev tools

Guide to relational queries in Drizzle ORM v1. Use when defining Relations v2, writing relational queries, using through many-to-many relations, predefined filters, or migrating from legacy Drizzle.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Drizzle ORM v1 (Relations Query v2) 指南 skill

What this skill tells your AI

The instructions your AI receives, as published by zhe-qi/clhoria-template in .agents/skills/drizzle-v1/SKILL.md and read by ahel’s review.

版本信息

  • 当前版本: drizzle-orm@1.0.0-beta.21(已进入 RC 阶段)
  • 官方文档: 所有 drizzle 官方文档已更新为 v1 写法
  • 关键变化: Relations 定义方式、查询 API、多对多 through 支持

核心概念

drizzle() 初始化

传入 relations 而非 schema

import { drizzle } from "drizzle-orm/postgres-js";
import { relations } from "./relations";

const db = drizzle({
  client: getQueryClient(),
  relations,
  casing: "snake_case",
});

Relations 定义

基本结构

使用 defineRelations 在一处定义所有关系:

// src/db/relations/index.ts
import { defineRelations } from "drizzle-orm";
import * as schema from "@/db/schema";

export const relations = defineRelations(schema, (r) => ({
  // 每个表的关系定义
  users: {
    posts: r.many.posts(),
  },
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  },
}));

本项目的分片模式

本项目使用 defineRelationsPart + spread 方式拆分关系定义:

// src/db/relations/index.ts
import { defineRelations } from "drizzle-orm";
import * as schema from "@/db/schema";
import { userRolesRelations } from "./admin/user-roles";

export const relations = defineRelations(schema, (r) => ({
  ...userRolesRelations(r),
}));
// src/db/relations/admin/user-roles.ts
import type { ExtractTablesFromSchema, RelationsBuilder } from "drizzle-orm";
import type * as schema from "@/db/schema";

type Schema = ExtractTablesFromSchema<typeof schema>;

export const userRolesRelations = (r: RelationsBuilder<Schema>) => ({
  systemUsers: {
    roles: r.many.systemRoles({
      from: r.systemUsers.id.through(r.systemUserRoles.userId),
      to: r.systemRoles.id.through(r.systemUserRoles.roleId),
    }),
    enabledRoles: r.many.systemRoles({
      from: r.systemUsers.id.through(r.systemUserRoles.userId),
      to: r.systemRoles.id.through(r.systemUserRoles.roleId),
      where: { status: Status.ENABLED },
    }),
  },
  systemRoles: {
    users: r.many.systemUsers(),
  },
});

关键模式

  • 类型使用 ExtractTablesFromSchema<typeof schema> + RelationsBuilder<Schema>
  • 返回对象直接 spread 到 defineRelations
  • src/db/relations/{tier}/{feature}.ts 组织文件

一对一 / 一对多

// one: 指定 from → to 映射
posts: {
  author: r.one.users({
    from: r.posts.authorId,
    to: r.users.id,
  }),
},

// many: 可以只定义 many 侧(无需在对端定义 one)
users: {
  posts: r.many.posts({
    from: r.users.id,
    to: r.posts.authorId,
  }),
},

多对多 (through)

使用 .through() 指定连接表,无需手动查询连接表再映射:

users: {
  groups: r.many.groups({
    from: r.users.id.through(r.usersToGroups.userId),
    to: r.groups.id.through(r.usersToGroups.groupId),
  }),
},
groups: {
  participants: r.many.users(), // 反向可省略 from/to
},

预定义过滤器 (where)

在关系定义中预设过滤条件,查询时直接使用:

systemUsers: {
  enabledRoles: r.many.systemRoles({
    from: r.systemUsers.id.through(r.systemUserRoles.userId),
    to: r.systemRoles.id.through(r.systemUserRoles.roleId),
    where: { status: Status.ENABLED },
  }),
},

optional 选项

optional: false 使关系成为必填(TypeScript 类型非 nullable):

posts: {
  author: r.one.users({
    from: r.posts.authorId,
    to: r.users.id,
    optional: false, // author 不会是 null
  }),
},

alias(替代旧版 relationName)

自引用或同表多关系时使用 alias 区分:

posts: {
  author: r.one.users({
    from: r.posts.authorId,
    to: r.users.id,
    alias: "author_post",
  }),
  reviewer: r.one.users({
    from: r.posts.reviewerId,
    to: r.users.id,
    alias: "reviewer_post",
  }),
},

查询 API

where(对象语法)

// 简单等值
db.query.users.findFirst({
  where: { id: userId },
});

// 多条件(AND)
db.query.users.findMany({
  where: { status: Status.ENABLED, username: "admin" },
});

// 操作符
db.query.users.findMany({
  where: {
    id: { gt: 10 },
    name: { like: "M%" },
  },
});

// 按关系过滤(v1 新增!)
db.query.users.findMany({
  where: {
    id: { gt: 10 },
    posts: {
      content: { like: "M%" },
    },
  },
});

// 关系存在性过滤:获取至少有 1 篇 post 的用户
db.query.users.findMany({
  with: { posts: true },
  where: { posts: true },
});
where 完整操作符参考
where: {
  // 逻辑组合
  OR: [],        // 或
  AND: [],       // 与
  NOT: {},       // 非

  // RAW SQL
  RAW: (table) => sql`${table.id} = 1`,

  // 按关系过滤
  [relation]: {},  // 如 posts: { content: { like: "M%" } }

  // 列操作符
  [column]: {
    eq: 1,                     // =
    ne: 1,                     // !=
    gt: 1,                     // >
    gte: 1,                    // >=
    lt: 1,                     // <
    lte: 1,                    // <=
    in: [1, 2],                // IN
    notIn: [1, 2],             // NOT IN
    like: "M%",                // LIKE
    ilike: "m%",               // ILIKE(不区分大小写)
    notLike: "M%",             // NOT LIKE
    notIlike: "m%",            // NOT ILIKE
    isNull: true,              // IS NULL
    isNotNull: true,           // IS NOT NULL
    arrayOverlaps: [1, 2],     // 数组重叠
    arrayContained: [1, 2],    // 数组被包含
    arrayContains: [1, 2],     // 数组包含

    // 列级逻辑组合
    OR: [],
    AND: [],
    NOT: {},
  },
}

orderBy(对象语法)

db.query.users.findMany({
  orderBy: { id: "asc" },
});

// 多字段排序
db.query.users.findMany({
  orderBy: { createdAt: "desc", name: "asc" },
});

// 自定义 SQL 排序
db.query.posts.findMany({
  orderBy: (t) => sql`${t.id} asc`,
});

// 关系内排序
db.query.posts.findMany({
  orderBy: { id: "asc" },
  with: {
    comments: {
      orderBy: { id: "desc" },
    },
  },
});

with(关系加载)

// 加载全部列
db.query.users.findFirst({
  where: { id: userId },
  with: { roles: true },
});

// 选择部分列
db.query.users.findFirst({
  where: { id: userId },
  with: {
    roles: {
      columns: { id: true, name: true },
    },
  },
});

// 预定义过滤关系
db.query.users.findFirst({
  where: { id: userId },
  with: { enabledRoles: { columns: { id: true } } },
});

columns(选择列)

db.query.users.findFirst({
  where: { id: userId },
  columns: { id: true, username: true, avatar: true },
});

offset(支持关系对象)

// v1 新增:关系内也支持 offset
db.query.posts.findMany({
  limit: 5,
  offset: 2,
  with: {
    comments: {
      offset: 3,
      limit: 3,
    },
  },
});

extras(自定义计算字段)

在查询中添加自定义 SQL 计算字段:

import { sql } from "drizzle-orm";

// 简单 extras
db.query.users.findMany({
  extras: {
    loweredName: sql`lower(${users.name})`,
  },
});

// 回调语法
db.query.users.findMany({
  extras: {
    loweredName: (users, { sql }) => sql`lower(${users.name})`,
  },
});

// 嵌套关系中也支持 extras
db.query.posts.findMany({
  extras: {
    contentLength: (table, { sql }) => sql<number>`length(${table.content})`,
  },
  with: {
    comments: {
      extras: {
        commentSize: (table, { sql }) => sql<number>`length(${table.content})`,
      },
    },
  },
});

注意:extras 目前不支持聚合函数,需要聚合请使用 core queries。

extras 子查询

import { posts } from "./schema";
import { eq } from "drizzle-orm";

// 获取用户及其 post 总数
db.query.users.findMany({
  with: { posts: true },
  extras: {
    totalPostsCount: (table) => db.$count(posts, eq(posts.authorId, table.id)),
  },
});

Prepared Statements(预编译查询)

使用 placeholder 提升重复查询性能:

import { sql } from "drizzle-orm";

// where 中使用 placeholder
const prepared = db.query.users.findMany({
  where: { id: { eq: sql.placeholder("id") } },
  with: {
    posts: {
      where: { id: 1 },
    },
  },
}).prepare("query_name");

const result = await prepared.execute({ id: 1 });

// limit/offset 中使用 placeholder
const prepared2 = db.query.users.findMany({
  limit: sql.placeholder("uLimit"),
  offset: sql.placeholder("uOffset"),
  where: {
    OR: [{ id: { eq: sql.placeholder("id") } }, { id: 3 }],
  },
  with: {
    posts: {
      where: { id: { eq: sql.placeholder("pid") } },
      limit: sql.placeholder("pLimit"),
    },
  },
}).prepare("query_name");

const result2 = await prepared2.execute({
  pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6,
});

defineRelationsPart 规则

使用 defineRelationsPart 拆分关系定义时的重要规则:

规则 1:spread 时主 relations 必须在前:

// ✅ 正确
const db = drizzle(url, { relations: { ...relations, ...part } })
// ❌ 错误
const db = drizzle(url, { relations: { ...part, ...relations } })

规则 2:必须有一个 main relations(使用 defineRelations),让 drizzle 能推断所有表。如果只想用 parts,可以创建一个空的 main:

export const mainPart = defineRelationsPart(schema); // 空 main,用于推断所有表

多对多查询对比

v1 之前(绕过连接表)

// 旧版:必须嵌套查询连接表再映射
const response = await db.query.users.findMany({
  with: {
    usersToGroups: {
      columns: {},
      with: { group: true },
    },
  },
});
// 还需要手动 map: response.map(u => ({ ...u, groups: u.usersToGroups.map(utg => utg.group) }))

v1 新版(through 直查)

// 新版:直接查多对多,无需映射
const response = await db.query.users.findMany({
  with: { groups: true },
});

新增 Relations 文件步骤

  1. 创建关系文件 src/db/relations/{tier}/{feature}.ts
  2. 定义类型化函数,返回关系对象
  3. src/db/relations/index.ts 注册:spread 到 defineRelations
// 1. src/db/relations/{tier}/{feature}.ts
import type { ExtractTablesFromSchema, RelationsBuilder } from "drizzle-orm";
import type * as schema from "@/db/schema";

type Schema = ExtractTablesFromSchema<typeof schema>;

export const {feature}Relations = (r: RelationsBuilder<Schema>) => ({
  {parentTable}: {
    {relName}: r.many.{childTable}({
      from: r.{parentTable}.id,
      to: r.{childTable}.{parentTable}Id,
    }),
  },
  {childTable}: {
    {parentRef}: r.one.{parentTable}({
      from: r.{childTable}.{parentTable}Id,
      to: r.{parentTable}.id,
    }),
  },
});

// 2. src/db/relations/index.ts
import { {feature}Relations } from "./{tier}/{feature}";

export const relations = defineRelations(schema, (r) => ({
  ...userRolesRelations(r),
  ...{feature}Relations(r),  // 新增
}));

注意:同一个表的关系在多个 part 中定义时,后 spread 的会覆盖前面同名的 key。确保不同 part 中同一表的关系 key 名不冲突,或在 index.ts 中手动合并。

从旧版迁移速查

v1 旧版v1 新版 (Relations v2)
import { relations } from "drizzle-orm"import { relations } from "drizzle-orm/_relations" ← 旧版移到这
fields: [posts.authorId]from: r.posts.authorId
references: [users.id]to: r.users.id
relationName: "xxx"alias: "xxx"
每表单独 relations()统一 defineRelations()
db.query.xxx (旧语法)db._query.xxx (可继续用旧语法)
where: (t, { eq }) => eq(t.id, 1)where: { id: 1 }
orderBy: (t, { asc }) => [asc(t.id)]orderBy: { id: "asc" }
drizzle(url, { schema })drizzle(url, { relations })
MySQL mode: "planetscale"不再需要 mode

渐进迁移策略

如果需要逐步迁移而非一次性切换:

  1. 将旧版 import { relations } from "drizzle-orm" 改为 import { relations } from "drizzle-orm/_relations"
  2. 旧查询用 db._query.xxx 替代 db.query.xxx
  3. 新查询用 db.query.xxx(新语法)
  4. 逐个迁移旧查询

drizzle-kit pull 自动迁移

pnpm drizzle-kit pull

会在 drizzle/relations.ts 生成新语法的关系定义,可直接复制到项目的 src/db/relations/ 中使用。注意修改导入路径。

常见陷阱

  1. 不要混用新旧 relations 定义defineRelations (v2) 和旧版 relations() (v1) 不兼容
  2. from/to 不再用数组:单列时直接传值 from: r.posts.authorId,多列时才用数组 from: [r.posts.a, r.posts.b]
  3. spread 覆盖:多个 part spread 时,同表同 key 会被后者覆盖
  4. through 需要连接表已在 schema 中定义r.usersToGroups.userId 前提是 usersToGroups 表已导出
  5. where 预定义过滤器只能过滤目标表where 子句只能包含 to 端表的列
  6. extras 不支持 .as() 别名:drizzle 会忽略 extras 字段上的 .as("<alias>")

升级步骤(从旧版到 v1 RC)

  1. 运行 pnpm drizzle-kit up:更新 migrations 文件夹结构(移除 journal.json,按文件夹分组)
  2. 更新 validator 包导入
    • drizzle-zoddrizzle-orm/zod
    • drizzle-valibotdrizzle-orm/valibot
    • drizzle-typeboxdrizzle-orm/typebox
    • drizzle-arktypedrizzle-orm/arktype
  3. 迁移 Relations 和查询:参照本文档的迁移速查表

官方文档参考

Signals

GitHub stars
190
Forks
19
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
drizzle-v1
Source
github.com/zhe-qi/clhoria-template