1. 前言
先了解什么是orm,其对应的全称为Object-Relational Mapping,对象关系映射。在开发中,通常是指将数据库中的表(关系模型)映射到编程语言中的对象(对象模型),ORM框架的作用就是帮助我们实现这种映射,以方便地在程序中进行数据的存储和检索。
typeorm 就是一种orm框架,它可以运行在 NodeJS、Browser、React Native、Electron 等平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用。
与传统数据访问技术的比较,orm通常会减少需要编写的代码量,但其高度的抽象模糊了代码实现中实际发生的逻辑。在习惯了原生sql语法的情况下,使用orm进行代码编写,需要额外翻看手册,了解其语法规则,不然也是一头雾水,虽然减少了代码量,但又增加了初始的学习探索成本。因此本文尝试整理一些常用语法,希望能节约大家的一些探索时间,提供一定的帮助。
本文以nestjs框架为例,nestjs和typeorm有着紧密的集成,提供了开箱即用的@nestjs/typeorm,更方便地进行数据库的连接,实体管理和依赖注入,详细可查看文档Database。
有了@nestjs/typeorm的帮助,在service中进行数据操作变得更为便捷高效,主要集中在Repository和EntityManager两种API上。
2. Repository
注入
每个实体都有自己的Repository存储库,当你要操作具体的某个实体的数据时,使用@injectRepository装饰器来注入对应实体的Repository,可以直接使用Repository的能力。
代码语言:ts复制class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>
) {}
...
}
insert
插入新的实体数据,不会检查记录是否已存在
代码语言:ts复制async insert(insertUserDto: InsertUserDto): Promise<InsertResult> {
const user = new User();
user.firstName = insertUserDto.firstName;
user.lastName = insertUserDto.lastName;
return await this.usersRepository.insert(user);
}
save
数据库中不存在该实体,则类似insert插入该实体数据;如果已存在,则更新实体数据
代码语言:ts复制async create(createUserDto: CreateUserDto): Promise<User> {
return await this.usersRepository.save(createUserDto);
}
这里使用了两种写法,第一种在insert里显示的创建了User实体,第二种实体由typeorm隐式处理,数据赋值通过dto自动映射到实体。在保障dto类型检查准确的情况系下,第二种写法较为简洁。
find
通用查询方法,无条件时查询所有实体数据。支持多种查询参数如select、where、order、skip、take 和 relations等,可构建复杂的查询
代码语言:ts复制const users = await this.usersRepository.find({
select: ['firstName', 'lastName'],
where: {
age: MoreThan(18),
lastName: 'Doe'
},
order: {
firstName: 'ASC'
},
skip: 5,
take: 10,
relations: ['profile'] // 加载关联的 profile
});
其他
- findBy 查询指定where条件的实体
- findOne 用于查找单个实体,和find类似,只是会返回符合条件的一个实体或者null
- findOneBy 查询指定where条件的单个实体
- findAndCount 和find类似查询实体,并给出这些实体的总数,在分页查询中较常使用
- findAndCountBy 更直接的where条件查询方法
- update 通过执行的条件来更新对应实体的数据,不检查记录是否存在
- remove 删除 相应的实体数据,在操作之前,会先执行一个查询操作来获取实体
- delete 删除匹配条件的记录,操作前不会查询加载对应实体
- query 执行原生sql查询
this.usersRepository.query(
'SELECT * FROM user WHERE isActive = true'
);
3. EntityManager
另外一种方式是,是使用EntityManager API,
代码语言:ts复制class UsersService {
constructor(
private entityManager: EntityManager
) {}
...
}
这时候数据插入可以这么写
代码语言:ts复制this.entityManager.insert(User, insertUserDto);
// 或者
this.entityManager.save(User, createUserDto);
上述Repository 的api,在EntityManager上都支持的,不过使用EntityManager api需要先指定对应的实体类,后续参数完全相同。 因为从源码层面来看,Repository 实际上是 EntityManager的一个封装,它内部持有对 EntityManager的引用,其背后是调用 EntityManager来完成实际的工作的。
transaction
因此如果操作单个实体,推荐使用Repository,EntityManager更多的使用在事务管理上,尤其在涉及多个实体时。
代码语言:ts复制this.entityManager.transaction(async manager => {
manager.update(User, id, userData);
const log = manager.create(Log, {
message, userId: id,
});
await manager.save(Log, log);
return manager.findOne(User, id);
});
createQueryBuilder
另外,createQueryBuilder是一个更为常用的功能,能够覆盖更多更为复杂的sql场景,如多表联查、分组聚合、子查询等;支持链式调用,使得代码更便于阅读和维护。
首先其有两种使用方式,即上述两种类型的api都包含它。
代码语言:ts复制async builder() {
const result1 = await this.usersRepository
.createQueryBuilder()
.select('User.firstName')
.where('User.isActive = true')
.getMany();
const result2 = await this.usersRepository
.createQueryBuilder('u')
.select('u.firstName')
.where('u.isActive = true')
.getMany();
console.log(result1, result2);
const res = await this.entityManager
.createQueryBuilder(User, 'u')
.select('u.firstName')
.where('u.isActive = true')
.getMany();
console.log(res);
}
通过Repository方式使用,可以指定别名,也可以不指定,不指定时默认会使用实体的类名来进行数据的操作, 因此建议使用简洁的别名。通过EntityManager使用时,需指定操作的实体类,且必须指定别名。
createQueryBuilder支持增删改查四种操作,最常用是查询操作,下面就几种查询场景进行介绍。
多表联查
TypeORM官方文档中,实体关系实际上是通过mysql的外键实现的,先在entity实体代码上添加关系,再使用leftJoinAndSelect等进行关联查询。
代码语言:ts复制@Entity()
class User {
@PrimaryGeneratedColumn()
id: number;
// ...其他属性...
@OneToMany(() => Photo, photo => photo.user)
photos: Photo[];
}
@Entity()
class Photo {
@PrimaryGeneratedColumn()
id: number;
// ...其他属性...
@ManyToOne(() => User, user => user.photos)
user: User;
}
此时可以使用createQueryBuilder来进行联查
代码语言:ts复制const users = await this.userRepository
.createQueryBuilder('user')
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getMany()
得到的数据结构如下所示,photo表的内容作为user的photos属性,这样也直接体现了一对多的关系。
代码语言:ts复制[
{
id: 1,
firstName: 'e',
lastName: 'm',
isActive: true,
photos: [
{
id: 1,
isRemoved: false
},
{
id: 2,
isRemoved: false
}
]
}
];
但在实际开发中,外键因为有诸多限制不被推荐使用,因此实体关系等应该在应用层解决,可以使用以下方法,达到和外键相同的效果。photo和user是多对一,单个photo来看都会有对应一个user,因此可通过user表的内部id来做关联,
代码语言:ts复制@Entity()
class User {
@PrimaryGeneratedColumn()
id: number;
// ...其他属性...
}
@Entity()
class Photo {
@PrimaryGeneratedColumn() id: number;
// ...其他属性...
@Column()
userId: number;
}
在进行查询时,通过指明两表中的数据关系来进行联查,通过leftJoinAndMapMany来将数据映射为user的虚拟属性photos中。
代码语言:ts复制const users = await this.userRepository
.createQueryBuilder('user')
.leftJoinAndMapMany(
"user.photos",
"photo",
"photo",
'photo.userId = user.id'
)
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getMany()
可获得和leftJoinAndSelect一致的效果。
代码语言:ts复制[
{
"id": 1,
"firstName": "e",
"lastName": "m",
"isActive": true,
"photos": [
{
"id": 1,
"isRemoved": false,
"userId": 1
},
{
"id": 2,
"isRemoved": false,
"userId": 1
}
]
}
]
分组聚合
createQueryBuilder支持分组聚合,来满足一些业务场景。 比如将订单按用户分组,并找出订单数超过2的用户
代码语言:ts复制const res = await this.orderRepository
.createQueryBuilder('order')
.select('order.customerId', 'customerId')
.addSelect('COUNT(order.id)', 'orderCount')
.groupBy('order.customerId')
.having('orderCount > 2')
.getRawMany();
子查询
子查询可以用于多种情况,比如在SELECT语句中、WHERE条件中或者FROM子句中,通过createQueryBuilder结合回调函数或subQuery()方法来实现。以下分别做示例。
在SELECT中使用子查询,查询用户及其最新照片。
代码语言:ts复制const res = await this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect(subQuery => {
return subQuery
.from(Photo, 'photo')
.addSelect('photo.userId','userId')
.addSelect('MAX(photo.createdAt)', 'latest')
.groupBy('photo.userId');
},
'latest_photo',
'latest_photo.userId = user.id'
).getRawMany();
在WHERE中使用子查询,查询有超过10张照片的用户
代码语言:ts复制const res = await this.usersRepository
.createQueryBuilder('user')
.where((qb) => {
const subQuery = qb
.subQuery()
.from(Photo, 'photo')
.select('photo.userId')
.groupBy('photo.userId')
.having('COUNT(photo.id) > :photoCount', { photoCount: 10 })
.getQuery();
return 'user.id IN ' subQuery;
})
.getMany();
在FROM中使用子查询,构建一个新的表并获取里面的内容,展示每个用户的照片数量
代码语言:ts复制const res = await this.entityManager
.createQueryBuilder()
.select('userSummary.userId', 'userId')
.addSelect('userSummary.totalPhotos', 'totalPhotos')
.from((subQuery) => {
return subQuery
.from(User, 'user')
.leftJoinAndSelect(Photo, 'photo', 'photo.userId = user.id')
.select('user.id', 'userId')
.addSelect('COUNT(photo.id)', 'totalPhotos')
.groupBy('user.id');
}, 'userSummary')
.getRawMany();
注意,这里使用 entityManager
而不是 Repository
,这样就不会自动包含User实体表。
参考
- 开始入门 | TypeORM 中文文档
- Database | NestJS - A progressive Node.js framework
- 做个图书借阅系统(2) 数据库设计
- 深入探讨:为何避免使用外键与级联操作