TypeORM用法浅析

2024-05-13 16:21:38 浏览数 (2)

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查询
代码语言:ts复制
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实体表。

参考

  1. 开始入门 | TypeORM 中文文档
  2. Database | NestJS - A progressive Node.js framework
  3. 做个图书借阅系统(2) 数据库设计
  4. 深入探讨:为何避免使用外键与级联操作

0 人点赞