Drizzle 查询

Drizzle ORM 旨在成为 SQL 之上的一层轻量级类型化封装。
我们坚信,我们已经设计出了从 TypeScript 操作 SQL 数据库的最佳方式,现在是把它做得更好的时候了。

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

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

重要

在关系查询中,对表列的引用必须通过回调参数进行,而不能通过导入的表对象进行。 这适用于所有接受回调的子句——orderBywhere.RAWextrasextras 中的子查询。 回调会暴露当前查询作用域下的已别名表,这对于在嵌套或自引用查询中正确生成 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`${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`${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)), // <- ✅ 使用回调
  },
});

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

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 很棒",
			authorId: 10,
		},
		{
			id: 2,
			content: "但请看看关系查询",
			authorId: 10,
		}
	]
}]

关系查询是对 Drizzle 原始 查询构建器 的扩展。
你需要在 drizzle() 初始化时提供 schema 文件/文件中的所有 tablesrelations
然后只需使用 db.query API 即可。

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

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 语句。

仅获取 idcontent 并包含 comments 的所有帖子:

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

获取所有不包含 content 的帖子:

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

当同时存在 truefalse 选择选项时,所有 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
    },
},

示例

简单相等
使用 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,仅当 user 至少有 1 篇 post 时

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

限制与偏移

Drizzle ORM 为查询以及嵌套实体提供了 limitoffset 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>`char_length(${table.content})`,
	},
	with: {
		comments: {
			extras: {
				commentSize: (table, { sql }) => sql<number>`char_length(${table.content})`,
			},
		},
	},
});
// 结果类型
const res: {
    id: number;
    createdAt: Date;
    content: string;
    authorId: number | null;
    contentLength: number;
    comments: {
        id: number;
        createdAt: Date;
        content: string;
        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`,
	(
		(
			SELECT
				count(*)
			FROM
				`posts`
			WHERE
				`posts`.`author_id` = `d0`.`id`
		)
	) AS `totalPostsCount`,
	`posts`.`r` AS `posts`
FROM
	`users` AS `d0`
	LEFT JOIN LATERAL (
		SELECT
			coalesce(json_arrayagg(json_object('id', `id`, 'name', `name`, 'authorId', `authorId`)), json_array()) AS `r`
		FROM
			(
				SELECT
					`d1`.`id` AS `id`,
					`d1`.`name` AS `name`,
					`d1`.`author_id` AS `authorId`
				FROM
					`posts` AS `d1`
				WHERE
					`d0`.`id` = `d1`.`author_id`
			) AS `t`
	) AS `posts` ON TRUE;

预处理语句

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

在本节中,你可以学习如何定义占位符,并使用 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 });