【译】关系对象模型 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 的保存;
  • 多对多的关系会创建一张中间表。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容