TypeORM
介绍
可简单理解为:
数据库的表(table) --> 类(class)
记录(record,行数据)--> 对象实例(object)
字段(field)--> 对象的属性(attribute)
以下有一些特性(好处)的说明,但糟糕的是学习 ORM 的成本并不低,于是有了这篇抄写(相对原文筛减)。
分步指南
1. 创建一个模型
使用数据库从创建表开始。如何告诉 TypeORM 创建数据库表?答案是 - 通过模型。应用程序中的模型即数据库的表。
举个例子,存在 Photo
模型:
export class Photo {
id: number
name: string
description: string
filename: string
views: number
}
并且希望将 photos 存储在数据库中。要在数据库中存储内容,首先需要一个数据库表,并从模型中创建数据库表。但是并非所有模型,只有定义为 entities 的模型才会被使用。
2. 创建一个实体
实体是由 @Entity
装饰器装饰的模型。将为此模型创建数据库表。
将 Photo
模型转为一个实体,此处采用 Active Record 模式:
import { Entity, BaseEntity } from 'typeorm'
@Entity()
export class Photo extends BaseEntity {
id: number
name: string
description: string
filename: string
views: number
}
现在,将为 Photo
实体创建一个数据库表,但没有指明哪个字段属于哪一列。
3. 添加表列
要添加数据列,只需要将要生成的实体属性加上 @Column
装饰器:
import { Entity, BaseEntity, Column } from 'typeorm'
@Entity()
export class Photo extends BaseEntity {
@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
等,当然也可以手动指定类型。
我们已经生成了一个包含列的数据库表,但是别忘了,每个数据库表必须包含主键的列。
4. 创建主列
每个表必须至少有一个主键,这是必须的,无法避免。要使列成为主键,可使用 @PrimaryColumn
装饰器:
import { Entity, BaseEntity, PrimaryColumn, Column } from 'typeorm'
@Entity()
export class Photo extends BaseEntity {
@PrimaryColumn()
id: number
@Column()
name: string
@Column()
description: string
@Column()
filename: string
@Column()
views: number
@Column()
isPublished: boolean
}
5. 创建自动生成的列
假设你希望 id
列自动生成,为此你需要将 @PrimaryColumn
替换为 @PrimaryGeneratedColumn
装饰器:
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class Photo extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column()
description: string
@Column()
filename: string
@Column()
views: number
@Column()
isPublished: boolean
}
6. 列数据类型
在添加表列中介绍了默认映射类型,但实际上并非想要的类型:
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class Photo extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 100
})
name: string
@Column("text")
description: string
@Column()
filename: string
@Column("double")
views: number
@Column()
isPublished: boolean
}
7. 数据库增删改查
在 Active Record 模式下使用:
const photo = new Photo()
photo.name = "photo name"
photo.description = "photo description"
photo.filename = "photo filename"
photo.isPublished = true
// 保存/更新
await photo.save()
// 删除
await photo.remove()
// 查询
await Photo.find({ skip: 2, take: 5 })
await Photo.find({ isPublished: true })
await Photo.findOne({ name: "photo name" })
当使用 save
保存实体时,它总是先尝试使用给定的实体 ID 在数据库中查找实体,如果找到则更新数据库中的这一行,如果没有则插入一个新行。
8. 创建一对一的关系
要与另一个类创建一对一的关系,需要在当前类包含另一个类的信息:
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { Photo } from './Photo'
@Entity()
export class PhotoMetadata extends BaseEntity {
@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
是一个函数,返回想要与之建立关系的实体的类。由于特定语言的关系,只能使用一个返回类的函数,而不是直接使用该类。同时也可以把它写成 () => Photo
,但是 type => Photo
显得代码更有可读性。type
变量本身不包含任何内容。
还有 @JoinColumn
装饰器,表明实体键的对应关系。关系可以时单向的或双向的。但是只有一方可以拥有,在关系的所有者方中需要使用 @JoinColumn
装饰器。
9. 保存一对一的关系
const photo = new Photo()
photo.name = "photo name"
photo.description = "photo description"
photo.filename = "photo filename"
photo.isPublished = true
const metadata = new PhotoMetadata()
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "metadata comment";
metadata.orientation = "portait";
metadata.photo = photo; // 联接两者
// 先保存 photo
await photo.save()
// 再保存 photo 的 metadata
await metadata.save()
10. 反向关系
关系可以是单向的或双向的。目前 PhotoMetadata 和 Photo 之间的关系是单向的。关系的所有者时 PhotoMetadata,而 Photo 对 PhotoMetadata 一无所知。这使得从 Photo 中访问 PhotoMetadata 变得很复杂。要解决这个问题,我们应该在它们之间建立双向关系。
// PhotoMetadata.ts
@Entity()
export class PhotoMetadata extends BaseEntity {
/* 省略其他列 */
@OneToOne(type => Photo, photo => photo.metadata)
@JoinColumn()
photo: Photo
}
// Photo.ts
@Entity()
export class Photo extends BaseEntity {
/* 省略其他列 */
@OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
metadata: PhotoMetadata
}
注意,我们应该仅在关系的一侧使用 @JoinColumn
装饰器,关系的拥有方包含数据库中具有外键的列。
11. 取出关系对象的数据
在完成 反向关系 后才可以使用该方法:
await Photo.find({ relations: ['metadata'] })
await Photo.findOne(id, { relations: ['metadata'] })
使用 find 选项很简单,但是如果需要更复杂的查询,则应该使用 QueryBuilder
:
await Photo.createQueryBuilder('photo')
.innerJoinAndSelect('photo.metadata', 'metadata')
.getMany()
12. 使用 cascades 自动保存相关对象
可以在关系中设置 cascades
选项,这时就可以保存其他对象的同时保存相关对象。不知这样是否会形成事务?
@Entity()
export class Photo extends BaseEntity {
/* 省略其他列 */
@OneToOne((type) => PhotoMetadata, (photoMetadata) => photoMetadata.photo, {
cascade: true,
})
metadata: PhotoMetadata;
}
在使用时:
const photo = new Photo()
photo.name = "photo name"
photo.description = "photo description"
photo.filename = "photo filename"
photo.isPublished = true
const metadata = new PhotoMetadata()
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "metadata comment";
metadata.orientation = "portait";
metadata.photo = photo;
photo.metadata = metadata;
// 先保存 photo
await photo.save()
13. 创建多对一/一对多关系
假设一个 photo 有一个 author,每个 author 都可以有多个 photos。创建一个 Author
实体:
import {
BaseEntity,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Photo } from './photo.entity';
@Entity()
export class Author extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Photo, (photo) => photo.author)
photos: Photo[];
}
Author
包含反向关系。@OneToMany
总是反向的,并且总是与 @ManyToOne
一起出现。
@Entity()
export class Photo extends BaseEntity {
/* 省略其他列 */
@ManyToOne(() => Author, (author) => author.photos)
author: Author;
}
14. 创建多对多关系
假设一个 photo 可以放在多个 album 中,每个 album 可以存放多个 photo。创建一个 Album
实体:
import {
BaseEntity,
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Photo } from './photo.entity';
@Entity()
export class Album extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Photo, (photo) => photo.album)
@JoinTable()
photos: Photo[];
}
@JoinTable
需要指定这是关系的所有者方。
接着添加反向关系到 Photo
中:
@Entity()
export class Photo extends BaseEntity {
/* 省略其他列 */
@ManyToMany(() => Album, (album) => album.photos)
album: Album[];
}
接着,数据库中会出现 album_photos_photo
联结表。
保存数据:
const album1 = new Album();
album1.name = 'Bears';
await album1.save();
const album2 = new Album();
album2.name = 'Me';
await album2.save();
const photo = new Photo();
photo.name = 'photo name3';
photo.description = 'photo description3';
photo.filename = 'photo filename3';
photo.views = 0;
photo.isPublished = true;
photo.album = [album1, album2];
await photo.save();
查询:
await Photo.findOne(id, { relations: ['album'] })
实体
基本实体由列和关系组成。每个实体必须有一个主列,否则该实体不会在数据库中生成对应的表。
实体列类型
可以将列类型指定为 @Column
的第一个参数,或者在 @Column
的列选项中指定:
@Column('int')
// or
@Column({ type: 'int' })
// 如果还有其他参数
@Column('varchar', { length: 200 })
// or
@Column({ type: 'varchar', length: 200 })
还有一些常用的类型:
enum
类型:export enum UserRole { ADMIN = 'admin', EDITOR = 'editor', GHOST = 'ghost' } @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column({ type: 'enum', enum: UserRole, default: UserRole.GHOST }) role: Role }
export enum UserRoleType = 'admin' | 'editor' | 'ghost' @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column({ type: 'enum', enum: ['admin', 'editor', 'ghost'], default: 'ghost' }) role: Role }
simple-array
类型:注意不能在值里面有任何逗号。
@Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column('simple-array') hobbies: string[] }
const user = new User() user.hobbies = ['play', 'sleep']
simple-json
类型:interface UserProfile { name: string nickname: string } @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column('simple-json') profile: UserProfile }
const user = new User() user.profile = { name: 'John', nickname: 'Malkovich' }
实体列选项
该选项参考 ColumnOptions
接口:
type: ColumnType
:列类型name: string
:数据库表中的列名length: number
:列类型的长度,例如创建varchar(150)
width:number
:列类型的显示范围onUpdate: string
:ON UPDATE
触发器nullable: boolean
:在数据库中使列NULL
或NOT NULL
,默认 falseselect: boolean
:定义在查询时是否默认此列default: string
:添加数据库列的DEFAULT
值primary: boolean
:将列标记为主要列unique: boolean
:将列标记为唯一列comment: string
:数据库列备注precision: number
:十进制列的精度,这是为值存储的最大位数scale: number
:十进制列的比例,表示小数点右侧的位数zerofill: boolean
:将ZEROFILL
属性设置为数字列unsigned: boolean
:将UNSIGNED
属性设置为数字列charset: string
:定义列字符集collation: string
:定义列排序规则enum: string[] | AnyEnum
:在enum
列类型中使用,以指定允许的枚举值列表asExpression: string
:生成的列表达式generatedType: 'VIRTUAL' | 'STORED'
:生成的列类型
嵌入式实体
相对于继承而言,组合可能是更好的选择,不过这会影响数据库的列名。
组合会减少代码,但并不会影响数据库,因为未提供生成数据库表所必需的 @Entity
装饰器和 主键。
export class Name {
@Column()
first: string
@Column()
last: string
}
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: string
@Column(() => Name)
name: Name
@Column()
isActive: boolean
}
@Entity()
export class Employee extends BaseEnity {
@PrimaryGeneratedColumn()
id: string
@Column(() => Name)
name: Name
@Column()
isActive: number
}
树实体
树结构支持多种类型,除 邻接列表外都可通过 @Tree
装饰器简单区分:
nested-set
:嵌套集。对读取非常有效,但对写入不利,且不能在嵌套集中有多个根materialized-path
:物化路径(“路径枚举”)。简单有效closure-table
:闭合表。在读取和写入方面都很有效
使用:
// entity
@Entity()
@Tree('closure-table')
export class Category extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
存储数据:
const a1 = new Category();
a1.name = 'a1';
await a1.save();
const a11 = new Category();
a11.name = 'a11';
a11.parent = a1;
await a11.save();
const a12 = new Category();
a12.name = 'a12';
a12.parent = a1;
await a12.save();
const a111 = new Category();
a111.name = 'a111';
a111.parent = a11;
await a111.save();
const a112 = new Category();
a112.name = 'a112';
a112.parent = a11;
await a112.save();
查询数据:
// 返回所有
const trees = await getTreeRepository(Category).findTrees()
// 返回根
const roots = await getTreeRepository(Category).findRoots()
// 返回子 Tree
const a11 = await Category.findOne({ name: 'a11' })
const children = await getTreeRepository(Category).findDescendantsTree(a11)
// 返回父级
const a11 = await Category.findOne({ name: 'a11' })
const parent = await getTreeRepository(Category).findAncestorsTree(a11)
更多查询参考 使用树实体。
关系
关系可以帮助你轻松地与相关实体合作。
JoinColumn & JoinTable
@JoinColumn
不仅定义了关系的哪一侧包含带有外键的连接列,还允许自定义连接列名和引用的列名。
当我们设置 @JoinColumn
时,它会自动在数据库中创建一个名为 propertyName + referenceColumnName
的列。
@JoinTable
用于 “多对多” 关系,联结表是由 TypeORM 自动创建的一个特殊的单独表。
多对一和一对多
可以在 @ManyToOne
/ @OneToMany
关系中省略 @JoinColumn
,除非需要自定义关联列在数据库中的名称。
@ManyToOne
可以单独使用,但 @OneToMany
必须搭配 @ManyToOne
使用。
在设置 @ManyToOne
的地方,相关实体将会有 “关联 id” 和外键。