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 的保存;
- 多对多的关系会创建一张中间表。