Drizzle 关系

WARNING

此页面解释的是 Drizzle 版本 1.0.0-beta.1 及以上版本支持的概念。

npm
yarn
pnpm
bun
npm i drizzle-orm@beta
npm i drizzle-kit@beta -D

本指南假定您已熟悉:
  • 关系基础 - 熟悉外键约束、软关联、数据库范式等概念 - 查看这里
  • 声明模式 - 熟悉如何定义 Drizzle 模式 - 查看这里
  • 数据库连接 - 熟悉如何使用 Drizzle 连接数据库 - 查看这里

Drizzle 关系的唯一目的是让你以最简单且简洁的方式查询关系型数据:

关系查询
关联选择
import { drizzle } from 'drizzle-orm/…';
import { defineRelations } from 'drizzle-orm';
import * as p from 'drizzle-orm/pg-core';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text().notNull()
});

export const posts = p.pgTable('posts', {
	id: p.integer().primaryKey(),
	content: p.text().notNull(),
	ownerId: p.integer('owner_id'),
});

const relations = defineRelations({ users, posts }, (r) => ({
	posts: {
		author: r.one.users({
			from: r.posts.ownerId,
			to: r.users.id,
		}),
	}
}))

const db = drizzle(client, { relations });

const result = db.query.posts.findMany({
  with: {
    author: true,
  },
});
[{
  id: 10,
  content: "我的第一篇帖子!",
  author: {
    id: 1,
    name: "Alex"
  }
}]

one()

以下是 drizzle 关系中 .one() 支持的所有字段列表

const relations = defineRelations({ users, posts }, (r) => ({
	posts: {
		author: r.one.users({
			from: r.posts.ownerId,
			to: r.users.id,
			optional: false,
      alias: 'custom_name',
			where: {
				verified: true,
			}
		}),
	}
}))

many()

以下是 drizzle 关系中 .many() 支持的所有字段列表

const relations = defineRelations({ users, posts }, (r) => ({
	users: {
		feed: r.many.posts({
			from: r.users.id,
			to: r.posts.ownerId,
			optional: false,
      alias: 'custom_name',
			where: {
				approved: true,
			}
		}),
	}
}))

---

一对一

Drizzle ORM 提供了通过 defineRelations 方法定义表之间 一对一 关系的接口。

示例:用户与用户间的一对一关系,一个用户可以邀请另一个用户(自引用示例):

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

export const users = pgTable('users', {
	id: integer().primaryKey(),
	name: text(),
	invitedBy: integer('invited_by'),
});

export const relations = defineRelations({ users }, (r) => ({
	users: {
		invitee: r.one.users({
			from: r.users.invitedBy,
			to: r.users.id,
		})
	}
}));

另一个例子:用户的个人资料信息存储在独立表中。由于外键存储在 profile_info 表,用户的关系没有字段或引用,这意味着 Typescript 推断 user.profileInfo 是可空的:

import { pgTable, serial, text, integer, jsonb } from 'drizzle-orm/pg-core';
import { defineRelations } from 'drizzle-orm';

export const users = pgTable('users', {
	id: integer().primaryKey(),
	name: text(),
});

export const profileInfo = pgTable('profile_info', {
	id: serial().primaryKey(),
	userId: integer('user_id').references(() => users.id),
	metadata: jsonb(),
});

export const relations = defineRelations({ users, profileInfo }, (r) => ({
	users: {
		profileInfo: r.one.profileInfo({
			from: r.users.id,
			to: r.profileInfo.userId,
		})
	}
}));

const user = await db.query.posts.findFirst({ with: { profileInfo: true } });
//____^? 类型 { id: number, profileInfo: { ... } | null  }

一对多

Drizzle ORM 提供了通过 defineRelations 方法定义表之间 一对多 关系的接口。

示例:用户与其所有帖子的 一对多 关系:

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

export const users = pgTable('users', {
	id: integer('id').primaryKey(),
	name: text('name'),
});

export const posts = pgTable('posts', {
	id: integer('id').primaryKey(),
	content: text('content'),
	authorId: integer('author_id'),
});

export const relations = defineRelations({ users, posts }, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  },
  users: {
    posts: r.many.posts(),
  },
}));

现在给帖子添加评论:

...

export const posts = pgTable('posts', {
	id: integer('id').primaryKey(),
	content: text('content'),
	authorId: integer('author_id'),
});

export const comments = pgTable("comments", {
  id: integer().primaryKey(),
  text: text(),
  authorId: integer("author_id"),
  postId: integer("post_id"),
});

export const relations = defineRelations({ users, posts, comments }, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
    comments: r.many.comments(),
  },
  users: {
    posts: r.many.posts(),
  },
  comments: {
    post: r.one.posts({
      from: r.comments.postId,
      to: r.posts.id,
    }),
  },
}));

多对多

Drizzle ORM 提供了通过所谓的“连接表”或“中间表”定义表之间 多对多 关系的接口, 这类表需要显式定义,并存储相关表之间的关联。

示例:用户与群组的多对多关系,使用 through 直接跳过连接表选择,为每个用户选择多个群组。

import { defineRelations } from 'drizzle-orm';
import { integer, pgTable, primaryKey, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: integer().primaryKey(),
  name: text(),
});

export const groups = pgTable('groups', {
  id: integer().primaryKey(),
  name: text(),
});

export const usersToGroups = pgTable(
  'users_to_groups',
  {
    userId: integer('user_id')
      .notNull()
      .references(() => users.id),
    groupId: integer('group_id')
      .notNull()
      .references(() => groups.id),
  },
  (t) => [primaryKey({ columns: [t.userId, t.groupId] })],
);

export const relations = defineRelations({ users, groups, usersToGroups },
  (r) => ({
    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(),
    },
  })
);

查询示例:

const res = await db.query.users.findMany({
  with: { 
    groups: true 
  },
});

// 响应类型
type Response = {
  id: number;
  name: string | null;
  groups: {
    id: number;
    name: string | null;
  }[];
}[];
关系查询 v1

之前,你需要通过连接表进行查询,然后对每个响应进行映射。

❌ 现在你不需要这样做了!

const response = await db._query.users.findMany({
	with: {
		usersToGroups: {
			columns: {},
			with: {
				groups: true,
			},
		},
	},
});

// 响应类型
type Response = {
  id: number;
  name: string | null;
  usersToGroups: {
    groups: {
       id: number;
       name: string | null;
    }
  }[];
}[];

预定义过滤器

在 Drizzle 关系定义中,预定义的 where 语句是一种多态关系的实现形式,虽然不是完全相同。 本质上,预定义过滤器允许你不仅仅通过指定列进行连接,还可以通过自定义的 where 语句来连接表。来看几个示例:

我们可以定义 groupsusers 之间的关系,在查询群组用户时,只获取 verified 字段为 true 的用户。

关系
模式
import { defineRelations } from "drizzle-orm";
import * as p from "drizzle-orm/pg-core";
import * as schema from './schema';

export const relations = defineRelations(schema,(r) => ({
    groups: {
      verifiedUsers: r.many.users({
        from: r.groups.id.through(r.usersToGroups.groupId),
        to: r.users.id.through(r.usersToGroups.userId),
        where: {
          verified: true,
        },
      }),
    },
  })
);

...

await db.query.groups.findMany({
    with: {
      verifiedUsers: true,
    },
});
IMPORTANT

你只能对目标表(to)指定过滤器。因此在此示例中,where 子句只应包含来自 users 表的列,因为我们是建立 到 users 的关系。

export const relations = defineRelations(schema,(r) => ({
    groups: {
      verifiedUsers: r.many.users({
        from: r.groups.id.through(r.usersToGroups.groupId),
        to: r.users.id.through(r.usersToGroups.userId),
        where: {
          verified: true,
        },
      }),
    },
  })
);

---

关系拆分部分

如果你需要将关系配置拆分成多个部分,可以使用 defineRelationsPart 辅助方法

import { defineRelations, defineRelationsPart } from 'drizzle-orm';
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
  users: {
    invitee: r.one.users({
      from: r.users.invitedBy,
      to: r.users.id,
    }),
    posts: r.many.posts(),
  }
}));

export const part = defineRelationsPart(schema, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  }
}));

然后你可以传给数据库实例使用

const db = drizzle(process.env.DB_URL, { relations: { ...relations, ...part } })
IMPORTANT

为确保 defineRelationsParts 正常工作,需遵守以下几个规则:

规则1:如果你用 parts 形式传递关系,调用 drizzle 数据库函数时必须保证顺序正确(主关系应该优先)

// ✅
const db = drizzle(process.env.DB_URL, { relations: { ...relations, ...part } })

// ❌
const db = drizzle(process.env.DB_URL, { relations: { ...part, ...relations } })

为什么很重要?

即使没有类型或运行时错误,这也是对象扩展符 ... 的正常工作规则。主关系递归推断所有表名,确保能在自动补全中可用。示例:

export const relations = defineRelations(schema, (r) => ({
  users: {
    invitee: r.one.users({
      from: r.users.invitedBy,
      to: r.users.id,
    }),
    posts: r.many.posts(),
  }
}));

export const part = defineRelationsPart(schema, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  }
}));

这里 relationspart 可分别表示为下面对象:

// relations
{
  "users": {"invitee": {...}, "posts": {...}},
  // 添加在这里,确保所有 schema 表存在于自动补全中
  "posts": {}
}

// part
{
  "posts": {"author": {...}}
}

合并 { ...relations, ...part } 结果是:

{
  "users": {"invitee": {...}, "posts": {...}},
  "posts": {"author": {...}}
}

而合并 { ...part, ...relations } 结果会是:

{
  "users": {"invitee": {...}, "posts": {...}},
  // 你可以看到最终对象中 posts 关系信息会丢失
  "posts": {}
}

规则2:你应该至少有一个关系,以便 drizzle 能推断所有表名用于自动补全。如果只想使用 parts,则其中一个 parts 应该是空的,比如:

export const mainPart = defineRelationsPart(schema);

这样就能正确推断所有表并拥有完整的 Schema 信息。

---

性能

在使用 Drizzle ORM 处理关系时,尤其对于数据量大或查询复杂的应用,优化数据库性能至关重要。
索引在加速数据检索方面扮演着关键角色,特别是在查询相关数据时。
本节概述了为每种 Drizzle ORM 定义的关系类型推荐的索引策略。

一对一关系

对于一对一关系,例如「用户邀请用户」或「用户拥有个人资料信息」示例,关键在于高效关联连接。

在一对一关系中,建议在被引用的外键列(关系中“目标”表所在表)创建索引。

为何重要

当你查询有相关一对一信息的数据时,Drizzle 执行 JOIN 操作。
在外键列上创建索引让数据库能快速定位目标表中相关行,大幅提升连接性能。

示例:

import * as p from 'drizzle-orm/pg-core';
import { defineRelations } from 'drizzle-orm';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text(),
});

export const profileInfo = p.pgTable('profile_info', {
	id: p.integer().primaryKey(),
	userId: p.integer('user_id').references(() => users.id),
	metadata: p.jsonb(),
});

export const relations = defineRelations({ users, profileInfo }, (r) => ({
	users: {
		profileInfo: r.one.profileInfo({
			from: r.users.id,
			to: r.profileInfo.userId,
		})
	}
}));

为了优化同时获取用户及其个人资料信息的查询,你应该为 profile_info 表的 userId 列创建索引。

import * as p from 'drizzle-orm/pg-core';
import { defineRelations } from 'drizzle-orm';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text(),
});

export const profileInfo = pgTable('profile_info', {
	id: p.integer().primaryKey(),
	userId: p.integer('user_id').references(() => users.id),
	metadata: p.jsonb(),
}, (table) => [
  p.index('profile_info_user_id_idx').on(table.userId)
]);

export const relations = defineRelations({ users, profileInfo }, (r) => ({
	users: {
		profileInfo: r.one.profileInfo({
			from: r.users.id,
			to: r.profileInfo.userId,
		})
	}
}));
CREATE INDEX idx_profile_info_user_id ON profile_info (user_id);

一对多关系

一对多关系同样依赖索引优化连接查询。例如用户与帖子多对一关系。

对于一对多关系,应在“多”方的外键列上创建索引(此表包含外键指向“一”方表)。

为何重要

当你查询一个用户及其帖子,或帖子及其作者时会执行连接操作。
posts 表的外键列(例如 authorId)建立索引能让数据库更高效地获取对应的帖子或作者。

示例:

import * as p from "drizzle-orm/pg-core";
import { defineRelations } from 'drizzle-orm';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text(),
});

export const posts = p.pgTable('posts', {
	id: p.integer().primaryKey(),
	content: p.text(),
	authorId: p.integer('author_id'),
});

export const relations = defineRelations({ users, posts }, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  },
  users: {
    posts: r.many.posts(),
  },
}));

为了优化用户和帖子查询,应该为 posts 表的 authorId 列创建索引。

import * as p from "drizzle-orm/pg-core";
import { defineRelations } from 'drizzle-orm';

export const users = p.pgTable('users', {
	id: p.integer().primaryKey(),
	name: p.text(),
});

export const posts = p.pgTable('posts', {
	id: p.integer().primaryKey(),
	content: p.text(),
	authorId: p.integer('author_id'),
}, (t) => [
  index('posts_author_id_idx').on(table.authorId)
]);

export const relations = defineRelations({ users, posts }, (r) => ({
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
    }),
  },
  users: {
    posts: r.many.posts(),
  },
}));
CREATE INDEX idx_posts_author_id ON posts (author_id);

多对多关系

多对多关系通过连接表实现,对连接表索引策略要求更复杂。以用户与群组示例中 usersToGroups 表为例。

针对多对多关系,建议为连接表创建以下索引:

  1. 单独为每个外键列分别创建索引:优化基于其中一侧关系的筛选或连接(例如,查询某用户的所有群组或某群组的所有用户)。
  2. 联合索引(组合索引)覆盖两个外键列:对快速定位连接关系尤为重要,提升多对多关系查询效率。

为何重要

在查询多对多关系,尤其是 Drizzle ORM 使用 through 实现时,数据库需高效定位连接表数据。

  • 对单个外键列(如 userIdgroupId)创建索引有助于从一侧查找另一侧的关联。
  • (userId, groupId) 创建联合索引可加速查询连接关系,提升多对多关联实体的查询性能。

示例:

用户与群组的多对多关联通过 usersToGroups 连接表实现。

import { defineRelations } from 'drizzle-orm';
import * as p from 'drizzle-orm/pg-core';

export const users = p.pgTable('users', {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const groups = p.pgTable('groups', {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const usersToGroups = p.pgTable(
  'users_to_groups',
  {
    userId: p.integer('user_id')
      .notNull()
      .references(() => users.id),
    groupId: p.integer('group_id')
      .notNull()
      .references(() => groups.id),
  },
  (t) => [p.primaryKey({ columns: [t.userId, t.groupId] })],
);

export const relations = defineRelations({ users, groups, usersToGroups },
  (r) => ({
    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(),
    },
  })
);

优化查询应为 usersToGroups 表创建以下索引:

import { defineRelations } from 'drizzle-orm';
import * as p from 'drizzle-orm/pg-core';

export const users = p.pgTable('users', {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const groups = p.pgTable('groups', {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const usersToGroups = p.pgTable(
  'users_to_groups',
  {
    userId: p.integer('user_id')
      .notNull()
      .references(() => users.id),
    groupId: p.integer('group_id')
      .notNull()
      .references(() => groups.id),
  },
  (t) => [
    p.primaryKey({ columns: [t.userId, t.groupId] }),
    p.index('users_to_groups_user_id_idx').on(table.userId),
    p.index('users_to_groups_group_id_idx').on(table.groupId),
    p.index('users_to_groups_composite_idx').on(table.userId, table.groupId),
  ],
);

export const relations = defineRelations({ users, groups, usersToGroups },
  (r) => ({
    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(),
    },
  })
);
CREATE INDEX idx_users_to_groups_user_id ON users_to_groups (user_id);
CREATE INDEX idx_users_to_groups_group_id ON users_to_groups (group_id);
CREATE INDEX idx_users_to_groups_composite ON users_to_groups (userId, groupId);

应用这些索引策略后,随着数据量增长和查询复杂性提升,你的 Drizzle ORM 应用在处理关系数据时性能会显著提升。请根据你的具体查询模式和需求选择最合适的索引。

---

外键

你可能注意到,relations 看起来类似于外键 — 它们甚至有一个 references 属性。那么它们有什么区别呢?

虽然外键和 relations 都表示表之间的关系,但它们工作在不同层级。

外键是数据库级别的约束,每次 insert/update/delete 操作时都会校验,如果违反约束会抛出错误。 而 relations 是高层抽象,仅用于应用层定义表之间的关系。 它们不会影响数据库架构,也不会隐式创建外键。

这意味着 relations 和外键可以同时使用,但彼此独立。 你可以不定义外键来定义 relations (反之亦然),这允许 relations 用于不支持外键的数据库。

下面两段示例在使用 Drizzle 关系查询时效果完全相同。

schema1.ts
schema2.ts
export const users = p.pgTable("users", {
  id: p.integer().primaryKey(),
  name: p.text(),
});

export const profileInfo = p.pgTable("profile_info", {
  id: p.integer().primaryKey(),
  userId: p.integer("user_id"),
  metadata: p.jsonb(),
});

export const relations = defineRelations({ users, profileInfo }, (r) => ({
  users: {
    profileInfo: r.one.profileInfo({
      from: r.users.id,
      to: r.profileInfo.userId,
    }),
  },
}));

关系歧义消除

Drizzle 还提供了 alias 选项,用于在同两张表之间定义多个关系时进行区分。例如你定义一个 posts 表,其中有 authorreviewer 两个关系:

import { pgTable, integer, text } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
 
export const users = pgTable('users', {
	id: integer('id').primaryKey(),
	name: text('name'),
});

export const posts = pgTable('posts', {
	id: integer('id').primaryKey(),
	content: text('content'),
	authorId: integer('author_id'),
	reviewerId: integer('reviewer_id'),
});
 
export const relations = defineRelations({ users, posts }, (r) => ({
  users: {
    posts: r.many.posts({
      alias: "author",
    }),
    reviewedPosts: r.many.posts({
      alias: "reviewer",
    }),
  },
  posts: {
    author: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
      alias: "author",
    }),
    reviewer: r.one.users({
      from: r.posts.authorId,
      to: r.users.id,
      alias: "reviewer",
    }),
  },
}));

故障排查