Skip to content

延迟加载 (Lazy Loading)

IBest ORM 提供两种关联数据加载策略:默认的显式加载和真正的延迟加载。

加载策略对比

策略方法行为适用场景
显式加载(默认)with()关联属性为 undefined,需手动指定加载大多数场景
真正延迟加载lazy()访问关联属性时自动查询数据库不确定是否需要关联数据

默认行为:显式加载

在 IBest ORM 中,关联数据默认不会自动加载。您需要使用 with() 方法显式指定要加载的关联。

typescript
// 不加载关联数据
const user = orm.query(User)
  .where({ id: 1 })
  .first();

console.log(user?.orders); // undefined - 关联未加载

// 使用 with() 显式加载关联数据
const userWithOrders = orm.query(User)
  .with('orders')
  .where({ id: 1 })
  .first();

console.log(userWithOrders?.orders); // Order[] - 关联已加载

真正延迟加载:lazy() 方法

使用 lazy() 方法可以启用真正的延迟加载。启用后,当您访问关联属性时,ORM 会自动执行数据库查询获取关联数据。

typescript
// 启用延迟加载
const user = orm.query(User)
  .lazy()
  .where({ id: 1 })
  .first();

// 访问 orders 属性时自动查询数据库
console.log(user?.orders); // Order[] - 自动加载
console.log(user?.profile); // Profile - 自动加载

lazy() 的工作原理

lazy() 方法会为实体的关联属性创建 getter 代理:

  1. 首次访问关联属性时,自动执行 SQL 查询
  2. 查询结果会被缓存,后续访问不会重复查询
  3. 支持 HasOne、HasMany、BelongsTo 关联(ManyToMany 暂不支持)
typescript
const user = orm.query(User).lazy().where({ id: 1 }).first();

// 第一次访问:执行 SQL 查询
const orders1 = user?.orders;  // SELECT * FROM order WHERE user_id = ?

// 第二次访问:使用缓存,不再查询
const orders2 = user?.orders;  // 直接返回缓存结果

使用场景

typescript
// 场景1:不确定是否需要关联数据
function getUserInfo(userId: number, includeOrders: boolean) {
  const user = orm.query(User).lazy().where({ id: userId }).first();

  const result: Record<string, unknown> = { name: user?.name };

  // 只有需要时才会触发查询
  if (includeOrders) {
    result.orders = user?.orders;  // 此时才查询订单
  }

  return result;
}

// 场景2:条件性访问关联
const user = orm.query(User).lazy().where({ id: 1 }).first();
if (user?.name === 'VIP') {
  // 只有 VIP 用户才加载订单
  console.log(user.orders);
}

注意

lazy() 可能导致 N+1 查询问题。在批量查询场景中,建议使用 with() 预加载。

关联定义

HasOne 关联

一对一关联,返回单个对象或 undefined。

typescript
import { Table, Column, PrimaryKey, HasOne } from '@ibestservices/ibest-orm';

@Table()
class User {
  @PrimaryKey()
  id?: number;

  @Column()
  name!: string;

  @HasOne(() => UserProfile, { foreignKey: 'user_id' })
  profile?: UserProfile;
}

// 使用
const user = orm.query(User)
  .with('profile')
  .where({ id: 1 })
  .first();

console.log(user?.profile?.bio);

HasMany 关联

一对多关联,返回数组。

typescript
@Table()
class User {
  @PrimaryKey()
  id?: number;

  @Column()
  name!: string;

  @HasMany(() => Order, { foreignKey: 'user_id' })
  orders?: Order[];
}

// 使用
const user = orm.query(User)
  .with('orders')
  .where({ id: 1 })
  .first();

console.log(user?.orders?.length);

BelongsTo 关联

多对一关联,返回单个对象或 undefined。

typescript
@Table()
class Order {
  @PrimaryKey()
  id?: number;

  @Column({ name: 'user_id', type: ColumnType.INTEGER })
  userId?: number;

  @Column()
  productName!: string;

  @BelongsTo(() => User, { foreignKey: 'userId' })
  user?: User;
}

// 使用
const order = orm.query(Order)
  .with('user')
  .where({ id: 1 })
  .first();

console.log(order?.user?.name);

ManyToMany 关联

多对多关联,返回数组。

typescript
@Table()
class User {
  @PrimaryKey()
  id?: number;

  @Column()
  name!: string;

  @ManyToMany(() => Role, {
    through: 'user_roles',
    throughForeignKey: 'user_id',
    throughOtherKey: 'role_id'
  })
  roles?: Role[];
}

// 使用
const user = orm.query(User)
  .with('roles')
  .where({ id: 1 })
  .first();

console.log(user?.roles?.map(r => r.name));

按需加载示例

typescript
function getUserBasicInfo(userId: number) {
  // 只查询用户基本信息,不加载关联
  return orm.query(User)
    .where({ id: userId })
    .first();
}

function getUserWithOrders(userId: number) {
  // 需要订单时才加载
  return orm.query(User)
    .with('orders')
    .where({ id: userId })
    .first();
}

function getUserFullProfile(userId: number) {
  // 需要完整信息时加载所有关联
  return orm.query(User)
    .with('profile')
    .with('orders')
    .with('roles')
    .where({ id: userId })
    .first();
}

延迟加载 vs 预加载

延迟加载的优势

  1. 减少初始查询复杂度:只查询主实体数据
  2. 按需加载:只加载实际使用的关联数据
  3. 内存效率:避免加载不必要的数据
typescript
// 延迟加载:只查询需要的数据
const user = orm.query(User)
  .where({ id: 1 })
  .first();

// 后续需要时再查询关联
const userWithOrders = orm.query(User)
  .with('orders')
  .where({ id: 1 })
  .first();

预加载的优势

  1. 避免 N+1 查询:一次性加载所有数据
  2. 性能可预测:查询次数固定
  3. 适合批量处理:处理大量数据时更高效
typescript
// 预加载:一次性加载所有数据
const users = orm.query(User)
  .with('profile')
  .find();

// 访问关联数据不会触发额外查询
users.forEach(user => {
  console.log(user.profile?.bio);
});

性能对比

场景延迟加载预加载
单条记录查询适合,按需加载适合,一次加载
批量记录查询可能产生 N+1 问题推荐,避免 N+1
不确定是否需要关联推荐,节省资源可能浪费资源
确定需要关联数据需要额外查询推荐,一次完成

最佳实践

  1. 明确需求:根据业务需求决定是否加载关联
  2. 避免 N+1:批量查询时使用 with() 预加载
  3. 按需加载:单条查询时可以按需加载关联
  4. 合理设计:关联层级不宜过深