typeORM大纲

typeORM大纲

七月 25, 2019

#ORM框架/typeORM #后端/数据库 #egg


前言:最近的项目中大佬用了typeorm操作数据库,看了两天没看太懂,找到一篇不错的简书就边看边跟着打下来,效果不错,分享一下~~


内容来源:typeorm数据库ORM框架中文文档 js,node,typescript - 简书


分布指南

对ORM的期望是什么?
它能帮助我创建数据库表、增删改查数据,并不必编写大量难以维护的SQL查询。


创建模型

如何通过typeORM创建一个数据库表?
答案:通过模型。
模型就是数据库中的表。
例如有一个Photo模型:

1
2
3
4
5
6
7
export class Photo {
id:number;
name:string;
description:string;
filename:string;
views:number;
}

要在数据库中存储东西,首先需要一个数据库表,并从模型创建数据库表。


创建实体

实体是::@Entity:: 装饰的模型。
typeORM将在任何地方使用实体,可以使用他们增删改查等其他操作。
例如以下,把Photo模型变成一个实体:

1
2
3
4
5
6
7
8
9
10
11
import {Entity} from "typeorm";

@Entity()
export class Photo{
id:number;
name:string;
description:string;
filename:string;
views:number;
isPublished:boolean;
}

typeORM将会为Photo实体创建一个数据库表,能在程序的任何地方使用它。
此时已经创建了一个数据库表,但没有表示列,需要添加列。


添加数据库列表

要添加数据库列,只需要将生成的实体属性用@Column装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {Entity,Column} from "typeorm"

@Entity()
export class Photo {

@Column()
id:number;

@Column()
name:string;

@Column()
description:string;

@Column()
filename:string;

@Column()
views:number;

@Column()
isPublished:boolean;
}

现在 id, name, description,filename,views和isPublished列将会被添加到Photo表中。
数据库中列的类型是由我使用的属性类型推断出来的,例如:number将会被转换成integer,string转换为varchar,boolean转换为bool等。
也可以通过隐式在@Column装饰器传入类型,将列类型指定为我的数据库支持的任何类型。


创建一个主键列

每个表都必须至少有一个主键列。要使列成为主键,需要使用@PrimaryColumn修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {Entity,Column,PrimaryColumn} from "typeorm";

@Entity()
export class Photo {

@PrimaryColumn()
id:number;

@Column()
name:string;

@column()
description:string;

@Column()
filename:string;

@Column()
views:number;

@Column()
isPublished:boolean;
}

创建一个自动生成的列

当需要列自动生成时(如id自动递增)
需要将@PrimaryColumn修饰符更改为@PrimaryGeneratedColumn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {Entity,Column,PrimaryGeneratedColumn} from "typeorm"

@Entity()
export class Photo {

@PrimaryGeneratedColumn()
id:number;

@Column()
name:string;

@Column()
description:string;

@Column()
filename:string;

@column()
views:number;

@Column()
isPublished:boolean;
}

列数据类型

接下来设置数据类型,默认情况下,字符串是被映射到一个varchar(255)类型中(取决于数据库类型)。
数字被映射到一个integer类型(取决于数据库类型)。
不希望所有的列都是有限的varchars或整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Entity,Column,PrimaryGeneratedColumn} from "typeorm";

@Entity()
export class Photo {

@PrimaryGeneratedColumn()
id:number;

@Column({
length:100
})
name:string;

@Column("text")
description:string;

@Column()
filename:string;

@Column("double")
views:number;

@Column()
isPublished:boolean;
}

列类型取决于数据库支持的类型。
可以设置数据库支持的任何列类型。
更多关于支持的列类型信息可以在这里找到 这里


创建数据库连接

现在实体已经有了,现在新建一个index.ts配置数据库连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "reflect-metadata"
import {createConnection} from "typeorm"
import {Photo} from "./entity/Photo"

createConnection({
type:"mysql",
host:"localhost",
port:3306,
username:"root",
password:"admin",
database:"test",
entities:[
Photo
],
synchronize:true,
logging:false
}) .then(connection => {
//此处可以写实体操作相关的代码
}).catch(error => console.log(error));

把Photo实体加到数据库的连接实体列表中,所有需要在这个连接下使用的实体都必须加到这个列表中。
Synchronize选项可以在应用启动时确保你的实体和数据库保持同步。


引用目录下的所有实体

当创建更多的实体并把它们加到配置当中时,一个个添加到entities中比较麻烦,可以直接写上实体店目录,这样这个目录下的所有实体都可以在当前连接中被使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {createConnection} from "typeorm"
createConnection({
driver:{
type:"mysql",
host:"localhost",
port:3306,
username:"root",
password:"admin",
database:"test"
},
entities:[
__dirname+"/entity/*.ts"
],
synchronize:true,
}).then(connection => {
//here you can start to work with your entities
}).catch(error => console.log(error));

添加和插入photo

创建一个新的photo并存到数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {createConnection} from "typeorm"

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

let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;

connection.manager.save(photo).then(photo => {
console.log("Photo has been saved");
});
}).catch(error => console.log(error));

使用async/await语法

现在利用TypeScript的async/await语法来实现同样的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

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

let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;

await connection.manager.save(photo);
console.log("Photo has been saved");

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

使用EntityManager

刚刚创建了一个新的photo并存进了数据库。
使用EntityManager可以操作实体,现在用EntityManager来把photo从数据库中取出来。

1
2
3
4
5
6
7
8
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

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

let savedPhotos = await connection.manager.find(Photo);
console.log("All photos from the db:",savedPhotos);
}).catch(error => console.log(error));

savedPhotos会从数据库中取得一个Photo对象数组。


使用Repositories

重构代码,使用Repository来代替EntityManage。每个实体都有自己的repository,可以对这个实体进行任何操作。
如果要对实体进行很多操作,Repositories会比EntityManager更加方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {createConnection} from "typefrm";
import {Photo} from "./entity/Photo";

createConnection().then(async connectiojn => {

let photo = new Photot();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;

let photoRepository = connection.getRepository(Photo);

await photoRepository.save(photo);
console.log("Photo has been saved");

let savedPhotos = await photoRepository.find();
console.log("All photos from the db:",savedPhotos);

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

从数据库中获取photos

尝试用Repository做一些数据方面的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection().then(async connection =>{
//省略上方代码
let allPhotos = await photoRepository.find();
console.log("All photos from the db:",allPhotos);

let firstPhoto = await photoRepository.findOne(1);
console.log("First photo from the db:",firstPhoto);

let meAndBearsPhoto = await photoRepository.findOne({
name:"Me and Bears photo from the db:"});
console.log("Me and Bears photo from the db:",meAndBearsPhoto);

let allViewedPhotos = await photoRepository.find({ views: 1});
console.log("All viewed photos:",allViewedPhotos);

let allPublishedPhotos = await photoRepository.find({ isPublished:true});
console.log("All published photos:",allPublishedPhotos);

let [allPhotos,photosCount] = await photoRepository.findAndCount();
console.log("All photos:",allPhotos);
console.log("Photots count:",photosCount);
}).catch(error =>console.log(error));

更新photo

从数据库中取出一个photo,修改并更新到数据库。

1
2
3
4
5
6
7
8
9
import {createConnection} from "typeorm";
import {Photo} from "./entity/photo";

createConnection().then(async connection =>{

let phototToUpdate = await photoRepository.findOne(1);
photoToUpdate.name = "Me,my friends and polar bears";
await photoRepository.save(photoToUpdate);
}).catch(error => console.log(error));

这个id = 1的photo在数据库中就更新成功了。


删除photo

接下来是删除photo:

1
2
3
4
5
6
7
8
9
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection().then(async connection =>{

let photoToRemove = await photoRepository.findOne(1);
await photoRepository.remove(photoToRemove);

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

这个Id= 1的photo在数据库中被移除。


创建与另一个类一对一的关系

新建PhotoMetadata.ts用来存photo的元信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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,它可以用来在两个实体之间创建一对一关系。
Type => Photo指示了我们想要连接的实体类名,这里因为TypeScript语言的支持原因不能直接用类名。
也可使用() => Photo ,但是type =>Photo显得更有可读性。
Type变量本身不包含任何东西。
@JoinColumn装饰器可以指定一对一关系的拥有者。
可以是单向或是双向,但只有一方是拥有者,加这个装饰器表示这个关系是给这个表服务的。
运行后会新建一个“PhotoMetadata”表,里面有个外键“photoId”


保存一个有一对一关系的对象

现在创建一个photo和一个photo的元信息,并将它们连接起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.isPublished = true;
//创建一个photo元信息
let metadata = new PhotoMetadata();
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "cybershoot";
metadata.orientation = "portait";
metadata.photo = photo;
//获取photo实体
let photoRepository = connection.getRepository(Photo);
let metadataRepository = connection.getRepository(PhotoMetadata);

await photoRepository.save(photo);

await metadataRepository.save(metadata);

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

}).catch(error => console.log(error));
```
- - - -
## 双向关系
关系可以是单向也可以是双向
现在PhotoMetadata和Photo是单向关系,关系拥有者是PhotoMetadata,Photo并不知道PhotoMetadata,这样如果想要从Photo里得到PhotoMetadata大数据会比较麻烦,现在来把单向改成双向:
```ts
improt {Entity,Column,PrimaryGeneratedColumn,OneToOne,JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class PhotoMetadata {
//...其他列
@OneToOne"(type => Photo,photo =>photo.metadata)
@JoinColumn()
photo:Photo;
}
1
2
3
4
5
6
7
8
9
10
11
import {Entity,Column,PrimaryGeneratedColumn,OneToOne}from "typeorm";
import {PhotoMetadata} from "./PhotoMetadata";

@Entity()
export class Photo {

//其他列

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

photo => photo.metadata 是用来指定反向关系的字段名字,photo.metadata就指出了photo里的metadata字段名字。
也可以使用@OneToOne(‘metadata’)来达到同样的目的,不过这种对于以后的代码重构不友好。
以上,@JoinColumn只能在关系的一边使用,使这边作为关系的拥有者,关系拥有者在数据库里的表现就是拥有一个外键列。


取出关系对象的数据

用一个查询将photo以及它的元信息取出。
有两种方式,一是用FindOptions,另一个是QueryBuilder。
先试一下FindOptions,通过指定FindOptions接口作为参数来使用Repository.find方法可以完成非常复杂的查询。

1
2
3
4
5
6
7
8
9
10
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"]});

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

返回的photos是从数据库里取回的数组,每个photo都包含它的元信息。

alias是FindOptions的一个必须选项,这是在select里定义的别名,需要用在下面的where,order by,group by,join以及其他表达式。

这里还用到了innerJoinAndSelect,表示内联查询photo.metadata的数据。
“photo.metadata”里”photo”是一个别名,“metadata”则是想查询的那个对象的属性名。
“Metadata”:是内联返回数据的新的别名。

下面尝试第二种:QueryBuilder来达到同样目的,使用QueryBuilder可以优雅地完成复杂的查询:

1
2
3
4
5
6
7
8
9
10
11
12
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();
}).catch(error => console.log(error));

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

上面要保存关系对象需要一个一个保存,有些麻烦。
如果要当关系对象中的一个被保存后,另一个也被保存,则可使用cascade选项。
稍微修改@OneToOne装饰:

1
2
3
4
5
6
export class Photo{
@OneToOne()type =>PhotoMetadata,metadata =>metadata.photo,{
cascade:true,
})
metadata:PhotoMetadata;
}

使用cascade就可以不需要像上面那样边存photo再存metadata了。
现在我们来单存photo对象,由于cascade作用,metadata也会自动存上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
createConnection(options).then(async connection =>{
//创建photo对象
let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.isPublished =true;

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

photo.metadata = metadata; //连接

//得到repository
let photoRepository = connection.getRepository(Photo);

//存photo
await photoRepository.save(photo);
//photo metadata也自动存储了
console.log("Photo is saved, photo metadata is saved too.");

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

多对一/一对多关系

接下来显示多对一/一对多关系。
假设一个photo会有一个author,并且每个author可以有很多photo。
先创建Author实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Entity,Column,PrimaryGeneratedColumn,OneToMany,JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class Author {

@PrimaryGeneratedColumn()
id:number;

@Column()
name:string;

@OneToMany(type => Photo,photo => photo.author)//注:下面会为Photo创建author属性
photo:Photo[];
}

Author包含一个反向的关系,OneToMany总是反向的,并且总是与ManyToOne成对出现。

现在来为Photo加上关系拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
import {Entity,Column,PrimaryGeneratedColumn,ManToOne} from "typeorm";
import {PhotoMetadata} from "./PhotoMetadata";
import {Author} from "./Author";

@Entity()
export class Photo {
//其他列

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

}

在ManyToOne/OneToMany关系中,拥有者一边总是ManyToOne。注:拥有外键者即关系拥有者。
也就是ManyToOne的那个字段存的是另一个对象的id。注:也就是上面的author虽然属性是Author,但在数据库中类型是Author id的类型,存的也是id。
执行上面的代码将会自动创建author表,如下:

因为photo表已经存在,所以不是增加表,而是修改photo表,添加一个新的外键列author;


多对多关系

假设photo可以存在多个相册中,并且相册里可以包含多个photo。
先创建一个Album类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable} from "typeorm";

@Entity()
export class Album {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

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

@JoinTable多对多关系拥有者必须指定的。
接着给Photo实体加个反向关系:

1
2
3
4
5
export class Photo{
//其他列
@ManyToMany(type => Album, album => album.photos)
albums:Album[];
}

执行上面的代码后会自动创建一个叫album_photos_photo_albums的联接表:

记得把Album实体加到ConnectionOptions中:

1
2
3
4
const options: ConnectionOptions = {
//其他配置
entities: [Photo, PhotoMetadata, Author, Album]
};

现在往数据库里插入albums和photos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let connection = await createConnection(options);

//创建几张相册
let album1 = new Album();
album1.name = "Bears";
await connection.manager.save(album1);

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

//创建几个相片
let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";l
photo.albums = [album1, album2];
//现在我们相片已经保存了,并且添加到相册中了
//让我们开始加载它们:
const loadedPhoto = await connection
.getRepository(Photo)
.findOne(1,{ relations:["albums"]});

loadedPhoto将是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
id:1,
name:"Me and Bears",
description:"I am near polar bears",
filename:"photo-with-bears.jpg",
albums:[{
id:1,
name:"Bears"
},{
id:2,
name:"Me"
}]
}

使用QueryBuilder

可以利用QueryBuilder来构建一个非常复杂的查询,例如:

1
2
3
4
5
6
7
8
9
10
11
12
let photos = await connection
.getRepository(Photo)
.createQueryBuilder("photo")
.innerJoinAndSelect("photo.metadata","metadata")
.leftJoinAndSelect("photo.albums","album")
.where("photo.isPublished = true")
.andWhere("(photo.name = :photoName OR photo.name = :bearName)")
.orderBy("photo.id","DESC")
.skip(5)
.take(10)
.setParameters({ photoName:"My",bearName:"Mishka"})
.getMany();

这个查询会查找已经published的,并且那么是“My”或“Mishka”的数据,
得到的结果会从第五个开始(分页偏移决定的),
并且只会得到10个结果(分页每页个数决定的),
所得结果以id倒序排序,
Photo的albums是左连接,Photo的metadata是内连接。
将在应用程序中大量使用QueryBuilder。
了解更多QueryBuilder 这里 .


样例

看看 样例 里这些例子的用法
这些仓库,你可以克隆下来帮助你开始:


扩展

这几个扩展可以简化TypeORM的使用,并将其与其他模块集成:


留言

欢迎查看使用TypeORM 和 nestjs 实现的 Mock Server 前端Mock接口和数据方案

浙ICP备17032786号-2