TypeORM 知多少? 关于 Active Record/Data Mapper 模式那点事儿

194 阅读6分钟

前言

大家好!我是 嘟老板ZimuAdmin 集成了 TypeORM 框架,以更便捷的处理持久数据存储。TypeORM 针对不同的应用场景,为我们提供了两种开发模式 - Active RecordData Mapper,今天我们就来聊聊两者的差异及 ZimuAdmin 是如何选择的。

ZimuAdmin 项目基于以下技术构建:

阅读本文您将收获:

  1. 了解 Active Record/Data Mapper 两种模式的特点及应用范例。
  2. 了解如何选择合适的开发模式。

Active Record

Active Record 模式可直译为 活动记录 模式,是一种在实体模型内部访问数据库的架构模式,允许我们将所有的操作都写在实体类中,如保存、更新、删除等,通过实体类函数方式调用。

使用 Active Record 模式的实体类必须继承 TypeORM 提供的 BaseEntity 类,以使用 BaseEntity 中提供的数据库操作函数。

标准操作函数

标准操作是指 TypeORM 框架提供的操作,如 saveremovefind 等。

直接上代码,定义个 User 实体类:

import { BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm'

export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number

  @Column()
  username!: string

  @Column()
  sex!: string

  @Column()
  isActive!: boolean
}

后续数据保存、删除等操作,通过以下方式调用:

async function operateWithAR() {
  // 创建 UserEntity 实例,并赋值
  const user = new User()
  user.username = 'dulaoban'
  user.sex = 'Male'
  user.isActive = true

  // 保存
  await user.save()

  // 删除
  await user.remove()

  // 查询列表
  const users = await User.find({ skip: 2, take: 5 })
}

此时实体类已经包含了大部分数据库操作函数,使用 Active Record 模式,基本不需要使用 Repository 和 EntityManager

自定义操作函数

除了标准的数据库操作函数(saveremovefind 等),我们可以在实体类中定义自定义操作函数,以满足多样化的需求。

比如创建一个通过 username 查询用户信息的函数 - queryByUsername

import { BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm'

export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number

  @Column()
  username!: string

  @Column()
  sex!: string

  @Column()
  isActive!: boolean
  
  static queryByUsername(username: string) {
    return this.createQueryBuilder('user')
      .where('user.username = :username', { username })
      .getOne()
  }
}

调用 queryByUsername

const user = await User.queryByUsername('dulaoban')

需要留意一点,queryByUsername静态函数,即属于 User 类,而不是其实例,为什么定义成静态函数呢?主要因为 this 指向 这个经典思考,定义为静态函数,函数内部的 this 指向类的构造函数,就可以访问 BaseEntity 定义的标准处理函数,用以实现定制化的数据处理需求。当然啦,若用不到 BaseEntity 的函数,定义为实例函数也无不可。

Data Mapper

Data Mapper 模式可直译为 数据映射器 模式,用于在持久数据存储(通常是关系数据库)和内存数据表现形式之间进行双向传输。

使用 Data Mapper 模式,需要在存储库中定义数据操作方法,并且通过存储库对象调用执行,如保存、更新、删除等;而 实体类 则仅仅用来定义实体对象结构,如实体属性。

标准操作函数

还是定义 User 实体类:

import { PrimaryGeneratedColumn, Column } from 'typeorm'

export class User {
  @PrimaryGeneratedColumn()
  id!: number

  @Column()
  username!: string

  @Column()
  sex!: string

  @Column()
  isActive!: boolean
}

Active Record 不同之处在于,Data Mapper 模式不需要继承 BaseEntity 类,因为该模式下,不用实体类做太多的事情,只要定义好实体对象就 OK 了。

后续数据操作要通过存储库 (Repository) 对象执行:

不了解 Repository 的同学,可点击阅读 《TypeORM 知多少?来看看 node 服务端如何基于 typeORM 封装 BaseService》

async function operateWithDM() {
  // 获取存储库对象
  const userRepository = dataSource.getRepository(UserEntity)
  // 创建 UserEntity 实例,并赋值
  const user = new User()
  user.username = 'dulaoban'
  user.sex = 'Male'
  user.isActive = true

  // 保存
  await userRepository.save(user)

  // 删除
  await userRepository.remove(user)

  // 查询列表
  const users = await userRepository.find({ skip: 2, take: 5 })
}

自定义操作函数

Repository 对象提供了 extend 方法,允许我们扩展存储库对象,开发自定义功能。还是以 queryByUsername 函数为例:

export const userRepository = dataSource.getRepository(User).extend({ 
    queryByUsername(username: string) { 
        return this.createQueryBuilder("user")
        .where("user.username = :username", { username })
        .getOne()
    }
})

以上代码在 userRepository 上扩展了 queryByUsername 函数,后续可通过 userRepository 直接调用:

const user = await userRepository.queryByUsername('dulaoban')

更多细节在 《TypeORM 知多少?来看看 node 服务端如何基于 typeORM 封装 BaseService》有介绍,就不过多赘述了,感兴趣的小伙伴可点击阅读。

如何选?

既然存在两种模式,自然就涉及到选择问题。用哪个更好永远是让人头疼的问题。

两种模式各有优劣,但都能实现业务需求,能力方面大可放心。更需要关注的点是 架构风格项目规模

抛开编码方式的差异不谈,Active Record 模式主打一个简单,快速,一切逻辑都可依靠实体类搞定,在实体类内可以定义任何你想要的处理函数,比较适合业务逻辑相对简单的 小项目Data Mapper 模式中实体类回归本质,仅用于实体相关属性的定义,加入独立的数据映射器,甚至需要设计独立的服务层,专门处理业务逻辑,将数据处理与实例类定义解耦,更适合业务复杂的 大项目

ZimuAdmin 采用了 Data Mapper 模式,个人认为 Data Mapper 模式扩展性更强且易维护。

结语

本文重点介绍了 TypeORM 的两种开发模式 - Active RecordData Mapper,包括各自的编码规则,扩展方式及择优思路,旨在帮助同学们加深对于这两种模式的应用理解。希望对您有所帮助。

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

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


往期推荐