Orchid ORM 查询构建器

53 阅读1分钟

OrchidORM 由查询构建器(如 Knex 或 Kysely)和关系操作层(类似 Prisma)两部分构成。

查询构建器负责生成并执行 selectinsertupdatedelete 等 SQL 查询语句。

ORM 则支持定义 belongsTohasMany 等关联关系,可对关联数据进行查询、级联操作(创建 / 更新 / 删除)及其他高级查询

安装

使用以下命令进行安装:

npm i orchid-orm
# 或
pnpm i orchid-orm

orchidORM 初始化函数

orchidORM() 作为 ORM 的初始化入口,接受两个参数:

  1. 数据库连接配置(必填)

  2. 数据表模型映射(必填)

    • 格式:{ [表名]: 表类 }
    • 表类定义方式见后续章节

函数返回一个 ORM 实例,包含:

  • 所有注册的表模型(直接访问)
  • ORM 内置方法(以 $ 前缀标识,避免命名冲突)
import { orchidORM } from 'orchid-orm';

// 导入所有表
import { UserTable } from './tables/user';
import { MessageTable } from './tables/message';

export const db = orchidORM(
  {
    // databaseURL 的详细信息如下
    databaseURL: process.env.DATABASE_URL,

    // ssl 和 schema 可以在此处设置,也可以作为 databaseURL 参数:
    ssl: true,
    schema: 'my_schema',

    // 数据库启动时重试连接,默认情况下不重试,
    // 请参阅下面的 `connectRetry` 部分
    connectRetry: true,

    // 日志选项,默认值为 false
    log: true,

    // 自动为关系创建外键
    // 请参阅下面的 `autoForeignKeys` 部分
    autoForeignKeys: true,

    // 选项以隐式方式创建命名的预备语句,默认值为 false
    autoPreparedStatements: true,
  },
  {
    user: UserTable,
    message: MessageTable,
  },
);

也可以通过自定义适配器初始化

import { orchidORM, Adapter } from 'orchid-orm';

export const db = orchidORM(
  {
    adapter: new Adapter({ databaseURL: process.env.DATABASE_URL }),
    log: true,
  },
  {
    // ...tables
  },
);

定义基础表

定义一个基础表类用于扩展,建议将此代码与 db 文件分离:

import { createBaseTable } from 'orchid-orm';

export const BaseTable = createBaseTable();

export const { sql } = BaseTable;

导出 sql 以便与 BaseTable 中定义的自定义列类型关联使用。

可选择性自定义列类型行为,使其应用于所有表:

import { createBaseTable } from 'orchid-orm';
// 可选地,使用以下验证集成之一:
import { zodSchemaConfig } from 'orchid-orm-schema-to-zod';
import { valibotSchemaConfig } from 'orchid-orm-valibot';

export const BaseTable = createBaseTable({
  // 如果数据库中的列为 snake_case,请设置为 true
  snakeCase: true,

  // 可选,但推荐:从您的表派生并使用验证模式
  schemaConfig: zodSchemaConfig,
  // 或
  schemaConfig: valibotSchemaConfig,

  columnTypes: (t) => ({
    // 默认情况下,时间戳返回为字符串,覆盖为 Data
    timestamp: () => t.timestamp().asDate(),

    // 在 BaseTable 中定义自定义类型以便以后在表中使用
    myEnum: () => t.enum('myEnum', ['one', 'two', 'three']),
  }),
});

export const { sql } = BaseTable;

自定义列类型详情见覆盖列类型

定义数据表

数据表通过类形式定义,必须包含 table 和 columns 属性:

  • table: 数据库表名(标记为 readonly 用于类型检查)
  • columns: 定义表结构(详见列模式
import { Selectable, DefaultSelect, Insertable, Updatable } from 'orchid-orm';
// 从上一步的文件导入 BaseTable:
import { BaseTable } from './baseTable';

// 导出各种用例的用户类型:
export type User = Selectable<UserTable>;
export type UserDefault = DefaultSelect<UserTable>;
export type UserNew = Insertable<UserTable>;
export type UserUpdate = Updateable<UserTable>;

export class UserTable extends BaseTable {
  readonly table = 'user';
  columns = this.setColumns((t) => ({
    id: t.identity().primaryKey(),
    name: t.string(),
    password: t.string(),
    ...t.timestamps(),
  }));
}

注册数据表

定义完成后,需在 db 文件中注册表类:

import { UserTable } from './tables/user';

export const db = orchidORM(
  {
    databaseURL: process.env.DATABASE_URL,
  },
  {
    user: UserTable, // 注册 UserTable
  },
);

使用方式

通过 db 实例访问表类进行查询:

import { db } from './db';

// 正确:通过 db 实例访问
const user = await db.user.findBy({ name: 'John' });

// 错误:直接使用表类无法执行查询
await UserTable.findBy({ name: 'John' }); // ❌ 无效

单独配置表的 snake_case

如需为特定表单独配置 snake_case:

import { BaseTable } from './baseTable';

export class SnakeCaseTable extends BaseTable {
  readonly table = 'table';
  snakeCase = true; // 单独启用 snake_case

  columns = this.setColumns((t) => ({
    // 对应数据库中的 snake_column
    snakeColumn: t.text(),
  }));
}

定义表类

表类类似于其他 ORM 中的模型或实体,但职责不同:

  • 其他 ORM 的模型 / 实体通常包含业务逻辑
  • OrchidORM 的表类仅用于配置数据库表结构(列、关系)、定义软删除、查询钩子(回调)等数据库相关特性,不涉及应用逻辑
import { BaseTable, sql } from './baseTable';
import { PostTable } from './post.table';
import { SubscriptionTable } from './subscription.table';

export class UserTable extends BaseTable {
  schema = 'customSchema'; // 数据库模式(可选)
  readonly table = 'user'; // 数据库表名(必填,TypeScript 类型检查需要)

  comment = '这是一个用于存储用户的表'; // 表注释(数据库元数据)

  noPrimaryKey = true; // 无主键表(需显式声明,否则 ORM 会报错)

  snakeCase = true; // 单独启用 snake_case 命名(基类已配置时可省略)

  language = 'spanish'; // 全文搜索语言(默认 'english')

  readonly softDelete = true; // 启用软删除(默认使用 deletedAt 列,可指定列名)

  columns = this.setColumns(
    // 第一参数:定义列结构
    (t) => ({
      id: t.uuid().primaryKey(), // 主键(必填,除非设置 noPrimaryKey)
      firstName: t.string(),
      lastName: t.string(),
      username: t.string().unique(), // 唯一约束
      email: t.string().email().unique(), // 邮箱格式验证 + 唯一约束
      deletedAt: t.timestamp().nullable(), // 软删除时间戳(可为 null)
      subscriptionProvider: t.enum('paymentProvider', ['stripe', 'paypal']), // 枚举类型
      subscriptionId: t.uuid(),
      startDate: t.timestamp(),
      endDate: t.timestamp(),
      ...t.timestamps(), // 自动添加 createdAt/updatedAt 时间戳
    }),
    // 第二参数(可选):定义复合约束(主键、索引、外键等)
    (t) => [
      // 复合主键
      t.primaryKey(['firstName', 'lastName']),
      // 复合唯一索引
      t.unique(['subscriptionProvider', 'subscriptionId']),
      // 复合外键(指向 SubscriptionTable 的 provider/id 列)
      t.foreignKey(
        ['subscriptionProvider', 'subscriptionId'],
        () => SubscriptionTable,
        ['provider', 'id'],
      ),
      // Postgres 排除约束(时间范围不重叠)
      t.exclude([
        { expression: `tstzrange("startDate", "endDate")`, with: '&&' },
      ]),
      // 数据库级检查约束
      t.check(sql`username != email`), // 使用 sql 模板字面量
    ],
  );

  // 定义数据库端计算列(虚拟列)
  computed = this.setComputed({
    fullName: (q) => // q 是当前表的查询构建器上下文
      sql`${q.column('firstName')} || ' ' || ${q.column('lastName')}` // 拼接姓名
        .type((t) => t.string()), // 显式指定返回类型
  });

  // 定义查询作用域(可复用的查询条件)
  scopes = this.setScopes({
    default: (q) => q.where({ hidden: false }), // 默认作用域(自动应用)
    active: (q) => q.where({ active: true }), // 自定义作用域
  });

  // 定义关系(此处为一对多关系:用户拥有多篇文章)
  relations = {
    posts: this.hasMany(() => PostTable, {
      columns: ['id'], // 当前表关联列(用户 ID)
      references: ['authorId'], // 目标表关联列(文章作者 ID)
    }),
  };
}

重要说明

  • table 和 softDelete 必须标记为 readonly,确保 TypeScript 类型推导正确
  • 列配置详见列模式概述
  • 复合约束(主键 / 索引 / 外键等)文档见迁移列方法
  • 关系定义详见建模关系

生成迁移

定义或修改表结构后,通过命令生成并应用迁移:

# 生成迁移(自动命名)
pnpm db g

# 生成指定名称的迁移
pnpm db g create-user-table

# 生成后立即应用迁移
pnpm db g up

注意事项

  • 危险操作:此工具会删除代码未引用的数据库实体(模式、表等),仅适用于完全由应用管理的数据库
  • 重命名表 / 列时,工具会交互式询问是否保留数据(重命名列)或删除旧列(数据丢失)
  • 自动处理的数据库对象包括:表、列、模式、枚举、主键、外键、索引、检查约束、排除约束

Postgres 扩展

启用 Postgres 扩展(如 citext)需在配置中声明:

export const db = orchidORM(
  {
    databaseURL: process.env.DATABASE_URL,
    extensions: [
      'citext', // 最新版本
      { name: 'citext', version: '1.2.3' }, // 指定版本
      'mySchema.citext', // 特定模式下的扩展
    ],
    generatorIgnore: {
      tables: ['spatial_ref_sys'], // 忽略扩展自动创建的表(如 postgis 的系统表)
    },
  },
  { /* 表定义 */ },
);

表实用类型

OrchidORM 提供多种类型别名,用于不同操作场景的类型安全:

Selectable

数据库查询结果类型,包含所有列(包括计算列和 select(false) 列),应用列解析器(如时间戳转 Date):

export type User = Selectable<UserTable>; // 包含所有列的完整类型

DefaultSelect

默认查询结果类型,排除 select(false) 列和计算列,仅包含默认选中的列:

export type UserDefault = DefaultSelect<UserTable>; // 常规查询返回类型

Insertable

插入数据类型,允许的输入字段(列类型支持 encode 转换):

export type UserNew = Insertable<UserTable>; // 创建新记录时的参数类型

Updatable

更新数据类型,与 Insertable 相同,但所有字段可选:

export type UserUpdate = Updatable<UserTable>; // 更新记录时的参数类型

Queryable

查询条件类型,用于 where 等方法的参数,保持原始数据库类型(不应用解析 / 编码):

export type UserQueryable = Queryable<UserTable>; // 构建查询条件时的类型

独立查询构建器(createDb

若无需 ORM 功能,可单独使用底层查询构建器 pqb

import { createDb } from 'orchid-orm';

const db = createDb({
  databaseURL: process.env.DATABASE_URL,
  snakeCase: true, // 数据库列名为 snake_case 时启用
  schemaConfig: zodSchemaConfig, // 集成 Zod 验证(可选)
  columnTypes: (t) => ({
    timestamp: () => t.timestamp().asNumber(), // 自定义列类型
  }),
});

// 定义表结构
export const User = db('user', (t) => ({
  id: t.identity().primaryKey(),
  name: t.string(),
  age: t.integer().nullable(),
}));

// 类型安全查询
const users = await User.select('id', 'name').where({ age: { gte: 20 } });

关键配置选项

snakeCase

控制列名转换策略:

  • 默认:使用 camelCase(代码中)映射数据库列名
  • 启用snakeCase: true 自动将 camelCase 转换为 snake_case(数据库列名规范)
  • 覆盖:单个表可通过 snakeCase = true 单独启用,或通过 t.name('snake_case') 显式指定列名

softDelete

启用软删除(逻辑删除):

export class SomeTable extends BaseTable {
  readonly softDelete = true; // 使用默认列 deletedAt
  // 或自定义列名
  readonly softDelete = 'my_deleted_at';
}
  • 默认查询自动过滤已删除记录(deletedAt IS NULL
  • 强制查询所有记录:db.table.includeDeleted()
  • 物理删除:db.table.find(1).hardDelete()

scopes

定义可复用的查询条件(作用域):

scopes = this.setScopes({
  default: (q) => q.where({ hidden: false }), // 所有查询默认应用
  active: (q) => q.where({ active: true }), // 按需应用
});

// 使用方式
await db.table.scope('active'); // 应用 active 作用域
await db.table.unscope('default'); // 移除 default 作用域

原始 SQL 操作

$query

执行原始 SQL 查询(支持事务):

// 安全插值(反引号模板)
const result = await db.$query<{ one: number }>`SELECT ${1} AS one`;

// 手动构造 SQL 对象(适用于动态参数)
import { sql } from './baseTable';
const result = await db.$query(sql`SELECT $1 AS one`, [123]);

$from

基于子查询构建查询:

const subQuery = db.user.select('name', { count: (q) => q.related.count() });
const result = await db.$from(subQuery).where({ count: { gte: 5 } });

最佳实践

  • 主键约束:所有表应定义主键,无主键表需显式声明 noPrimaryKey = true
  • 外键自动生成:通过 createBaseTable({ autoForeignKeys: true }) 自动创建数据库级外键(需配合迁移工具)
  • 类型安全:始终通过 db.table 访问表类,避免直接使用表类(如 UserTable.findBy(...) 无效) 所有表文件需在 orchidORM 实例中注册(见设置部分),已有项目可通过 db pull 命令反向生成表代码和迁移。