在 NestJS 中使用 sequelize-typescript 时,如果数据库表中没有建立物理的外键约束(Foreign Key Constraint),但在业务逻辑上存在一对一的关系,你完全可以通过在代码层面(ORM 层)定义关联来解决。

Sequelize 的关联关系本质上是基于字段的值进行 JOIN 查询,并不强依赖数据库底层的物理外键约束。处理这种情况主要分以下两种场景:

场景一:表中有外键字段,但没有物理约束(最常见)

比如 Profile 表里有一个 userId 字段,但数据库并没有设置它必须关联 User 表的主键。你只需要在模型中显式指定这个字段作为外键即可。

核心做法:在 @ForeignKey 和关联装饰器(@BelongsTo / @HasOne)中明确指定外键字段名。

import { Table, Column, Model, ForeignKey, BelongsTo, HasOne } from 'sequelize-typescript';

// 1. User 模型(一方)
@Table({ tableName: 'users' })
export class User extends Model<User> {
  @Column
  name: string;

  // 一个用户拥有一个资料
  // 显式告诉 Sequelize,Profile 表里的 'userId' 字段就是外键
  @HasOne(() => Profile, { foreignKey: 'userId' })
  profile: Profile;
}

// 2. Profile 模型(另一方)
@Table({ tableName: 'profiles' })
export class Profile extends Model<Profile> {
  @Column
  bio: string;

  // 在代码中定义外键字段(即使数据库没加约束)
  @ForeignKey(() => User)
  @Column
  userId: number;

  // 建立一对一关联,指向 User
  @BelongsTo(() => User, { foreignKey: 'userId' })
  user: User;
}

场景二:表中连外键字段都没有(纯逻辑关联)

如果你的两张表里完全没有关联字段(例如 User 表有 usernameProfile 表也有一个 username,你想通过这两个相同的 username 来建立一对一关系),可以通过指定 sourceKeytargetKey 来实现。

核心做法:使用 sourceKey(源模型的关联键)和 targetKey(目标模型的关联键)来告诉 Sequelize 按照哪两个字段进行 JOIN

// User 模型
@Table({ tableName: 'users' })
export class User extends Model<User> {
  @Column
  username: string; // 假设通过 username 关联

  // sourceKey: 当前模型(User)用什么字段去关联
  @HasOne(() => Profile, { 
    foreignKey: 'username', // 逻辑上的外键名
    sourceKey: 'username'   // User 表里的字段
  })
  profile: Profile;
}

// Profile 模型
@Table({ tableName: 'profiles' })
export class Profile extends Model<Profile> {
  @Column
  username: string; // Profile 表里也有 username
  @Column
  bio: string;

  // targetKey: 目标模型(Profile)用什么字段被关联
  @BelongsTo(() => User, { 
    foreignKey: 'username', // 逻辑上的外键名
    targetKey: 'username'   // Profile 表里的字段
  })
  user: User;
}

💡 业务层数据一致性提醒

由于数据库层面没有物理外键约束,数据库不会自动帮你拦截非法数据(比如删除了 User,但 Profile 里的 userId 还在)。你需要在 NestJS 的业务逻辑层(Service 层)手动保证数据的一致性:

  1. 事务处理(Transaction):在同时新增或修改关联数据时,务必使用数据库事务,确保要么全成功,要么全回滚。
  2. 手动校验与级联:在删除 User 之前,先查询并手动删除对应的 Profile 数据。
  3. 联表查询:在获取数据时,通过 include 轻松实现一对一数据的联表查询:
    // 获取用户及其资料
    const user = await this.userModel.findOne({
      where: { id: 1 },
      include: [Profile] // Sequelize 会自动生成 LEFT JOIN 语句
    });
    

只要代码层面的关联配置正确,即使没有物理外键,Sequelize 依然能完美处理一对一的联表查询和数据映射。

更多推荐