Drizzle 查询

Drizzle ORM 设计为 SQL 之上的一个轻量级类型层。
我们真心相信,我们已经设计出了从 TypeScript 操作 SQL 数据库的最佳方式,现在是时候让它变得更好了。

关系查询旨在为你提供出色的开发者体验,用于从 SQL 数据库中查询嵌套的关系数据,避免多次 join 和复杂的数据映射。

它是对现有 schema 定义和查询构建器的扩展。
你可以根据需要选择性启用它。
我们确保你既拥有一流的开发者体验,也拥有出色的性能。

重要

在关系查询中,对表列的引用必须通过回调参数进行,而不是通过导入的表对象。 这适用于所有接受回调的子句——orderBy、where.RAW、extras 和 extras 中的子查询。 回调会暴露当前查询作用域中带别名的表,这对于在嵌套或自引用查询中正确生成 SQL 是必需的。

❌

示例

await db.query.posts.findMany({
  orderBy: sql`${posts.id} asc`, // <- ❌ 直接使用表
  with: {
    comments: {
      orderBy: sql`${comments.id} desc`, // <- ❌ 直接使用表
    },
  },
});
await db.query.users.findMany({
  where: {
    RAW: sql`LOWER(${users.name}) LIKE 'john%'`, // <- ❌ 直接使用表
  },
});
await db.query.users.findMany({
  extras: {
    loweredName: sql<string>`lower(${users.name})`, // <- ❌ 直接使用表
    totalPostsCount: db.$count(posts, eq(posts.authorId, users.id)),  // <- ❌ 直接使用表
  },
});
✅

示例

await db.query.posts.findMany({
  orderBy: (t) => sql`${t.id} asc`, // <- ✅ 使用回调
  with: {
    comments: {
      orderBy: (t, { desc }) => desc(t.id), // <- ✅ 使用回调
    },
  },
});
await db.query.users.findMany({
  where: {
    RAW: (t) => sql`LOWER(${t.name}) LIKE 'john%'`, // <- ✅ 使用回调
  },
});
await db.query.users.findMany({
  extras: {
    loweredName: (t, { sql }) => sql<string>`lower(${t.name})`, // <- ✅ 使用回调
    totalPostsCount: (t) => db.$count(posts, eq(posts.authorId, t.id)), // <- ✅ 使用回调
  },
});

drizzle 导入路径取决于你正在使用的 数据库驱动。

index.ts
schema.ts
relations.ts
import { relations } from "./relations";
import { drizzle } from "drizzle-orm/...";

const db = drizzle(process.env.DATABASE_URL, { relations });

const result = await db.query.users.findMany({
  with: {
    posts: true,
  },
});
[{
	id: 10,
	name: "Dan",
	posts: [
		{
			id: 1,
			content: "SQL is awesome",
			authorId: 10,
		},
		{
			id: 2,
			content: "But check relational queries",
			authorId: 10,
		}
	]
}]

关系查询是 Drizzle 原始 查询构建器 的扩展。
你需要在调用 drizzle() 初始化时提供 schema 文件/多个文件中的所有 tables 和 relations,然后直接使用 db.query API。

drizzle 导入路径取决于你正在使用的 数据库驱动。

index.ts
schema.ts
relations.ts
import { relations } from './relations';
import { drizzle } from 'drizzle-orm/...';

const db = drizzle(process.env.DATABASE_URL,{ relations });

await db.query.users.findMany(...);

Drizzle 提供 .findMany() 和 .findFirst() API。

查找多个

const users = await db.query.users.findMany();
// 结果类型
const result: {
	id: number;
	name: string;
	verified: boolean;
	invitedBy: number | null;
}[];

查找第一个

.findFirst() 将向查询中添加 limit 1。

const user = await db.query.users.findFirst();
// 结果类型
const result: {
	id: number;
	name: string;
	verified: boolean;
	invitedBy: number | null;
};

包含关联

With 运算符可让你将来自多个相关表的数据组合起来,并正确聚合结果。

获取所有带有评论的帖子:

const posts = await db.query.posts.findMany({
	with: {
		comments: true,
	},
});

获取第一条带有评论的帖子:

const post = await db.query.posts.findFirst({
	with: {
		comments: true,
	},
});

你可以根据需要连续嵌套 with 语句。
对于任何嵌套的 with 查询,Drizzle 会使用 Core Type API 推断类型。

获取所有带有帖子用户。每个帖子应包含评论列表:

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

部分字段选择

columns 参数让你可以包含或省略你想从数据库中获取的列。

Drizzle 在查询层面执行部分选择,不会从数据库传输任何额外数据。

请记住,Drizzle 只会输出一条 SQL 语句。

仅获取 id、content 并包含 comments 的所有 posts:

const posts = await db.query.posts.findMany({
	columns: {
		id: true,
		content: true,
	},
	with: {
		comments: true,
	}
});

获取所有不包含 content 的 posts:

const posts = await db.query.posts.findMany({
	columns: {
		content: false,
	},
});

当同时存在 true 和 false 选择选项时,所有 false 选项都会被忽略。

如果你包含 name 字段并排除 id 字段,id 的排除将是多余的,
除了 name 之外的所有字段本来都会被排除。

在同一个查询中排除和包含字段:

const users = await db.query.users.findMany({
	columns: {
		name: true,
		id: false //被忽略
	},
});
// 结果类型
const users: {
	name: string;
};

仅包含嵌套关系中的列:

const res = await db.query.users.findMany({
	columns: {},
	with: {
		posts: true
	}
});
// 结果类型
const res: {
	posts: {
		id: number,
		text: string
	}
}[];

嵌套部分字段选择

就像使用 partial select 一样,您可以包含或排除嵌套关联的列:

const posts = await db.query.posts.findMany({
	columns: {
		id: true,
		content: true,
	},
	with: {
		comments: {
			columns: {
				authorId: false
			}
		}
	}
});

选择过滤器

就像在我们的类 SQL 查询构建器中一样,
关系查询 API 允许你使用我们的 operators 列表来定义过滤器和条件。

你可以从 drizzle-orm 中导入它们,也可以使用回调语法:

const users = await db.query.users.findMany({
	where: {
		id: 1
	}
});
select
  "d0"."id" as "id",
  "d0"."name" as "name"
from
  "users" as "d0"
where
  "d0"."id" = 1

查找 id=1 的帖子,以及在特定日期之前创建的评论:

await db.query.posts.findMany({
  where: {
    id: 1,
  },
  with: {
    comments: {
      where: {
        createdAt: { lt: new Date() },
      },
    },
  },
});

所有过滤器操作符列表

where: {
    OR: [],
    AND: [],
    NOT: {},
    RAW: (table) => sql`${table.id} = 1`,

    // 按关系过滤
    [relation]: {},

	  // 按列过滤
    [column]: {
      OR: [],
      AND: [],
      NOT: {},
      eq: 1,
      ne: 1,
      gt: 1,
      gte: 1,
      lt: 1,
      lte: 1,
      in: [1],
      notIn: [1],
      like: "",
      notLike: "",
      isNull: true,
      isNotNull: true,
    },
},

示例

简单 eq
使用 AND
使用 OR
使用 NOT
使用 RAW 的复杂示例
const response = await db.query.users.findMany({
  where: {
    age: 15,
  },
});
select
  "d0"."id" as "id",
  "d0"."name" as "name",
  "d0"."age" as "age"
from
  "users" as "d0"
where
  "d0"."age" = 15

关系筛选

使用 Drizzle Relations,你不仅可以按正在查询的表进行筛选,还可以按查询中包含的任何表进行筛选。

示例: 获取所有 ID>10 且至少有一篇内容以 “M” 开头的帖子用户 users

const usersWithPosts = await db.query.usersTable.findMany({
  where: {
    id: {
      gt: 10
    },
    posts: {
      content: {
        like: 'M%'
      }
    }
  },
});

示例: 获取所有带有帖子(posts)的 users,仅当用户至少有 1 篇帖子时

const response = await db.query.users.findMany({
  with: {
    posts: true,
  },
  where: {
    posts: true,
  },
});

限制与偏移

Drizzle ORM 为查询和嵌套实体提供了 limit 和 offset API。

查找 5 篇帖子:

await db.query.posts.findMany({
	limit: 5,
});

查找帖子,并最多获取 3 条评论:

await db.query.posts.findMany({
	with: {
		comments: {
			limit: 3,
		},
	},
});
IMPORTANT

offset 现在也可以在 with 表中使用了!

await db.query.posts.findMany({
	limit: 5,
	offset: 2, // 正确 ✅
	with: {
		comments: {
			offset: 3, // 正确 ✅
			limit: 3,
		},
	},
});

查找带有评论的帖子,评论范围从第 6 篇帖子到第 10 篇帖子:

await db.query.posts.findMany({
	with: {
		comments: true,
	},
  limit: 5,
  offset: 5,
});

排序依据

Drizzle 为关系型查询构建器提供了排序 API。

你可以使用相同的排序 核心 API,或者在回调中使用 order by 操作符,无需导入。

重要

当你在同一个表上使用多个 orderBy 语句时,它们会按照你添加它们的相同顺序包含在查询中

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

按 asc + desc 排序:

  await db.query.posts.findMany({
    orderBy: { id: "asc" },
    with: {
      comments: {
        orderBy: { id: "desc" },
      },
    },
  });

你也可以在 order by 语句中使用自定义 sql:

await db.query.posts.findMany({
  orderBy: (t) => sql`${t.id} asc`,
  with: {
    comments: {
      orderBy: (t, { desc }) => desc(t.id),
    },
  },
});

包含自定义字段

关系型查询 API 允许你添加额外的自定义字段。
当你需要检索数据并对其应用额外函数时,这非常有用。

IMPORTANT

目前 extras 中不支持聚合,请改用 core queries。

import { sql } from 'drizzle-orm';

await db.query.users.findMany({
  extras: {
    loweredName: (table) => sql`lower(${table.name})`,
  },
});
await db.query.users.findMany({
	extras: {
		loweredName: (users, { sql }) => sql`lower(${users.name})`,
	},
})

lowerName 作为键会包含在返回对象的所有字段中。

IMPORTANT

如果你为任何 extras 字段指定了 .as("<alias>") - drizzle 将忽略它

要获取所有带分组的用户,并同时包含 fullName 字段(它由 firstName 和 lastName 拼接而成), 你可以使用 Drizzle 关系型查询构建器执行以下查询。

const res = await db.query.users.findMany({
	extras: {
		fullName: (users, { sql }) => sql<string>`concat(${users.name}, " ", ${users.name})`,
	},
	with: {
		groups: true,
	},
});
// 结果类型
const res: {
    id: number;
    name: string;
    age: number | null;
    invitedBy: number | null;
    fullName: string;
    groups: {
        id: number;
        name: string;
        description: string | null;
    }[];
}[]

要获取所有带评论的帖子,并添加额外字段来计算帖子内容的大小以及每条评论内容的大小:

const res = await db.query.posts.findMany({
	extras: {
		contentLength: (table, { sql }) => sql<number>`length(${table.content})`,
	},
	with: {
		comments: {
			extras: {
				commentSize: (table, { sql }) => sql<number>`length(${table.content})`,
			},
		},
	},
});
// 结果类型
const res: {
    id: number;
    content: string;
    authorId: number | null;
    createdAt: Date;
    contentLength: number;
    comments: {
        id: number;
        content: string;
        createdAt: Date;
        creator: number | null;
        postId: number | null;
        commentSize: number;
    }[];
}[]

包含子查询

你也可以在关系查询中使用子查询,以利用自定义 SQL 语法的强大能力

获取有帖子且每个用户的帖子总数

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

await db.query.users.findMany({
  with: {
    posts: true
  },
  extras: {
    totalPostsCount: (table) => db.$count(posts, eq(posts.authorId, table.id)),
  }
});
SELECT
	"d0"."id" AS "id",
	"d0"."name" AS "name",
	"d0"."age" AS "age",
	"d0"."invited_by" AS "invitedBy",
	(
		(
			SELECT
				count(*)
			FROM
				"posts"
			WHERE
				"posts"."author_id" = "d0"."id"
		)
	) AS "totalPostsCount",
	coalesce(
		(
			SELECT
				json_group_array(json_object('id', "id", 'content', "content", 'authorId', "authorId", 'createdAt', "createdAt")) AS "r"
			FROM
				(
					SELECT
						"d1"."id" AS "id",
						"d1"."content" AS "content",
						"d1"."author_id" AS "authorId",
						"d1"."created_at" AS "createdAt"
					FROM
						"posts" AS "d1"
					WHERE
						"d0"."id" = "d1"."author_id"
				) AS "t"
		),
		jsonb_array ()
	) AS "posts"
FROM
	"users" AS "d0"

预备语句

预备语句旨在大幅提升查询性能 — 请看这里。

在本节中,你将学习如何定义占位符,并使用 Drizzle 关系查询构建器执行预备语句。

where 中的占位符
const prepared = db.query.users.findMany({
    where: { id: { eq: sql.placeholder("id") } },
    with: {
      posts: {
        where: { id: 1 },
      },
    },
}).prepare();

const usersWithPosts = await prepared.execute({ id: 1 });
limit 中的占位符
const prepared = db.query.users.findMany({
    with: {
      posts: {
        limit: sql.placeholder("limit"),
      },
    },
  }).prepare();

const usersWithPosts = await prepared.execute({ limit: 1 });
offset 中的占位符
const prepared = db.query.users.findMany({
	offset: sql.placeholder('offset'),
	with: {
		posts: true,
	},
}).prepare();

const usersWithPosts = await prepared.execute({ offset: 1 });
多个占位符
const prepared = 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();

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