Orchid ORM 与 Kysely 的比较

22 阅读19分钟

核心定位与设计差异

Kysely 本质上是一款查询构建器,与对象关系映射(ORM)工具存在明显区别。从设计理念出发,它并不承担处理数据关系以及其他 ORM 工具常见功能的职责。反观 OrchidORM,尽管两者定位不同,但将它们进行对比依然具备重要意义。这两个库均在复杂查询构建方面表现出色,并且都高度重视类型安全,保障代码在数据操作过程中的准确性与可靠性。

OrchidORM 的设计在一定程度上借鉴了 Kysely 的思路。让 OrchidORM 拥有与 Kysely 同等水平的灵活性与类型安全性,是 OrchidORM 项目的重要目标之一。然而,目前 OrchidORM 尚未完全覆盖 Kysely 的全部功能,例如 Kysely 支持的 case-when SQL 构建器,在 OrchidORM 中暂未实现。

在数据库支持范围上,两者也有显著差异。OrchidORM 仅适配 PostgreSQL 数据库,专注于挖掘该数据库的强大功能;而 Kysely 则展现出更广泛的兼容性,支持多种不同类型的数据库,为开发者提供了更多选择。

详细功能对比示例

数据查询操作

单列数据检索

在单列数据查询场景下,OrchidORM 具备独特的默认行为,它默认返回符合条件的多条记录,因此无需调用 execute() 方法执行查询操作。开发者可以使用 take 方法获取第一条记录(若记录不存在则抛出错误),或通过 takeOptional 方法获取第一条记录(记录不存在时返回 undefined),这两个方法分别对应 Kysely 中的 executeTakeFirstOrThrow 和 executeTakeFirst 功能。此外,OrchidORM 还提供了 get 方法用于精准获取单个值,pluck 方法用于提取单列数据并生成扁平数组,满足多样化的数据获取需求。

// OrchidORM
import { db } from './tables';

const persons = await db.person.select('id').where({ firstName: 'Arnold' });

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .select('id')
  .where('first_name', '=', 'Arnold')
  .execute();

多表关联列查询

当涉及从多个表中选取数据时,OrchidORM 有着严格的规范,明确禁止在未指定表名的情况下进行模糊的列选择操作,以此确保查询结果的准确性和唯一性。而 Kysely 在部分情况下允许不指定表名的模糊列选择,但这种操作方式可能会引发结果不确定性的风险,相比之下,OrchidORM 的设计更倾向于通过强制指定表名来保障查询逻辑的清晰性。

// OrchidORM
import { db } from './tables';

const persons = await db.$from([db.person, db.pet]).select('person.id');
// 模糊选择被禁止
// .select('id')

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom(['person', 'pet'])
  .select('person.id')
  // 模糊选择被允许
  // .select('id')
  .execute();

多列数据选取

在进行多列数据查询时,OrchidORM 和 Kysely 采用了不同的语法与操作模式。OrchidORM 可以直接在 select 方法中依次列出多个列名,简洁地完成多列选择;Kysely 则需要在 select 方法中明确指定每一列所属的表(若使用了表别名,必须正确引用),并通过调用 execute() 方法来触发查询执行,操作流程相对更为细致。

// OrchidORM
import { db } from './tables';

const persons = await db.person.select('id', 'firstName');

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .select(['person.id', 'first_name'])
  .execute();

列别名设置与使用

在列别名的设置与运用方面,两个库同样存在差异。OrchidORM 能够通过 as 方法为表设置别名,并在 select 操作中巧妙运用别名指定列,实现灵活的数据展示;Kysely 则是在 selectFrom 方法中为表赋予别名,并在 select 方法中借助 SQL 表达式,结合别名完成列的选择与别名效果的设置,两者在实现逻辑上各有特点。

// OrchidORM
import { db } from './tables';

const persons = await db.person.as('p').select({
  fn: 'firstName',
  ln: 'p.lastName',
});

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person as p')
  .select(['first_name as fn', 'p.last_name as ln'])
  .execute();

复杂数据筛选查询

面对复杂查询场景,例如涉及子查询、连接条件等操作时,q 通常作为 “query builder”(查询构建器)的标识,开发者也可根据习惯使用其他名称。在 OrchidORM 中,像 firstPetName 这类数据的查询,采用 LEFT JOIN LATERAL 方式实现,该方式不仅执行效率较高,还支持根据实际需求灵活切换为内连接操作,展现出强大的适应性。Kysely 同样能够实现复杂查询功能,通过合理构建子查询、灵活运用表达式构建器等手段,达成与 OrchidORM 相似的复杂数据筛选与获取效果,满足开发者多样化的业务需求。

// OrchidORM
import { db } from './tables';

const persons = await db.person.select({
  // Select a correlated subquery
  firstPetName: (q) =>
    q.pets
      .getOptional('pets.name')
      .order('pets.name'),

  // The same as above,
  // but referencing the join condition explicitly
  firstPetName2: (q) =>
    db.pet
      .getOptional('pet.name')
      .where({ ownerId: q.ref('person.id') })
      .order('pet.name'),

  // Build and select an expression using
  // the expression builder
  isJenniferOrArnold: (q) =>
    q.ref('firstName').equals('Jennifer').or(
      q.ref('firstName').equals('Arnold')
    ),

  // Select a raw sql expression
  fullName: (q) =>
    q.sql<string>`concat(first_name, ' ', last_name)`,
})

import { db } from './tables';
import { sql } from 'kysely';

const persons = await db.selectFrom('person')
  .select(({ eb, selectFrom, or }) => [
    // Select a correlated subquery
    selectFrom('pet')
      .whereRef('person.id', '=', 'pet.owner_id')
      .select('pet.name')
      .orderBy('pet.name')
      .limit(1)
      .as('first_pet_name'),

    // Build and select an expression using
    // the expression builder
    or([
      eb('first_name', '=', 'Jennifer'),
      eb('first_name', '=', 'Arnold')
    ]).as('is_jennifer_or_arnold'),

    // Select a raw sql expression
    sql<string>`concat(first_name, ' ', last_name)`.as('full_name')
  ])
  .execute()

非空数据查询

OrchidORM 的 take 方法与 Kysely 的 executeTakeFirstOrThrow 功能类似,但在 OrchidORM 中,take 方法还可应用于嵌套选择场景。在实际操作中,开发者可以利用这一特性,结合 where 条件筛选出非空数据,并通过 narrowType 方法移除结果中的 null 类型,确保获取到的数据准确有效。而 Kysely 则通过构建复杂的 select 语句,使用 $notNull() 等方法实现类似的非空数据查询与类型处理逻辑。

// OrchidORM
import { db } from './tables';

const persons = await db.person
  .select('lastName', {
    // `take()` 如果找不到 pet 会抛出异常
    pet: (q) => q.pets.take(),
  })
  .where({ lastName: { not: null } })
  // 使用 `narrowType` 移除结果中的 null 类型
  .narrowType()<{ lastName: string }>();

// Kysely
import { db } from './tables';
import { NotNull } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';

const persons = await db
  .selectFrom('person')
  .select((eb) => [
    'last_name',
    jsonObjectFrom(
      eb
        .selectFrom('pet')
        .selectAll('pet')
        .limit(1)
        .whereRef('person.id', '=', 'pet.owner_id'),
    )
      .$notNull()
      .as('pet'),
  ])
  .where('last_name', 'is not', null)
  .$narrowType<{ last_name: NotNull }>()
  .execute();

函数调用查询

在函数调用查询方面,OrchidORM 和 Kysely 存在细微差别。Kysely 采用 .type((t) => t.string()) 的方式对查询进行类型包裹,以便使用字符串专用的 where 过滤条件;而 OrchidORM 则通过 fn 方法调用函数,并指定相应的参数和返回类型,实现对数据的计算与筛选。例如,在计算宠物数量、拼接完整姓名、聚合宠物名字等操作中,两者均能通过函数调用完成复杂的数据处理任务,但具体的语法和操作方式有所不同。

// Orchid ORM
import { db } from './tables';

const result = await db.person
  .join('pets') // join is equal to inner join
  .select('person.id', {
    petCount: (q) => q.count('pets.*'),

    fullNameWithTitle: (q) =>
      q.fn('concat', [
        q.val('Ms. '),
        'firstName',
        q.val(' '),
        'lastName',
      ]).type((t) => t.string()),

    petNames: (q) =>
      q.fn('array_agg', ['pets.name']).type((t) => t.array(t.string())),

    fullName: (q) =>
      q.sql<string>`concat(
        ${q.ref('firstName')},
        ' ',
        ${q.ref('lastName')}
      )`,
  })
  .group('id')
  .having((q) => q.count('pets.id').gt(10))
  
//Kysely
import { db } from './tables';
import { sql } from 'kysely'

const result = await db.selectFrom('person')
  .innerJoin('pet', 'pet.owner_id', 'person.id')
  .select(({ fn, val, ref }) => [
    'person.id',

    fn.count<number>('pet.id').as('pet_count'),

    fn<string>('concat', [
      val('Ms. '),
      'first_name',
      val(' '),
      'last_name'
    ]).as('full_name_with_title'),

    fn.agg<string[]>('array_agg', ['pet.name']).as('pet_names'),

    sql<string>`concat(
      ${ref('first_name')},
      ' ',
      ${ref('last_name')}
    )`.as('full_name')
  ])
  .groupBy('person.id')
  .having((eb) => eb.fn.count('pet.id'), '>', 10)
  .execute()


去重查询

在去重查询操作上,OrchidORM 和 Kysely 的实现方式较为相似。OrchidORM 只需调用 distinct() 方法,并在 select 中指定要去重的列,即可轻松实现数据去重;Kysely 则是在 selectFrom 构建查询后,调用 distinct() 方法,并通过 execute() 执行查询,获取去重后的结果,两者都为开发者提供了便捷的去重查询功能。

// OrchidORM
import { db } from './tables';

const persons = await db.person.distinct().select('firstName');

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .select('first_name')
  .distinct()
  .execute();

全列查询

在全列查询方面,OrchidORM 出于数据安全性和查询合理性的考量,不支持隐式选择所有表的所有字段,因为这种操作方式可能会带来性能损耗和数据冗余问题。而 Kysely 提供了 selectAll() 方法,方便开发者快速选择表中的所有列,适用于一些对数据完整性要求较高且性能影响在可接受范围内的场景。

// OrchidORM
import { db } from './tables';

// 不支持。
// 隐式选择所有表的所有字段不是一个好主意。

// Kysely
import { db } from './tables';

const persons = await db.selectFrom('person').selectAll().execute();

表全列查询

当需要查询表中的所有列时,OrchidORM 可以直接引用表对象,简洁地获取表的全部数据;Kysely 则需要在 selectFrom 方法中指定表名,并调用 selectAll('person') 方法,明确告知查询构建器要选择指定表的所有列,两者在实现方式上各有侧重。

// OrchidORM
import { db } from './tables';

const persons = await db.person;

// Kysely
import { db } from './tables';

const persons = await db.selectFrom('person').selectAll('person').execute();

嵌套数组查询

在处理嵌套数组查询时,OrchidORM 由于底层采用 JOIN LATERAL 技术,开发者可以灵活地将 q.pets 改为 q.join().pets,以此过滤掉没有宠物的结果,精准获取所需数据。Kysely 则通过 jsonArrayFrom 方法,将子查询结果转换为 JSON 数组形式,并在 select 中进行合理配置,实现类似的嵌套数组查询效果,满足复杂数据结构的查询需求。

// OrchidORM
import { db } from './tables';

const result = await db.person.select('id', {
  pets: (q) =>
    q.pets.select('name', {
      petId: 'id',
    }),
});

// Kysely
import { db } from './tables';
import { jsonArrayFrom } from 'kysely/helpers/postgres';

const result = await db
  .selectFrom('person')
  .select((eb) => [
    'id',
    jsonArrayFrom(
      eb
        .selectFrom('pet')
        .select(['pet.id as pet_id', 'pet.name'])
        .whereRef('pet.owner_id', '=', 'person.id')
        .orderBy('pet.name'),
    ).as('pets'),
  ])
  .execute();

嵌套对象查询

对于嵌套对象查询,OrchidORM 和 Kysely 同样提供了有效的解决方案。在 OrchidORM 中,开发者可以通过 select 方法结合子查询,筛选出符合条件的嵌套对象数据,并使用 takeOptional 等方法处理可选结果;Kysely 则借助 jsonObjectFrom 方法,将子查询结果转换为 JSON 对象,并在 select 中进行精确设置,两者都能准确地处理嵌套对象数据的查询与筛选。

// OrchidORM
import { db } from './tables';

const result = await db.person.select('id', {
  favoritePet: (q) =>
    q.pets
      .select('name', { petId: 'id' })
      .where({ isFavorite: true })
      .takeOptional(),
});

// Kysely
import { db } from './tables';
import { jsonObjectFrom } from 'kysely/helpers/postgres';

const result = await db
  .selectFrom('person')
  .select((eb) => [
    'id',
    jsonObjectFrom(
      eb
        .selectFrom('pet')
        .select(['pet.id as pet_id', 'pet.name'])
        .whereRef('pet.owner_id', '=', 'person.id')
        .where('pet.is_favorite', '=', true),
    ).as('favorite_pet'),
  ])
  .execute();

条件查询操作

基础条件过滤

在基础条件查询过程中,OrchidORM 针对不同的数据列类型,精心设计了相应的 where 辅助函数,确保开发者在设置查询条件时的准确性和规范性。例如,如果尝试在非数字列上使用 gt(大于)操作,TypeScript 会立即报错提示,避免因条件设置错误导致查询结果异常。而 Kysely 则通过 where 方法直接设置查询条件,并调用 executeTakeFirst() 等方法执行查询,获取符合条件的第一条记录,两者在基础条件查询的实现上各有特色。

// OrchidORM
import { db } from './tables';

const person = await db.person
  .where({
    firstName: 'Jennifer',
    age: { gt: 40 },
  })
  .takeOptional();

// Kysely
import { db } from './tables';

const person = await db
  .selectFrom('person')
  .selectAll()
  .where('first_name', '=', 'Jennifer')
  .where('age', '>', 40)
  .executeTakeFirst();

IN 条件查询

OrchidORM 和 Kysely 均对 IN 条件查询提供了良好的支持,并且都允许使用子查询或原始查询结果作为 IN 操作的值。这一特性使得开发者能够根据实际业务需求,灵活地构建复杂的 IN 条件查询,实现精准的数据筛选。

// OrchidORM
import { db } from './tables';

const persons = await db.person.whereIn('id', [1, 2, 3]);

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .selectAll()
  .where('id', 'in', [1, 2, 3])
  .execute();

多列 IN 条件查询

在多列 IN 条件查询场景下,两个库同样表现出色。OrchidORM 通过 whereIn 方法,传入包含多列的数组和对应的值数组,轻松实现多列 IN 条件查询;Kysely 则需要构建更为复杂的 where 条件,借助 refTuple(引用元组)等方法,准确地设置多列 IN 条件,确保查询结果符合预期。

// OrchidORM
import { db } from './tables';

const persons = await db.person.whereIn(
  ['firstName', 'lastName'],
  [
    ['Jennifer', 'Aniston'],
    ['Arnold', 'Schwarzenegger'],
  ],
);

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .selectAll()
  .where(({ eb, refTuple, tuple }) =>
    eb(refTuple('first_name', 'last_name'), 'in', [
      tuple('Jennifer', 'Aniston'),
      tuple('Arnold', 'Schwarzenegger'),
    ]),
  )
  .execute();

对象条件过滤

在对象条件过滤方面,OrchidORM 允许开发者在 where 方法中以对象形式设置多个条件,并且支持使用子查询动态生成条件值,实现灵活的数据筛选。Kysely 则通过 where 方法结合 and 操作,在回调函数中设置多个条件,同样能够满足对象条件过滤的需求,两者在实现逻辑上有所不同,但都能有效完成数据筛选任务。

// OrchidORM
import { db } from './tables';

const persons = await db.person.where({
  firstName: 'Jennifer',
  lastName: (q) => q.ref('firstName'),
});

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .selectAll()
  .where((eb) =>
    eb.and({
      first_name: 'Jennifer',
      last_name: eb.ref('first_name'),
    }),
  )
  .execute();

逻辑或条件查询

OrchidORM 提供了 whereOneOf 和 orWhere 方法,分别表示 “并且其中之一为真” 和 “或者其中之一为真” 的逻辑关系。开发者可以通过链式调用这些方法,轻松构建复杂的逻辑或条件查询。Kysely 则通过 or 方法在回调函数中灵活组合多个条件,实现类似的逻辑或查询功能,为开发者提供了多样化的条件组合方式。

// OrchidORM
import { db } from './tables';

const persons = await db.person
  // 1. 使用 `whereOneOf` 方法
  .whereOneOf({ firstName: 'Jennifer' }, { firstName: 'Sylvester' })
  // 2. 链式调用 `orWhere`
  .where((q) =>
    q.where({ lastName: 'Aniston' }).orWhere({ lastName: 'Stallone' }),
  );

// Kysely
import { db } from './tables';

const persons = await db
  .selectFrom('person')
  .selectAll()
  // 1. 使用 `or` 方法
  .where((eb) =>
    eb.or([
      eb('first_name', '=', 'Jennifer'),
      eb('first_name', '=', 'Sylvester'),
    ]),
  )
  // 2. 链式调用 `or`
  .where((eb) =>
    eb('last_name', '=', 'Aniston').or('last_name', '=', 'Stallone'),
  )
  .execute();

条件动态查询

在条件动态查询场景下,常见的需求是根据可选参数动态过滤查询结果。OrchidORM 在这方面表现出极大的便利性,它会自动忽略值为 undefined 的参数,开发者无需额外编写复杂的条件判断逻辑,即可保持代码的简洁性和可读性。而 Kysely 则需要开发者手动编写 if 语句,根据参数是否存在动态构建查询条件,相比之下,OrchidORM 的设计更贴合实际开发需求,减少了开发者的代码编写量。

// OrchidORM
import { db } from './tables';

const firstName: string | undefined = 'Jennifer';
const lastName: string | undefined = 'Aniston';
const under18 = true;
const over60 = true;

const persons = await db.person
  .where({
    // undefined 会被忽略
    firstName,
    lastName,
  })
  .whereOneOf(
    ...[under18 && { age: { lt: 60 } }, over60 && { age: { gt: 60 } }].filter(
      Boolean,
    ),
  );

// Kysely
import { db } from './tables';

const firstName: string | undefined = 'Jennifer';
const lastName: string | undefined = 'Aniston';
const under18 = true;
const over60 = true;

let query = db.selectFrom('person').selectAll();

if (firstName) {
  query = query.where('first_name', '=', firstName);
}

if (lastName) {
  query = query.where('last_name', '=', lastName);
}

if (under18 || over60) {
  query = query.where((eb) =>
    eb.or(
      [under18 && eb('age', '<', 18), over60 && eb('age', '>', 60)].filter(
        Boolean,
      ),
    ),
  );
}

const persons = await query.execute();

复杂条件组合查询

对于复杂条件组合查询,OrchidORM 的 whereExists 和 whereNotExists 方法支持接受任意子查询作为条件,开发者可以利用这一特性构建复杂的嵌套条件,实现精准的数据筛选。Kysely 则通过在 where 方法中灵活运用 andornot 等逻辑操作符,结合子查询和表达式构建器,同样能够实现复杂条件组合查询,满足各种复杂业务场景的需求。

// OrchidORM
import { db } from './tables';

const firstName = 'Jennifer';
const maxAge = 60;

const persons = await db.person
  .whereOneOf({ firstName }, { age: { lt: maxAge } })
  .whereNotExists('pets');

// Kysely
import { db } from './tables';

const firstName = 'Jennifer';
const maxAge = 60;

const persons = await db
  .selectFrom('person')
  .selectAll('person')
  .where(({ eb, or, and, not, exists, selectFrom }) =>
    and([
      or([eb('first_name', '=', firstName), eb('age', '<', maxAge)]),
      not(
        exists(
          selectFrom('pet')
            .select('pet.id')
            .whereRef('pet.owner_id', '=', 'person.id'),
        ),
      ),
    ]),
  )
  .execute();

连接查询操作

简单内连接查询

在简单内连接查询时,当连接多个表并选择 id 列数据,OrchidORM 与 Kysely 存在明显差异。OrchidORM 会自动为列名添加表名前缀,避免因列名重复导致的歧义问题,确保查询结果的准确性;而 Kysely 则不会自动添加表名前缀,如果开发者不手动指定,可能会选择到错误表的 id 列,相比之下,OrchidORM 的设计更能保障查询的可靠性。

// OrchidORM
import { db } from './tables';

const result = await db.person
  .join('pets')
  // id 解析为 person.id,无歧义
  .select('id', { petName: 'pets.name' });

// Kysely
import { db } from './tables';

const result = await db
  .selectFrom('person')
  .innerJoin('pet', 'pet.owner_id', 'person.id')
  .select(['person.id', 'pet.name as pet_name'])
  // 这会选择 pet 的 id
  // .select(['id'])
  .execute();

别名内连接查询

在使用别名进行内连接查询时,OrchidORM 和 Kysely 的操作方式和结果呈现也有所不同。OrchidORM 通过 join((q) => q.pets.as('p')) 为连接的表设置别名,并在后续的 where 条件中使用别名进行筛选,查询结果中的 id 明确指向 person 表的 id 列;Kysely 则在 innerJoin('pet as p', 'p.owner_id', 'person.id') 中设置别名,若不仔细处理,查询结果中的 id 可能会被误认为是 pet 表的 id 列,需要开发者更加谨慎地编写查询语句。

// OrchidORM
import { db } from './tables';

const result = await db.person
  .join((q) => q.pets.as('p'))
  .where({ 'p.name': 'Doggo' });

result[0]?.id; // 这是 person 的 id

// Kysely
import { db } from './tables';

const result = await db
  .selectFrom('person')
  .innerJoin('pet as p', 'p.owner_id', 'person.id')
  .where('p.name', '=', 'Doggo')
  .selectAll()
  .execute();

result[0]?.id; // 这是 pet 的 id

复杂连接查询

面对复杂连接查询场景,OrchidORM 和 Kysely 的处理方式各有千秋。OrchidORM 在连接多个表时,通过在 join 方法的回调函数中设置详细的条件,能够精准地筛选出符合要求的数据,并且只选择主表(如 persons)的数据,避免数据冗余;Kysely 则在 innerJoin 方法中通过 onRefon 等方法设置连接条件,可能会返回主表与关联表合并后的数据,存在列冲突的潜在风险,开发者需要根据实际需求进行数据处理和筛选。

// OrchidORM
import { db } from './tables';

// 只选择 persons
const result = await db.person.join('pets', (q) =>
  q
    .where({ name: 'Doggo' })
    .whereOneOf({ 'person.age': { gt: 10 } }, { 'person.age': { lt: 10 } }),
);

// Kysely
import { db } from './tables';

// 选择 persons 与 pets 合并,可能有列冲突
const result = await db
  .selectFrom('person')
  .innerJoin('pet', (join) =>
    join
      .onRef('pet.owner_id', '=', 'person.id')
      .on('pet.name', '=', 'Doggo')
      .on((eb) =>
        eb.or([eb('person.age', '>', 18), eb('person.age', '<', 100)]),
      ),
  )
  .selectAll()
  .execute();

子查询连接查询

OrchidORM 在子查询连接查询方面具有独特的设计理念,它倾向于在 select 操作中使用隐式连接,同时也全面支持 joinleftJoinfullJoinjoinLateral 等多种连接方式,并且可以灵活地使用简单值、关系、子查询、CTE 表作为连接条件,展现出强大的兼容性和灵活性。Kysely 同样能够实现子查询连接查询功能,通过在 innerJoin 方法中合理构建子查询,并设置准确的连接条件,获取所需的查询结果,满足不同业务场景下的复杂数据连接需求。

// OrchidORM
import { db } from './tables';

const result = await db.person
  .join(
    db.pet
      .as('doggo')
      .select('name', { owner: 'ownerId' })
      .where({ name: 'Doggo' }),
    (q) => q.on('doggo.owner', 'person.id'),
  )
  // 'doggo' 会被选择为嵌套对象,避免 person 列与 pet 列冲突
  .select('id', 'doggo.*');

// Kysely
import { db } from './tables';

const result = await db
  .selectFrom('person')
  .innerJoin(
    (eb) =>
      eb
        .selectFrom('pet')
        .select(['owner_id as owner', 'name'])
        .where('name', '=', 'Doggo')
        .as('doggos'),
    (join) => join.onRef('doggos.owner', '=', 'person.id'),
  )
  .selectAll('doggos')
  .execute();

数据插入操作

单行数据插入

在单行数据插入操作上,OrchidORM 和 Kysely 的行为和返回结果有所不同。OrchidORM 的 insert 方法默认返回插入的记录数量,而 create 方法默认返回完整的插入记录,开发者可以根据具体需求选择合适的方法。例如,使用 get 方法结合 insert 操作,可以获取插入记录的单个值(如自增主键 id)。Kysely 则通过 insertInto 方法插入数据,并使用 returning 方法明确指定要返回的列(如 id),最后调用 executeTakeFirst() 方法执行插入操作并获取结果,操作流程相对较为固定。

// OrchidORM
import { db } from './tables';

// `get` 返回单个值
const id = await db.person.get('id').insert({
  firstName: 'Jennifer',
  lastName: 'Aniston',
  age: 40,
});

// Kysely
import { db } from './tables';

const result = await db
  .insertInto('person')
  .values({
    first_name: 'Jennifer',
    last_name: 'Aniston',
    age: 40,
  })
  .returning(['id'])
  .executeTakeFirst();

多行数据插入

对于多行数据插入,OrchidORM 的 insertMany 方法默认返回插入的记录总数,createMany 方法默认返回插入记录组成的数组,开发者还可以结合 pluck 方法获取插入记录中指定列的值组成的扁平数组。Kysely 则在 insertInto 方法中传入包含多个对象的数组,一次性插入多行数据,并通过调用 execute() 方法执行插入操作,实现高效的数据批量插入。

// OrchidORM
import { db } from './tables';

// `pluck` 返回值的扁平数组
const ids = await db.person.pluck('id').insertMany([
  {
    firstName: 'Jennifer',
    lastName: 'Aniston',
    age: 40,
  },
  {
    firstName: 'Arnold',
    lastName: 'Schwarzenegger',
    age: 70,
  },
]);

// Kysely
import { db } from './tables';

await db
  .insertInto('person')
  .values([
    {
      first_name: 'Jennifer',
      last_name: 'Aniston',
      age: 40,
    },
    {
      first_name: 'Arnold',
      last_name: 'Schwarzenegger',
      age: 70,
    },
  ])
  .execute();

插入返回数据

在插入数据并返回特定结果方面,OrchidORM 的 select 方法在功能上等同于 Kysely 的 returning 方法,并且在 OrchidORM 中,select 方法可以灵活地放置在插入操作之前或之后。开发者可以使用 select 方法指定要返回的列,实现插入数据的同时获取相关结果;而 Kysely 则需要在 insertInto 方法后使用 returning 方法明确指定返回的列,两者都为开发者提供了插入数据并获取反馈结果的能力。

// OrchidORM
import { db } from './tables';

const result = await db.person.select('id', { name: 'firstName' }).insert({
  firstName: 'Jennifer',
  lastName: 'Aniston',
  age: 40,
});

// Kysely
import { db } from './tables';

const result = await db
  .insertInto('person')
  .values({
    first_name: 'Jennifer',
    last_name: 'Aniston',
    age: 40,
  })
  .returning(['id', 'first_name as name'])
  .executeTakeFirstOrThrow();

复杂值插入

OrchidORM 和 Kysely 都支持在插入和更新操作中使用回调函数、子查询、原始 SQL 表达式等复杂值。在 OrchidORM 中,开发者可以在 create 方法中通过回调函数调用 sql 方法生成原始 SQL 表达式,或引用其他列的值,还可以使用聚合函数计算插入值;在 Kysely 中,开发者通过在 values 回调函数中使用 sql 方法和表达式构建器,同样能够实现复杂值的插入操作,满足各种复杂业务逻辑下的数据插入需求。


// OrchidORM
import { db } from './tables';

const ani = "Ani"
const ston = "ston"

const result = await db.person.create({
  firstName: 'Jennifer',
  lastName: (q) => q.sql<string>`concat(${ani}, ${ston})`,
  middleName: (q) => q.ref('firstName'),
  age: db.person.avg('age'),
})

// Kysely
import { db } from './tables';
import { sql } from 'kysely'

const ani = "Ani"
const ston = "ston"

const result = await db
  .insertInto('person')
  .values(({ ref, selectFrom, fn }) => ({
    first_name: 'Jennifer',
    last_name: sql<string>`>concat(${ani}, ${ston})`,
    middle_name: ref('first_name'),
    age: selectFrom('person')
      .select(fn.avg<number>('age').as('avg_age')),
  }))
  .executeTakeFirst()

子查询插入

在子查询插入操作上,Kysely 提供了 lit 方法用于插入未参数化的值,而 OrchidORM 没有与之类似的方法。不过,OrchidORM 可以通过 insertManyFrom 方法,结合子查询从其他表中选取数据并插入到目标表中;Kysely 则在 insertInto 方法中使用 expression 回调函数,通过子查询和 lit 方法实现更灵活的数据插入操作,两者在子查询插入的实现方式上存在差异。

// OrchidORM
import { db } from './tables';

const count = await db.person.insertManyFrom(
  db.pet.select({
    firstName: 'pet.name',
    lastName: (q) => q.val('Petson'),
    age: (q) => q.val(7),
  }),
);

// Kysely
import { db } from './tables';

const result = await db
  .insertInto('person')
  .columns(['first_name', 'last_name', 'age'])
  .expression((eb) =>
    eb
      .selectFrom('pet')
      .select((eb) => [
        'pet.name',
        eb.val('Petson').as('last_name'),
        eb.lit(7).as('age'),
      ]),
  )
  .execute();

数据更新操作

单行数据更新

在单行数据更新操作中,OrchidORM 的 update 方法默认返回更新的记录数量,并且支持开发者使用 selectget 等方法从更新操作中选择特定数据。例如,通过 find(1) 定位到特定记录后,调用 update 方法修改其字段值;Kysely 则通过 updateTable 方法设置要更新的表,使用 set 方法指定更新的字段和值,通过 where 方法设置更新条件,最后调用 executeTakeFirst() 方法执行更新操作并获取结果,实现对单行数据的精准更新。

// OrchidORM
import { db } from './tables';

const count = await db.person.find(1).update({
  firstName: 'Jennifer',
  lastName: 'Aniston',
});

// Kysely
import { db } from './tables';

const result = await db
  .updateTable('person')
  .set({
    first_name: 'Jennifer',
    last_name: 'Aniston',
  })
  .where('id', '=', 1)
  .executeTakeFirst();

复杂值更新

在复杂值更新场景下,OrchidORM 的 increment 和 decrement 方法在 SQL 层面与 Kysely 的 eb('age', '+', 1) 类似,都用于实现数值型字段的递增或递减操作。在 OrchidORM 中,开发者可以在 update 操作中结合 increment 方法和其他字段更新操作,实现复杂的数据更新逻辑;在 Kysely 中,通过在 set 回调函数中使用表达式构建器,灵活地组合各种更新操作,满足复杂业务场景下

// OrchidORM
import { db } from './tables';

const count = await db.person
  .find(1)
  .increment('age')
  .update({
    firstName: db.pet.get('name'),
    lastName: 'updated',
  });

// Kysely
import { db } from './tables';

const result = await db
  .updateTable('person')
  .set((eb) => ({
    age: eb('age', '+', 1),
    first_name: eb.selectFrom('pet').select('name').limit(1),
    last_name: 'updated',
  }))
  .where('id', '=', 1)
  .executeTakeFirst();

删除操作

单行删除

在执行单行数据删除时,OrchidORM 和 Kysely 都提供了便捷的操作方式。OrchidORM 的 delete 方法默认返回删除的记录数量,开发者能够结合 selectget 等方法,在删除数据的同时筛选并获取相关信息。例如,通过 find(1) 精准定位到某条记录后,调用 delete 方法即可将其从数据库中移除 。而 Kysely 则通过 deleteFrom 方法指定要删除数据的表,利用 where 方法设置删除条件(如 where('person.id', '=', 1)),最后调用 executeTakeFirst() 方法执行删除操作并获取结果,整个流程清晰且易于理解。

// OrchidORM
import { db } from './tables';

const count = await db.person.find(1).delete();

// Kysely
import { db } from './tables';

const result = await db
  .deleteFrom('person')
  .where('person.id', '=', 1)
  .executeTakeFirst();

事务处理

简单事务

在事务管理方面,忘记使用 trx 对象(事务对象)而误用普通数据库连接对象是开发者常见的错误。OrchidORM 为解决这一问题,引入了 AsyncLocalStorage 机制,能够自动为事务范围内的所有查询匹配正确的事务实例,极大地降低了出错概率。在实际应用中,相关记录可以以嵌套方式创建,此时事务会自动触发,确保数据操作的原子性。例如,在创建宠物与其主人记录时,通过 $transaction 方法开启事务,先创建主人记录获取 ownerId,再基于此创建宠物记录,若其中任何一步失败,整个事务将回滚 。

反观 Kysely,则通过 transaction 方法开启事务,并在其回调函数中使用 trx 对象执行具体的数据库操作。如先在事务内插入主人记录并获取 id,再利用该 id 插入对应的宠物记录,最后通过 execute 方法提交事务,若出现异常则可通过捕获错误实现事务回滚,保障数据的一致性和完整性 。

// OrchidORM
import { db } from './tables';

const catto = await db.$transaction(async () => {
  const ownerId = await db.person.get('id').create({
    firstName: 'Jennifer',
    lastName: 'Aniston',
    age: 40,
  });

  return await db.pet.create({
    ownerId,
    name: 'Catto',
    species: 'cat',
    isFavorite: false,
  });
});

const catto2 = await db.pet.create({
  owner: {
    create: {
      firstName: 'Jennifer',
      lastName: 'Aniston',
      age: 40,
    },
  },
  name: 'Catto',
  species: 'cat',
  isFavorite: false,
});

// Kysely
import { db } from './tables';

const catto = await db.transaction().execute(async (trx) => {
  const jennifer = await trx
    .insertInto('person')
    .values({
      first_name: 'Jennifer',
      last_name: 'Aniston',
      age: 40,
    })
    .returning('id')
    .executeTakeFirstOrThrow();

  return await trx
    .insertInto('pet')
    .values({
      owner_id: jennifer.id,
      name: 'Catto',
      species: 'cat',
      is_favorite: false,
    })
    .returningAll()
    .executeTakeFirst();
});

CTE(公用表表达式)

简单选择

OrchidORM 和 Kysely 均支持递归 CTE 操作,在简单 CTE 查询场景下,两者展现出不同的实现方式。OrchidORM 通过 $queryBuilder 结合 with 方法定义 CTE,首先定义名为 jennifers 的 CTE,筛选出名字为 Jennifer 的人员记录并选取 id 和 age 列;接着定义 adult_jennifers CTE,基于 jennifers 进一步筛选出年龄大于 18 岁的记录;最后从 adult_jennifers 中查询年龄小于 60 岁的数据 。

Kysely 同样通过 with 方法定义 CTE,先创建 jennifers CTE 进行初步筛选,再构建 adult_jennifers CTE 进行二次过滤,最后在 selectFrom 方法中引用已定义的 CTE,并通过 where 条件筛选数据,调用 execute() 方法执行查询获取最终结果。两者虽然语法略有不同,但都能高效实现基于 CTE 的数据查询与处理 。

// OrchidORM
import { db } from './tables';

const result = await db.$queryBuilder
  .with(
    'jennifers',
    db.person.where({ firstName: 'Jennifer' }).select('id', 'age'),
  )
  .with('adult_jennifers', (q) =>
    q.from('jennifers').where({ age: { gt: 18 } }),
  )
  .from('adult_jennifers')
  .where({ age: { lt: 60 } });

// Kysely
import { db } from './tables';

const result = await db
  .with('jennifers', (db) =>
    db
      .selectFrom('person')
      .where('first_name', '=', 'Jennifer')
      .select(['id', 'age']),
  )
  .with('adult_jennifers', (db) =>
    db.selectFrom('jennifers').where('age', '>', 18).select(['id', 'age']),
  )
  .selectFrom('adult_jennifers')
  .where('age', '<', 60)
  .selectAll()
  .execute();