【译】关系对象模型 typeorm 下

typeorm 有什么用?typeorm 以操作对象方法的方式与数据库交互,而不是像传统库(mysql)那样需要写 sql 语句。

本文主要说什么?Typeorm README.md 写的很长,往下拉可以看到 "Step-by-Step Guide" 标题,本文翻译这部分内容(非直译),帮助新手一步步熟悉并使用 typeorm。(翻译时 typeorm 版本为 0.2.24。)

接上文.......

表与表之间的关系

创建一对一的关系

PhotoMetadata 类,描述照片的详细信息,如长度、宽度、朝向、评论等信息,该类与 Photo 类的关系是一对一的关系:一张照片有且仅有一份详细信息,一份详细信息仅属于一张照片。

创建 src/entity/PhotoMetadata.ts 文件,内容如下:

import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from "typeorm";
import { Photo } from "./Photo";

@Entity()
export class PhotoMetadata {

    @PrimaryGeneratedColumn()
    id: number;

    @Column("int")
    height: number;

    @Column("int")
    width: number;

    @Column()
    orientation: string;

    @Column()
    compressed: boolean;

    @Column()
    comment: string;

    @OneToOne(type => Photo)
    @JoinColumn()
    photo: Photo;
}

上面,我们使用了一个新的装饰器 @OneToOne。它允许我们为两个 Entity 设置一对一的关系,type => Photo 是一个函数,指向该类要关联的那个类。

我们强制使用一个函数来返回关联类 @OneToOne(type => Photo),而不是直接使用该类的名字 @OneToOne(Photo),这样做是由 js 语言特性决定的。(译者问:具体啥特性?)

我们也可以将函数写成 () => Photo,但是推荐使用 type => Photo,因为这样更易读,type 参数本身没有任何意义,即它的值是 undefined。

我们还使用了 @JoinColumn 装饰器,这个装饰器可以指定一对一关系的拥有者。
Photo 拥有 PhotoMetadata,PhotoMetadata 属于 Photo,在 PhotoMetadata 上加 @JoinColumn,在建表时体现在 photo_metadata 表多一个 photoId 这个外键。

运行应用 $ node dist/testConnect.js ,可以看到数据库里又新增了一张 photo_metadata 表,并且包含了关联 photo 表的外键。

+-------------+--------------+----------------------------+
|                     photo_metadata                      |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| height      | int(11)      |                            |
| width       | int(11)      |                            |
| comment     | varchar(255) |                            |
| compressed  | boolean      |                            |
| orientation | varchar(255) |                            |
| photoId     | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

保存一对一的关系

现在让我们保存一张照片及其元数据,并将它们彼此连接起来。

创建 src/testRelation.ts 文件,内容如下:

import { createConnection } from "typeorm";
import { Photo } from "./entity/Photo";
import { PhotoMetadata } from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    // 创建 Photo 对象
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.views = 2;
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // 创建 PhotoMetadata 对象
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo = photo; // <=== 将 Photo 和 PhotoMetadata 关联起来

    // 获取 repository
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);

    // 首先保存照片
    await photoRepository.save(photo);

    // 照片保存之后,再保存元数据,这是因为元数据中包含了照片数据的外键
    await metadataRepository.save(metadata);

    // done
    console.log("Metadata is saved, and relation between metadata and photo is created in the database too");

}).catch(error => console.log(error));

执行程序 $ node dist/testRelation.js,查看数据库新增了一条 photo 数据,一条 photo_metadata 数据,并且后者包含前者的外键值。

双向关系

上面的关系是单向的,PhotoMetadata 中包含 Photo 的外键,因此查询 PhotoMetadata 时,可以顺带查出 Photo 的信息;

import { createConnection } from "typeorm";
import { PhotoMetadata } from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    let photometadataRepository = connection.getRepository(PhotoMetadata);
    let photometadatas = await photometadataRepository.find({ relations: ["photo"] });
    console.log('photometadatas ==================== \n', photometadatas)

}).catch(error => console.log(error));

查询结果,可以看到 PhotoMetadata 中确实包含了 photo 的数据。

photometadatas ==================== 
 [
  PhotoMetadata {
    id: 1,
    height: 640,
    width: 480,
    orientation: 'portait',
    compressed: true,
    comment: 'cybershoot',
    photo: Photo {      # <====== 只是查询 metadata 数据,会顺带把 photo 的数据也查出来
      id: 3,
      name: 'Me and Bears',
      description: 'I am near polar bears',
      filename: 'photo-with-bears.jpg',
      views: 2,
      isPublished: true
    }
  },
  // .....
 ]

上面的代码,Photo 和 PhotoMetadata 的关系是单向的,关系的拥有者是 PhotoMetadata,但 Photo 类不知道任何 PhotoMetadata 的信息,这使得从 Photo 类访问 PhotoMetadata 变得复杂。

为了解决这个问题,我们应该为 Photo 也添加一个关系,使得 Photo 和 PhotoMetadata 变成双向关系。
(改成双向关系后,查询 Photo 信息,也可以顺带查询出 PhotoMetadata 的信息)

修改 PhotoMetadata 类:

import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from "typeorm";
import { Photo } from "./Photo";

@Entity()
export class PhotoMetadata {

    /* ... other columns */

    @OneToOne(type => Photo, photo => photo.metadata)
    @JoinColumn()
    photo: Photo;
}

修改 Photo 类:

import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm";
import { PhotoMetadata } from "./PhotoMetadata";

@Entity()
export class Photo {

    /* ... other columns */

    @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
    metadata: PhotoMetadata;
}

查询关系对象数据

现在我们可以用一条查询语句得到照片信息和它的元数据信息,有两种方式可以做到这点:

  • 使用 find* 方法
  • 使用 QueryBuilder 函数

使用 find* 方法,该方法允许您指定关联关系的对象。(下面👇代码关联关系的对象是 Photo 类中的 metadata 属性)

import { createConnection } from "typeorm";
import { Photo } from "./entity/Photo";
import { PhotoMetadata } from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoRepository = connection.getRepository(Photo);
    let photos = await photoRepository.find({ relations: ["metadata"] });
    console.log('photos', photos)

}).catch(error => console.log(error));

返回的 photos 是从数据库中查询的照片数组,每张照片都包含元数据信息。

photos [
  Photo {
    id: 3,
    name: 'Me and Bears',
    description: 'I am near polar bears',
    filename: 'photo-with-bears.jpg',
    views: 2,
    isPublished: true,
    metadata: PhotoMetadata {   # <=== 查 photo 也能顺带把 metadata 数据查出来 
      id: 1,
      height: 640,
      width: 480,
      orientation: 'portait',
      compressed: true,
      comment: 'cybershoot'
    }
  },
]

使用 find() 是个简单有效的方式,但有时您需要更加复杂的查询,此时可以使用 QueryBuilder(),QueryBuilder() 允许用优雅的方式 处理复杂的查询

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photos = await connection
        .getRepository(Photo)
        .createQueryBuilder("photo")
        .innerJoinAndSelect("photo.metadata", "metadata")
        .getMany();
    console.log('photos', photos);

}).catch(error => console.log(error));

当你使用 QueryBuilder() 时,看起来就像创建 sql 查询语句一样。在这个例子中,photo 和 metadata 都是别名,你可以使用别名访问列。

使用 cascade 选项来自动保存关系对象

上面 保存 Photo 和 PhotoMetadata,我们分别做了两次保存操作。

// ......
// 首先保存照片
await photoRepository.save(photo);

// 照片保存之后,再保存元数据,这是因为元数据中包含了照片数据的外键
await metadataRepository.save(metadata);

我们希望只做一次保存操作就可以完成保存 Photo 和 PhotoMetadata,可以使用 cascade 选项。

修改 Photo 类中 @OneToOne 装饰器:

export class Photo {
    /// ... other columns

    @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
        cascade: true,     // <=== 加上 cascade 选项
    })
    metadata: PhotoMetadata;
}

因为设置了级联选项(cascade: true),现在保存 photo 时,会自动保存 metadata 数据。

createConnection(options).then(async connection => {

    // 创建 photo 对象
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.views = 3;
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // 创建 metadata 对象
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";

    // 将 photo 和 metadata 联系起来
    photo.metadata = metadata;

    // 获取 repository
    let photoRepository = connection.getRepository(Photo);

    // 做一次保存操作,可以完成保存 photo 和保存 metadata 操作
    await photoRepository.save(photo);

    console.log("Photo is saved, photo metadata is saved too.")

}).catch(error => console.log(error));

注意这里我们设置了 photo 对象的 metadata 属性,而不是像之前那样设置 metadata 的 photo 属性。因为你是在 photo 类中设置的 cascade 配置,因此保存 photo,会自动保存 metadata,但是保存 metadata,并不会自动保存 photo。

创建(多对一)/(一对多)关系

让我们创建一个(一对多)/(多对一)的关系,每张照片都有唯一一个作者,每个作者可以同时拥有很多张照片

创建 src/entity/Author.ts 文件,内容如下:

import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from "typeorm";
import { Photo } from "./Photo";

@Entity()
export class Author {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToMany(type => Photo, photo => photo.author) // note: 我们还需要在 Photo 类中添加另一种关系
    photos: Photo[];
}

@OneToMany 不能单独存在,需要在另一个类中添加 @ManyToOne。

修改 Photo 类:

import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from "typeorm";
import { PhotoMetadata } from "./PhotoMetadata";
import { Author } from "./Author";

@Entity()
export class Photo {

    /* ... other columns */

    @ManyToOne(type => Author, author => author.photos)
    author: Author;
}

@ManyToOne 装饰器和 @JoinColumn 装饰器类似,会为 photo 表添加一个 authorId 的外键。

运行应用 $ node dist/testConnect.js,数据库会自动创建 Author 表:

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

我们发现 Photo 表也被修改了,新增了 authorId 列,作为 Author 表的外键:

+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| description | varchar(255) |                            |
| filename    | varchar(255) |                            |
| isPublished | boolean      |                            |
| authorId    | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

创建多对多关系

让我们创建多对多的关系:一张照片可以保存在多个相册中(同一张照片可以打印很多张,但还是同一张照片),一个相册中可以存很多张照片

创建 src/entity/Album.ts 文件,内容如下:

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm";
import { Photo } from "./Photo"

@Entity()
export class Album {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @ManyToMany(type => Photo, photo => photo.albums)
    @JoinTable()
    photos: Photo[];
}

@JoinTable 必须被明确指定这是关系的所有者。

修改 Photo 类添加关系:

import { ..., ManyToMany } from "typeorm";
import { Album } from './Album'

export class Photo {
    /// ... other columns

    @ManyToMany(type => Album, album => album.photos)
    albums: Album[];
}

运行应用 $ node dist/testConnect.js,数据库会自动创建 album 表和 album_photos_photo 表:

+-------------+--------------+----------------------------+
|                       album                     |
+-------------+--------------+----------------------------+
|      id     | int(11)           | PRIMARY KEY FOREIGN KEY    |
|     name    | varchar(255)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
|                album_photos_photo               |
+-------------+--------------+----------------------------+
| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

多对多的关系会创建一张中间表。

现在让我们往数据库中插入一些相册和照片:

createConnection(options).then(async connection => {

    // create a few albums
    let album1 = new Album();
    album1.name = "Bears";
    await connection.manager.save(album1);

    let album2 = new Album();
    album2.name = "Me";
    await connection.manager.save(album2);

    // create a few photos
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.views = 2;
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.albums = [album1, album2];
    await connection.manager.save(photo);

    // now our photo is saved and albums are attached to it
    // now lets load them:
    const loadedPhoto = await connection
        .getRepository(Photo)
        .findOne(1, { relations: ["albums"] });

}).catch(error => console.log(error));

查询结果:

loadedPhoto Photo {
  id: 1,
  name: 'Me and Bears',
  description: 'I am near polar bears',
  filename: 'photo-with-bears.jpg',
  views: 1,
  isPublished: true,
  albums: []
}

总结:

  • 双向关系的目的是做一次查询操作,可以同时查出 Photo 以及它的 metadata 数据;
  • cascade 的目的是只要做一次保存操作,就可以完成 photo 和 metadata 的保存;
  • 多对多的关系会创建一张中间表。

推荐阅读更多精彩内容