关系

关系

关系可以帮助你轻松地与相关实体合作。有几种类型的关系:

你可以为关系指定几个选项:

  • eager: boolean - 如果设置为 true,则在此实体上使用 find *QueryBuilder 时,将始终使用主实体加载关系
  • cascade: boolean - 如果设置为 true,则将插入相关对象并在数据库中更新。
  • onDelete: "RESTRICT"|"CASCADE"|"SET NULL" - 指定删除引用对象时外键的行为方式
  • primary: boolean - 指示此关系的列是否为主列。
  • nullable: boolean -指示此关系的列是否可为空。默认情况下是可空。

一对一

一对一是一种 A 只包含一个 B 实例,而 B 只包含一个 A 实例的关系。我们以 User 和 Profile 实体为例。用户只能拥有一个配置文件,并且一个配置文件仅由一个用户拥有。

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

  @Column()
  gender: string;

  @Column()
  photo: string;
}

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

  @Column()
  name: string;

  @OneToOne(type => Profile)
  @JoinColumn()
  profile: Profile;
}

这里我们将 @OneToOne 添加到 profile 并将目标关系类型指定为 Profile。我们还添加了 @JoinColumn,这是必选项并且只能在关系的一侧设置。你设置 @JoinColumn 的哪一方,哪一方的表将包含一个 “relation id” 和目标实体表的外键。此示例将生成以下表:

+-------------+--------------+----------------------------+
|                        profile                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| gender      | varchar(255) |                            |
| photo       | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| profileId   | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

同样,@JoinColumn 必须仅设置在关系的一侧且必须在数据库表中具有外键的一侧。该例子展示如何保存这样的关系:

const profile = new Profile();
profile.gender = "male";
profile.photo = "me.jpg";
await connection.manager.save(profile);

const user = new User();
user.name = "Joe Smith";
user.profile = profile;
await connection.manager.save(user);

启用级联后,只需一次save调用即可保存此关系。要加载带有配置文件的用户,必须在FindOptions中指定关系:

const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["profile"] });

或者使用 QueryBuilder:

const users = await connection
  .getRepository(User)
  .createQueryBuilder("user")
  .leftJoinAndSelect("user.profile", "profile")
  .getMany();

通过在关系上启用预先加载,你不必指定关系或手动加入,它将始终自动加载。

双向关系

关系可以是单向的和双向的。单向是仅在一侧与关系装饰器的关系。双向是与关系两侧的装饰者的关系。我们刚刚创建了一个单向关系。让我们将它改为双向:

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

  @Column()
  gender: string;

  @Column()
  photo: string;

  @OneToOne(
    type => User,
    user => user.profile
  ) // 将另一面指定为第二个参数
  user: User;
}

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

  @Column()
  name: string;

  @OneToOne(
    type => Profile,
    profile => profile.user
  ) // 指定另一面作为第二个参数
  @JoinColumn()
  profile: Profile;
}

我们只是创建了双向关系。注意,反向关系没有@JoinColumn。@JoinColumn 必须只在关系的一侧且拥有外键的表上。双向关系允许你使用QueryBuilder从双方加入关系:

const profiles = await connection
  .getRepository(Profile)
  .createQueryBuilder("profile")
  .leftJoinAndSelect("profile.user", "user")
  .getMany();

多对一/一对多的关系

多对一/一对多是指 A 包含多个 B 实例的关系,但 B 只包含一个 A 实例。让我们以 User 和 Photo 实体为例。User 可以拥有多张 photos,但每张 photo 仅由一位 user 拥有。

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

  @Column()
  url: string;

  @ManyToOne(
    type => User,
    user => user.photos
  )
  user: User;
}

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

  @Column()
  name: string;

  @OneToMany(
    type => Photo,
    photo => photo.user
  )
  photos: Photo[];
}

这里我们将 @ManyToOne 添加到 photos 属性中,并将目标关系类型指定为 Photo。你也可以在 @ManyToOne/@OneToMany 关系中省略 @JoinColumn。没有 @ManyToOne,@OneToMany 就不可能存在。如果你想使用 @OneToMany,则需要 @ManyToOne。在你设置@ManyToOne 的地方,相关实体将有"关联 id"和外键。

+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| url         | varchar(255) |                            |
| userId      | int(11)      |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

如何保存这种关系:

const photo1 = new Photo();
photo1.url = "me.jpg";
await connection.manager.save(photo1);

const photo2 = new Photo();
photo2.url = "me-and-bears.jpg";
await connection.manager.save(photo2);

const user = new User();
user.name = "John";
user.photos = [photo1, photo2];
await connection.manager.save(user);

或者你可以选择:

const user = new User();
user.name = "Leo";
await connection.manager.save(user);

const photo1 = new Photo();
photo1.url = "me.jpg";
photo1.user = user;
await connection.manager.save(photo1);

const photo2 = new Photo();
photo2.url = "me-and-bears.jpg";
photo2.user = user;
await connection.manager.save(photo2);

启用级联后,只需一次 save 调用即可保存此关系。要在内部加载带有 photos 的 user,必须在FindOptions中指定关系:

const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["photos"] });

// or from inverse side

const photoRepository = connection.getRepository(Photo);
const photos = await photoRepository.find({ relations: ["user"] });

或者使用 QueryBuilder:

const users = await connection
  .getRepository(User)
  .createQueryBuilder("user")
  .leftJoinAndSelect("user.photos", "photo")
  .getMany();

// or from inverse side

const photos = await connection
  .getRepository(Photo)
  .createQueryBuilder("photo")
  .leftJoinAndSelect("photo.user", "user")
  .getMany();

通过在关系上启用预先加载,你不必指定关系或手动加入,它将始终自动加载。

多对多的关系

多对多是一种 A 包含多个 B 实例,而 B 包含多个 A 实例的关系。我们以 Question 和 Category 实体为例。Question 可以有多个 categories, 每个 category 可以有多个 questions。

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

  @Column()
  name: string;
}

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

  @Column()
  title: string;

  @Column()
  text: string;

  @ManyToMany(type => Category)
  @JoinTable()
  categories: Category[];
}

@JoinTable()是@ManyToMany 关系所必需的。你必须把@JoinTable 放在关系的一个(拥有)方面。此示例将生成以下表:

+-------------+--------------+----------------------------+
|                        category                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        question                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|              question_categories_category               |
+-------------+--------------+----------------------------+
| questionId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
| categoryId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

如何保存这种关系:

const category1 = new Category();
category1.name = "animals";
await connection.manager.save(category1);

const category2 = new Category();
category2.name = "zoo";
await connection.manager.save(category2);

const question = new Question();
question.categories = [category1, category2];
await connection.manager.save(question);

启用级联后,只需一次save调用即可保存此关系。要在 categories 里面加载 question,你必须在FindOptions中指定关系:

const questionRepository = connection.getRepository(Question);
const questions = await questionRepository.find({ relations: ["categories"] });

或者使用QueryBuilder

const questions = await connection
  .getRepository(Question)
  .createQueryBuilder("question")
  .leftJoinAndSelect("question.categories", "category")
  .getMany();

通过在关系上启用预先加载,你不必指定关系或手动加入,它将始终自动加载。

双向关系

关系可以是单向的和双向的。单向是仅在一侧与关系装饰器的关系。双向是与关系两侧的装饰者的关系。我们刚刚创建了一个单向关系。让我们改为双向:

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

  @Column()
  name: string;

  @ManyToMany(
    type => Question,
    question => question.categories
  )
  questions: Question[];
}

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

  @Column()
  title: string;

  @Column()
  text: string;

  @ManyToMany(
    type => Category,
    category => category.questions
  )
  @JoinTable()
  categories: Category[];
}

我们只是创建了双向关系。注意,反向关系没有 @JoinTable@JoinTable 必须只在关系的一边。双向关系允许您使用 QueryBuilder 从双方加入关系:

const categoriesWithQuestions = await connection
  .getRepository(Category)
  .createQueryBuilder("category")
  .leftJoinAndSelect("category.questions", "question")
  .getMany();

Eager 和 Lazy 关系

Eager 关系

每次从数据库加载实体时,都会自动加载 Eager 关系。例如:

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

  @Column()
  name: string;

  @ManyToMany(
    type => Question,
    question => question.categories
  )
  questions: Question[];
}

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

  @Column()
  title: string;

  @Column()
  text: string;

  @ManyToMany(
    type => Category,
    category => category.questions,
    {
      eager: true
    }
  )
  @JoinTable()
  categories: Category[];
}

现在当你加载 questions 时,不需要加入或指定要加载的关系。它们将自动加载:

const questionRepository = connection.getRepository(Question);

// questions 将加载其类别 categories
const questions = await questionRepository.find();

Eager 关系只有在使用find *方法时才有效。如果你使用QueryBuilder,则禁用 eager 关系,并且必须使用leftJoinAndSelect来加载。Eager 的关系只能用于关系的一方,在关系的两边使用eager:true是不允许的。

Lazy 关系

当你访问的时候会加载 Lazy 关系中的实体。这种关系必须有Promise作为类型,并且将值存储在一个 promise 中,当你加载它们时,也会返回 promise。例如:

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

  @Column()
  name: string;

  @ManyToMany(
    type => Question,
    question => question.categories
  )
  questions: Promise<Question[]>;
}

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

  @Column()
  title: string;

  @Column()
  text: string;

  @ManyToMany(
    type => Category,
    category => category.questions
  )
  @JoinTable()
  categories: Promise<Category[]>;
}

categories 是一个 Promise. 这意味着它是 lazy 的,它只能存储一个带有值的 promise。

例如:

保存这种关系:

const category1 = new Category();
category1.name = "animals";
await connection.manager.save(category1);

const category2 = new Category();
category2.name = "zoo";
await connection.manager.save(category2);

const question = new Question();
question.categories = Promise.resolve([category1, category2]);
await connection.manager.save(question);

如何在 Lazy 关系中加载对象:

const question = await connection.getRepository(Question).findOne(1);
const categories = await question.categories;
// you'll have all question's categories inside "categories" variable now

注意:如果你来自其他语言(Java,PHP 等)并且习惯于在任何地方使用 lazy 关系,请小心使用。这些语言不是异步的,延迟加载是以不同的方式实现的,这就是为什么不能使用 promises 的原因。在 JavaScript 和 Node.JS 中,如果你想拥有延迟加载的关系,你必须使用 promises。但是这是非标准技术,而且在 TypeORM 中被认为是实验性的。

下一页