关于 zenstack-querying
zenstack-querying 是 ZenStack V3 数据工具包的查询技能,面向使用 TypeScript 操作数据库的开发者。它涵盖 Prisma 兼容 ORM、Kysely 查询构建器及验证、事务等能力。
主要功能
- 01Prisma 兼容 ORM 查询
- 02Kysely 查询构建器
- 03关联查询与嵌套写入
- 04数据库端计算字段
- 05输入验证与事务
- 06原始 SQL 与自定义过程
适用场景
- 01按条件筛选、排序和聚合数据
- 02在 ORM 查询中混合 SQL 表达式
- 03执行多步骤事务与关联写入
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zenstackhq/ skills --skill zenstack-querying安装后在 Claude Code 中输入/zenstack-querying确认已加载。
详细介绍
ZenStack V3 — 查询
ZenStack 客户端提供兼容 Prisma 的 ORM API,用于日常查询;对于 ORM API 无法表达的 SQL 形状查询,则提供 Kysely 查询构建器($qb)。Schema 语法请参阅
zenstack-schema-modeling;访问控制请参阅 zenstack-access-control。
创建客户端
从 @zenstackhq/orm 导入 ZenStackClient,并传入生成的 schema 和 Kysely dialect。
请自行安装匹配的驱动程序(参见 zenstack-project-setup)。
SQLite(better-sqlite3):
import { ZenStackClient } from '@zenstackhq/orm';
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
import SQLite from 'better-sqlite3';
import { schema } from './zenstack/schema';
export const db = new ZenStackClient(schema, {
dialect: new SqliteDialect({ database: new SQLite('./dev.db') }),
});
PostgreSQL(pg):
import { ZenStackClient } from '@zenstackhq/orm';
import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
import { schema } from './zenstack/schema';
import { Pool } from 'pg';
export const db = new ZenStackClient(schema, {
dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
});
MySQL(mysql2):
import { MysqlDialect } from '@zenstackhq/orm/dialects/mysql';
import { createPool } from 'mysql2';
export const db = new ZenStackClient(schema, {
dialect: new MysqlDialect({ pool: createPool(process.env.DATABASE_URL) }),
});
类型会根据 schema 完整推断,因此大多数代码都无需单独导入模型。需要显式类型时:客户端可使用 ClientContract<SchemaType>;也可导入生成的 models / input 类型,或使用 @zenstackhq/orm 中的 ModelResult 泛型,根据给定的 select / include 计算行类型。
如需强制执行访问策略,请用策略插件包装此客户端 — 参见
zenstack-access-control。
ORM API
模型访问器位于客户端上(db.user、db.post……)。此 API 与 Prisma Client 一致。
读取:findMany、findUnique、findUniqueOrThrow、findFirst、findFirstOrThrow、
exists(v3.2.0+,比 findFirst / count 开销更低)、count、aggregate、groupBy。
const users = await db.user.findMany({
where: { age: { gt: 18 }, email: { endsWith: '@acme.com' } },
orderBy: { createdAt: 'desc' },
skip: 0,
take: 20,
});
const user = await db.user.findUnique({ where: { id: 1 } });
const n = await db.user.count({ where: { age: { gte: 18 } } });
筛选运算符:equals、not、in、notIn、contains、startsWith、endsWith、lt、
lte、gt、gte、between;逻辑运算符 AND / OR / NOT;列表运算符 has / hasEvery / hasSome / isEmpty
(Postgres)。对于一对多关系,使用 some / every / none 筛选;对于一对一关系,直接筛选字段。
聚合 / groupBy 支持 _count、_sum、_avg、_min、_max;groupBy 使用 having 筛选
聚合结果:
await db.post.groupBy({ by: 'authorId', _count: true, having: { viewCount: { _sum: { gt: 100 } } } });
创建:create(支持嵌套写入,并在事务中运行)、createMany(不支持嵌套关系;返回计数)、createManyAndReturn。
await db.user.create({
data: { email: '[email protected]', posts: { create: [{ title: 'Hello' }] } },
});
更新:update、updateMany / updateManyAndReturn(仅支持标量字段)、upsert。列表
字段使用 push / set:
await db.post.update({ where: { id: 1 }, data: { topics: { push: 'webdev' } } });
await db.post.upsert({
where: { id: 1 },
create: { id: 1, title: 'New' },
update: { title: 'Updated' },
});
删除:delete、deleteMany、deleteManyAndReturn。
关系查询 — select / include / omit
select:选择特定字段;对于关系,传入嵌套对象。不能与include/omit同时使用。include:提取关系(所有字段使用true,或传入对象以筛选关系 / 使用where过滤关系)。 不能与select同时使用。omit:排除特定标量字段。不能与select同时使用。
await db.user.findMany({
include: { posts: { where: { published: true }, select: { id: true, title: true } } },
});
create / update / upsert 中的嵌套写入可以对相关记录执行 create、connect、disconnect、更新和删除(支持深层嵌套),所有操作都在一个事务中完成。
计算字段
在 ZModel 中声明为 @computed 的字段(参见 zenstack-schema-modeling)会在数据库端进行计算。在客户端选项 computedFields 中以 Kysely 表达式提供其实现;之后,它的行为就像常规字段一样 — 查询会返回该字段,并可在 select、where、orderBy 和 aggregate 中使用。
const db = new ZenStackClient(schema, {
dialect,
computedFields: {
User: {
// SQL: (SELECT COUNT(*) FROM "Post" WHERE "Post"."authorId" = "User"."id")
postCount: (eb) =>
eb.selectFrom('Post')
.whereRef('Post.authorId', '=', 'id')
.select(({ fn }) => fn.countAll<number>().as('count')), // selection must be named
},
},
});
await db.user.findFirst(); // postCount included
await db.user.findFirst({ select: { email: true, postCount: true } });
await db.user.findMany({ where: { postCount: { gt: 1 } }, orderBy: { postCount: 'desc' } });
await db.user.aggregate({ _avg: { postCount: true } });
回调的第二个参数是一个包含 modelAlias 的 context;遇到冲突时,可使用 sql.ref(\${modelAlias}.id`)(从 @zenstackhq/orm/helpers导入sql`)限定所属模型的列。
多态模型
对于使用 @@delegate 继承的模型(参见 zenstack-schema-modeling),可通过常规模型访问器进行查询:
- 创建具体模型(
db.video.create(...))— 基础行会自动创建。不能直接创建基础模型。 - 查询基础模型(
db.content.findMany()),每个结果都会包含具体模型的字段(除非使用select缩小字段范围)。查询具体模型会返回基础模型和具体模型的字段。 - 删除任一侧时,对应的另一行也会一并删除。
const user = await db.user.create({ data: { email: '[email protected]' } });
await db.post.create({ data: { name: 'Post1', content: 'Hi', ownerId: user.id } });
await db.video.create({ data: { name: 'Video1', url: 'http://v/1', ownerId: user.id } });
const content = await db.content.findFirstOrThrow(); // includes concrete fields
await db.user.findFirstOrThrow({ include: { contents: true } });
判别值的类型是字符串字面量(或枚举成员),因此检查该值会将结果类型收窄为匹配的具体模型:
const c = await db.content.findUniqueOrThrow({ where: { id: 1 } });
if (c.type === 'video') console.log(c.url); // narrowed to Video
else if (c.type === 'image') console.log(c.data);
强类型 JSON
使用自定义 type + @json 声明的字段(参见 zenstack-schema-modeling)会以强类型形式返回(类型派生自 ZModel 类型),并且创建/更新时会根据该类型验证输入。验证较为宽松——允许包含类型中未定义的额外字段。
// query results are typed: user.profile.age / user.profile.gender
const user = await db.user.create({
data: { email: '[email protected]', profile: { gender: 'male', age: 20 } },
});
await db.user.update({
where: { id: user.id },
data: { profile: { ...user.profile, tag: 'vip' } }, // extra `tag` is allowed
});
数据验证
在 ZModel 中声明验证规则,并由 ORM 对 create/update 输入执行验证(不会对查询构建器 / 原始 SQL 执行验证)。验证失败时会抛出原因代码为 INVALID_INPUT 的 ORMError。每个属性都可以附加一个可选的尾随 message。
字段属性
- 字符串长度 / 列表:
@length(min?, max?) - 字符串内容:
@startsWith、@endsWith、@contains、@email、@url、@phone(E.164)、@datetime(ISO 8601)、@regex(pattern) - 字符串转换(保存前应用):
@lower、@upper、@trim - 数字:
@gt、@gte、@lt、@lte
model User {
email String @email("Invalid email")
password String @length(8, 128)
age Int @gte(0) @lte(150)
website String? @url
}
模型级 @@validate(condition, message?, path?) 用于跨字段规则,并支持以下辅助函数:now()、length()、startsWith/endsWith/contains、isEmail/isUrl/isPhone/isDateTime、regex()、has/hasSome/hasEvery/isEmpty。
model Event {
startDate DateTime
endDate DateTime
tags String[]
@@validate(endDate > startDate, "End date must be after start date")
@@validate(!isEmpty(tags), "At least one tag is required")
}
验证会在访问策略之前运行,并且在访问数据库之前完成。有关访问控制,请参阅 zenstack-access-control。编辑验证规则(或任何 ZModel 内容,包括 procedure 声明)后,运行 zen generate 以重新生成客户端(该命令也会验证架构);如果只想验证而不重新生成,请运行 zen check。
事务 — $transaction
顺序执行 — 操作数组,按顺序执行,不支持跨操作访问:
const [a, b] = await db.$transaction([
db.user.create({ data: { name: 'Alice' } }),
db.user.create({ data: { name: 'Bob' } }),
]);
交互式 — 回调接收事务客户端;操作结果可供后续步骤使用。请保持回调简短。
const post = await db.$transaction(async (tx) => {
const user = await tx.user.create({ data: { name: 'Alice' } });
return tx.post.create({ data: { title: 'Hi', authorId: user.id } });
});
ORM 调用结果是惰性 Promise——只有在被等待时(直接等待或在
$transaction内等待)才会执行。
Kysely 查询构建器 — $qb 和 $expr
当 ORM API 无法满足需求时,可使用完整的 Kysely API(类型基于架构)。有关构建器 API,请参阅 https://kysely.dev。
const rows = await db.$qb
.selectFrom('User')
.select(['id', 'name'])
.where('age', '>', 18)
.execute();
使用 $expr 将 Kysely 表达式注入 ORM 的 where 中,从而混合使用两者:
await db.post.findMany({
where: {
published: true,
$expr: (eb) => eb('viewCount', '>', 100),
},
});
查询构建器会绕过访问策略和 ORM 级验证——安装了策略插件时,请牢记这一点。
自定义过程(v3.2.0+)
在 ZModel 中声明带类型的操作,在客户端构造时实现,并通过 $procs 调用。
procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
mutation procedure signUp(email: String) : User
const db = new ZenStackClient(schema, {
dialect,
procedures: {
getUserFeeds: ({ client, args }) =>
client.post.findMany({ where: { authorId: args.userId }, take: args.limit }),
signUp: ({ client, args }) => client.user.create({ data: { email: args.email } }),
},
});
const user = await db.$procs.signUp({ args: { email: '[email protected]' } });
const feeds = await db.$procs.getUserFeeds({ args: { userId: user.id, limit: 20 } });
回调运行前,会根据 ZModel 类型验证参数;不会验证返回值——请自行确保其符合要求。发生错误时,抛出 ORMError。
原始 SQL
$queryRaw / $executeRaw 使用带标签的模板(参数化且安全);*Unsafe 变体接收普通字符串,存在 SQL 注入风险。
const users = await db.$queryRaw<{ id: number; email: string }[]>`
SELECT id, email FROM "User" WHERE role = ${role}`;
const affected = await db.$executeRaw`UPDATE "User" SET name = ${name} WHERE id = ${id}`;
原始 SQL 会绕过访问控制;安装策略插件后,必须设置 dangerouslyAllowRawSql。
日志记录
向 ZenStackClient 传入 log 选项——可以是级别数组,也可以是函数。该选项会转发给底层 Kysely 实例。
const db = new ZenStackClient(schema, {
dialect,
log: ['query', 'error'],
// or a function:
// log: (event) => console.log(`[${event.level}] ${event.queryDurationMillis}ms`),
});
错误处理
所有 ORM 错误都是 ORMError。检查 error.reason(CONFIG_ERROR、INVALID_INPUT、NOT_FOUND、REJECTED_BY_POLICY、DB_QUERY_ERROR、NOT_SUPPORTED、INTERNAL_ERROR);其他字段包括 model、dbErrorCode、dbErrorMessage、rejectedByPolicyReason,以及(针对 DB_QUERY_ERROR)sql/sqlParams。
import { ORMError } from '@zenstackhq/orm';
try {
await userDb.post.create({ data: { title: '' } });
} catch (e) {
if (e instanceof ORMError && e.reason === 'REJECTED_BY_POLICY') {
// handle access-denied
}
}
参考文档
此主题的完整 ZenStack 文档已打包在 references/ 下:
- client.md — 创建
ZenStackClient(SQLite/Postgres/MySQL 方言) - orm-overview.md — ORM 概述
- api-overview.md — ORM API 概述
- api-find.md — 查找操作
- api-create.md — 创建/嵌套写入
- api-update.md — 更新/upsert/嵌套写入
- api-delete.md — 删除操作
- api-filter.md — 筛选运算符
- api-aggregate.md — 聚合
- api-count.md — 计数
- api-group-by.md — groupBy
- api-transaction.md —
$transaction - api-raw-sql.md — 原始 SQL
- query-builder.md — Kysely
$qb逃生通道 - custom-procedures.md — 自定义过程
- computed-fields.md — 运行时计算字段
- polymorphism.md — 查询多态模型
- typed-json.md — 运行时强类型 JSON 字段
- validation.md — 数据验证
- input-validation-reference.md — 验证属性参考
- inferred-types.md — 推断的模型/输入类型
- logging.md — 客户端日志配置
- errors.md —
ORMError和错误原因