Drizzle 查询

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

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

它是现有模式定义和查询构建器的一个扩展。
你可以根据需要选择使用它。
我们确保你同时拥有最佳的开发者体验和性能。

重要

在关系查询中,对表列的引用必须通过回调参数来进行,而不是通过导入的表对象。 这适用于所有接受回调的子句——orderBywhere.RAWextras 以及 extras 内部的 subqueries。 回调会暴露当前查询作用域中已别名化的表,这对于在嵌套或自引用查询中正确生成 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 很棒",
			authorId: 10,
		},
		{
			id: 2,
			content: "但看看关系查询",
			authorId: 10,
		}
	]
}]

关系查询是 Drizzle 原始 查询生成器 的一个扩展。
你需要在初始化 drizzle() 时提供全部的 tablesrelations,然后使用 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 会使用 核心类型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
	}
}[];

嵌套部分字段选择

就像 部分选择,你也可以包含或排除嵌套关联的列:

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

选择过滤

和我们的类 SQL 查询构建器一样, 关系查询 API 允许你使用我们 操作符 列表定义过滤和条件。

你可以从 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: "",
      ilike: "",
      notLike: "",
      notIlike: "",
      isNull: true,
      isNotNull: true,
      arrayOverlaps: [1, 2],
      arrayContained: [1, 2],
      arrayContains: [1, 2]
    },
},

示例

简单等于
使用 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 关系查询,你不仅可以按查询的表进行过滤,还可以按查询中包含的任何表进行过滤。

示例: 获取 ID 大于 10 且至少有一条内容以 “M” 开头的帖子的 users

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

示例: 只获取至少有一条帖子的用户及帖子:

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 现在也可以用于关联表中!

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",
  },
});

同时使用升序和降序排序:

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

你还可以在排序语句中使用自定义 sql

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

包含自定义字段

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

IMPORTANT

目前 extras 中不支持聚合,请使用 核心查询 实现聚合。

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("<别名>") ,Drizzle 会忽略该操作。

用关系查询构建器获取所有用户及其群组,并包含 fullName 字段(由 firstName 和 lastName 拼接):

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",
  "posts"."r" AS "posts"
FROM
  "users" AS "d0"
  LEFT JOIN LATERAL (
    SELECT
      coalesce(json_agg(row_to_json("t".*)), '[]') AS "r"
    FROM
      (
        SELECT
          "d1"."id" AS "id",
          "d1"."content" AS "content",
          "d1"."author_id" AS "authorId",
          "d1"."created_at"::text AS "createdAt"
        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("query_name");

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

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

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("query_name");

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