Skip to content

第八章 一对一关系

本章目标

  1. 理解"拥有方 / 被拥有方"的概念,知道 @JoinColumn 该写在哪一边。
  2. 能用 @OneToOne 建立单向和双向一对一关系。
  3. 掌握一对一关系的保存顺序。
  4. 会用 relations 选项和 QueryBuilder 的 leftJoinAndSelect 查询关联数据。

核心概念

一个人 ↔ 一张身份证:一个人只有一张有效身份证,一张身份证也只属于一个人,这就是一对一关系。

在数据库里,一对一关系的实现方式是:在其中一张表上加一个"外键列 + 唯一约束",指向另一张表的主键。

  • 放了外键列的那张表,对应的实体叫 拥有方(owning side)
  • 没放外键、被指向的那张表,对应的实体叫 被拥有方(inverse side)
  • TypeORM 用 @JoinColumn() 标记"外键列放在我这边"。@JoinColumn 必须且只能写在拥有方

最小的一对一关系(单向)

ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToOne,
  JoinColumn,
} from "typeorm";

@Entity()
class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  avatar: string;

  @Column()
  bio: string;
}

@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToOne(() => Profile) // 目标实体:Profile
  @JoinColumn() // 外键列建在 user 表上
  profile: Profile;
}

建表后,user 表会多出一个 profileId 列,并且带唯一约束 + 外键(唯一约束正是"一对一"的保障——同一个 profileId 不能被两个 user 引用)。

双向一对一

  • 单向关系只能从 User 查到 Profile。想从 Profile 反查 User,要在 Profile 上补一个反向映射
ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToOne,
  JoinColumn,
} from "typeorm";
import { User } from "./User";

@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  avatar: string;

  @Column()
  bio: string;

  @OneToOne(() => User, (user) => user.profile) // 反向映射到 User.profile
  user: User;
}

使用

  • 因为外键在user表,所以先插入profile表,等profile表中有id,再插入user表
ts
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";
import { Profile } from "./entities/Profile";
AppDataSource.initialize()
  .then(async () => {
    const profile = new Profile();
    profile.avatar = "https://www.baidu.com";
    profile.bio = "a bio";
    await AppDataSource.manager.insert(Profile, profile);
    const user = new User();
    user.name = "yl840414";
    user.profile = profile;
    await AppDataSource.manager.insert(User, user);
  })
  .catch((error) => console.log(error));

每次都存两次很麻烦?可以用 cascade: true 让保存 User 时自动连带保存 Profile,见级联操作。

查询的两种方式

1. 用 relations 选项查询关联数据

ts
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";
AppDataSource.initialize()
  .then(async () => {
    const result = await AppDataSource.manager.find(User, {
      relations: {
        profile: true,
      },
    });
    console.log(result);
  })
  .catch((error) => console.log(error));

2. 使用 QueryBuilder 的 leftJoinAndSelect 查询关联数据

ts
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";
AppDataSource.initialize()
  .then(async () => {
    const userRepo = AppDataSource.getRepository(User);
    const user = await userRepo
      .createQueryBuilder("user")
      .leftJoinAndSelect("user.profile", "profile")
      .where("user.id = :id", { id: 1 })
      .getOne();
    console.log(user);
  })
  .catch((error) => console.log(error));

双向查询

ts
import { AppDataSource } from "./data-source";
import { Profile } from "./entities/Profile";
AppDataSource.initialize()
  .then(async () => {
    const result = await AppDataSource.manager.find(Profile, {
      relations: {
        user: true,
      },
    });
    console.log(result);
  })
  .catch((error) => console.log(error));