TypeORM 知多少?来看看 Node 服务端如何基于 TypeORM 封装 BaseService

647 阅读8分钟

前言

大家好!我是 嘟老板。目前 ZimuAdmin 服务端集成了 TypeORM 框架,用以处理数据库相关操作。今天我们来聊聊 TypeORM 比较常用的数据库操作方法以及项目中的实际应用场景。

ZimuAdmin 服务端基于以下技术构建:

阅读本文您将收获:

  1. 了解 TypeORM 常用 api 介绍及使用方法。
  2. 了解 ZimuAdmin BaseService 类封装思路、具体实现及应用。

TypeORM 存储库(Repository)

TypeORM 提供两种操作实体数据的方式:

  • EntityManager:管理所有实体,相当于实体存储库的集合。
  • Repository:某个特定实体的存储库。

本文内容内容重点关注 Repository 的介绍及应用。

Repository 可以对指定实体数据进行管理,比如 查询新增修改删除 等操作。

通过 DataSource 实例获取 Repository 对象。

import db from '../tools/data-source'
import { User } from '../entities/user.entity'

const repository = db.getRepository(User)

以上代码摘自 ZimuAdmin,引入的 db 就是 DataSource 实例,可理解为 new DataSource({ // 配置项... })

常用 api

TypeORM Repository 为我们提供了丰富的实用 api,方便调用。

查询类

  • find:获取匹配查询条件(Find Options)的数据列表。
const users = await repository.find({ where: { username: "dulaoban" } })
  • findBy:作用与 find 相同,参数是 Find Optionswhere 条件对象。
const users = await repository.findBy({ username: "dulaoban" })
  • exists:检查否存在匹配查询条件(Find Options)的数据。
const exists = await repository.exists({ where: { username: "dulaoban" } })
  • existsBy:作用与 exists 相同,参数是 Find Optionswhere 条件对象。
const exists = await repository.existsBy({ username: "dulaoban" })
  • count:获取匹配查询条件(Find Options) 的数据量。
const count = await repository.count({ where: { username: "dulaoban" } })
  • countBy:作用与 count 相同,参数是 Find Optionswhere 条件对象。
const exists = await repository.countBy({ username: "dulaoban" })
  • findAndCount:获取匹配查询条件(Find Options)的 数据列表数据总量,其中 数据总量 的计算会忽略分页相关配置(skiptake)。
const [users, count] = await repository.findAndCount({ where: { username: "dulaoban" }, skip: 10, take: 10 })
  • findAndCountBy:作用与 findAndCount 相同,参数是 Find Optionswhere 条件对象。
const [users, count] = await repository.findAndCountBy({ username: "dulaoban" })
  • findOne:获取匹配查询条件(Find Options)的第一条数据。
const user = await repository.findOne({ where: { username: "dulaoban" } })
  • findOneBy:作用与 findOne 相同,参数是 Find Optionswhere 条件对象。
const user = await repository.findOneBy({ username: "dulaoban" })
  • query:直接执行 SQL 语句。
const users = await repository.query(`SELECT * FROM USERS`)

对象处理类

  • create:创建实体类的实例对象,接收一个可选的对象参数,参数属性与实体类属性一致。若传递参数,参数的属性值会写入生成的实例对象。
// 等价于 const user = new User(); 
const user = repository.create()
// 等价于 const user = new User(); user.firstName = "Timber"; user.lastName = "Saw";
const user = repository.create({ id: 1, username: "dulaoban" }) 
  • merge:将多个实体对象合并为一个。
const user = new User()

// 等价于 user.username = "dulaoban"; user.age = 18;
repository.merge(user, { username: "dulaoban" }, { age: 18 })
  • preload:基于参数对象,生成新的实体对象。参数对象必须包含 主键,若该主键在数据库中存在数据,则获取该实体数据,并将参数对象中的所有值覆盖到该实体对象上。
const partialUser = { id: 1, username: "dulaoban", age: 18 } 

// 假设数据库中存在对应用户数据 { id: 1, username: "zhagnsan", sex: "M", age: 18 }
// user 对象:{ id: 1, username: "dulaoban", sex: "M", age: 18 } 
const user = await repository.preload(partialUser)

数据操作类

  • save:保存参数传递的实体数据到数据库,并返回保存的实体对象或实体数组。若待保存实体在数据库中已存在,则更新,否则新增一条数据。支持部分更新,所有值为 undefined 的属性都会被跳过。

    参数实体对象/实体对象数组

    await repository.save(user) 
    await repository.save([user1, user2, user3])
    
  • insert:数据库插入实体数据。

    参数实体对象 / 实体对象数组

    await repository.insert(user) 
    await repository.insert([user1, user2, user3])
    
  • update:更新数据库中符合条件的实体数据。

    参数:1. 条件对象,用以筛选需要更新的数据。2. 实体对象,用以更新数据库实体。

    // UPDATE user SET category = ADULT WHERE age = 18
    await repository.update({ age: 18 }, { category: "ADULT" })
    
    // UPDATE user SET username = dulaoban WHERE id = 1
    await repository.update(1, { username: "dulaoban" })
    
  • delete:删除数据库中符合条件的实体数据。

    参数id / id 数组 / 条件对象

    // DELETE from user WHERE id = 1
    await repository.delete(1)
    // DELETE from user WHERE id in (1, 2, 3)
    await repository.delete([1, 2, 3])
    // DELETE from user WHERE username = dulaoban
    await repository.delete({ username: "dulaoban" })
    
  • softDelete:软删除数据库指定 id 对应的实体数据。

    参数id

    await repository.softDelete(1)
    
  • restore:重新保存 softDelete 软删除的数据。

    参数id

    await repository.restore(1)
    
  • softRemove:功能与 softDelete 相同。

    参数:待删除的实体数组。

    await repository.softRemove([user1, user2])
    
  • recover:重新保存 softRemove 软删除的数据。

    参数:待保存的实体数组。

    await repository.recover([user1, user2])
    

自定义 Repository

Repository 提供的 api 无法满足需求场景,TypeORM 允许我们创建自定义 Repository,以扩展 Repository 能力。

可以通过 Repository 实例方法 extend 扩展自定义功能,比如为 UserRepository 添加 queryByUsername 方法,用来根据 username 查询实体数据。

过程如下:

  1. 创建 Repository 并导出

如新增 user.responsitory.ts

import db from '../tools/data-source'
import { User } from '../entities/user.entity'

export const UserRepository = db.getRepository(User).extend({ 
    queryByUsername(username: string) { 
        return this.createQueryBuilder("user")
        .where("user.username = :username", { username })
        .getMany()
    }
})
  1. 引用 UserRepository 并调用 queryByUsername

如在 UserController 中使用:

import { UserRepository } from './user.responsitory'

export class UserController { 
    queryByUsername() { 
        return UserRepository.queryByUsername("dulaoban") 
    } 
}

BaseService

BaseService 定位是基础服务类,用于封装基础的,通用的业务数据处理过程,其他业务类服务,如 UserService,都可继承 BaseService

若非特殊场景,业务类服务不需要重复编写 BaseService 中已有的通用函数;若需求特殊,业务类服务可自行编写,覆盖通用函数。

实现

BaseService 中包含以下内容:

  • 全列表查询 - queryList
  • 分页列表查询 - queryByPage
  • 详情查询 - queryById
  • 新增记录 - insert
  • 修改记录 - update
  • 删除记录 - delete
  • 软删除记录 - softDelete

services 目录下新建 base-service.ts 文件,来编写 BaseService 代码:

  1. 创建并导出 BaseService 类,构造函数结构一个实体类,用来获取对应的实体存储库对象,以执行数据操作。
import db from '../tools/data-source'

export class BaseService {
  repository

  constructor(entityClass: any) {
    this.repository = db.getRepository(entityClass)
  }
}
  1. 全列表查询 - queryList,调用 repository.findAndCountBy api,获取满足条件的所有数据及总数。
async queryList(params?: any) {
    const [rows, total] = await this.repository.findAndCountBy(params)
    return {
      rows,
      total
    }
}
  1. 分页列表查询 - queryByPage,通过 repository.countBy api 获取满足条件的数据总量,然后调用 repository.find api 获取满足条件及分页要求的数据列表。

此处借助 skiptake 配置项实现分页需求。

async queryByPage(
    params: any = {},
    pageSize: number = 10,
    pageNum: number = 1
  ) {
    // 总数
    const total = await this.repository.countBy(params)

    // 数据列表
    const skip = pageSize * (pageNum - 1)
    const rows = await this.repository.find({
      where: params,
      skip,
      take: pageSize
    })

    return {
      total,
      pageSize,
      pageNum,
      rows
    }
}
  1. 详情查询 - queryById,调用 repository.findOneBy 通过主键 id 查询实体明细数据。
async queryById(id: string) {
    return await this.repository.findOneBy({
      id
    })
}
  1. 新增记录 - insert,调用 repository.insert 将客户端传递的实体数据新增到数据库。
async insert(entity: any) {
    if (!entity) {
      return Promise.reject(new Error('不存在待插入的实体对象'))
    }
    const { identifiers } = await this.repository.insert(entity)
    // 获取主键
    const id = identifiers[0].id
    if (!id) {
      return Promise.reject(new Error('保存失败, repository.insert 未生成 id'))
    }

    return await this.queryById(id)
}
  1. 修改记录 - update,调用 repository.update 将客户端传递的实体数据更新到数据库。
async update(entity: any = {}) {
    if (!entity.id) {
      return Promise.reject(new Error('id 不存在'))
    }
    return await this.repository.update(entity, { id: entity.id })
}
  1. 删除记录 - delete,调用 repository.delete 删除指定 id 对应的数据库数据。
async delete(id: string) {
    if (!id) {
      return Promise.reject(new Error('id 不存在'))
    }
    return await this.repository.delete(id)
}
  1. 软删除记录 - softDelete,即逻辑删除,调用 repository.softDelete 为指定 id 对应的数据库数据打上删除标识。
async softDelete(id: string) {
    if (!id) {
      return Promise.reject(new Error('id 不存在'))
    }
    return await this.repository.softDelete(id)
}

目前 BaseService 仅涵盖基础的数据操作逻辑,后续会随着业务模块的深入应用,逐步完善。

应用

现在我们在用户模块应用一下。

services/user-service.ts 文件中包含用户模块的业务处理逻辑,我们先简单声明一下用户服务类:

/**
 * 用户服务类
 */
import { User } from '../entities/user.entity'
import { BaseService } from './base-service'

export class UserService extends BaseService {
  constructor() {
    super(User)
  }
}

因为 UserService 继承了 BaseService,所以 UserService 的实例可用 BaseService 的实例函数。

然后在用户模块控制器文件 controllers/user-controller.ts 中,编写业务接口,为节约篇幅,我们仅实现 插入全列表查询 接口。

/**
 * 用户 controller
 */
import { Controller, Get, Post, Body } from 'routing-controllers'
import { UserService } from '../services/user.service'

@Controller('/users')
export class UserController {
  service = new UserService()

  @Get('/list')
  async queryList() {
    return this.service.queryList()
  }

  @Post('/insert')
  async insert(@Body() body: any) {
    return this.service.insert(body)
  }
}

测试

用户模块接口已搞定,我们使用 Postman 工具来测试一下。

  1. 调用 insert 接口向数据库插入一条用户数据:

image.png

接口调用成功,查看数据库如下:

image.png

zhangsan 的用户数据已成功插入。

  1. 调用 list 接口查询用户数据列表:

image.png

OK,用户数据列表获取正常。

结语

本文重点介绍了 TypeORM 存储库(Repository) 相关内容及常用方法,并结合 ZimuAdmin 中的实际应用场景,旨在帮助同学们加深对于 TypeORM 实现数据操作的应用理解。希望对您有所帮助!相关代码已上传至 GitHub,欢迎 star

如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐