前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TypeORM用法浅析

TypeORM用法浅析

原创
作者头像
发布2024-05-13 16:21:38
1471
发布2024-05-13 16:21:38

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. 深入探讨:为何避免使用外键与级联操作

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 2. Repository
    • 注入
      • insert
        • save
          • find
            • 其他
            • 3. EntityManager
              • transaction
                • createQueryBuilder
                  • 多表联查
                    • 分组聚合
                      • 子查询
                      • 参考
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档