TypeORM 中的 Active Record 与 Data Mapper 两种 ORM 模式选型实战指南

发布时间:2026/9/10 21:20:39
TypeORM 中的 Active Record 与 Data Mapper 两种 ORM 模式选型实战指南 TypeORM 中的 Active Record 与 Data Mapper 两种 ORM 模式选型实战指南【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeormTypeORM 同时支持Active Record活动记录与Data Mapper数据映射器两种持久化模式前者把增删改查方法直接挂载到实体类上让模型“自带数据库访问能力”后者把实体与数据访问彻底分离所有数据库操作收敛到 Repository 层。本指南围绕 guides/1-active-record-data-mapper.md 的系统讲解结合 TypeORM 源码如 src/repository/BaseEntity.ts的实现细节帮助你完整掌握两种模式的写法、底层工作原理、各自的优劣边界以及在项目中如何做出正确选型。Active Record 模式把数据访问放进模型本身Active Record 是一种“在模型内部访问数据库”的模式。使用它时查询方法定义在实体类中而对象的保存save、删除remove与加载find也通过实体自身携带的方法完成。在 TypeORM 中所有 Active Record 实体必须继承BaseEntity类——正是这个父类向实体注入了全套数据访问能力。import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from typeorm Entity() export class User extends BaseEntity { PrimaryGeneratedColumn() id: number Column() firstName: string Column() lastName: string Column() isActive: boolean }定义实体后实例方法与静态方法即可直接操作数据库// 保存一条 Active Record 实体 const user new User() user.firstName Timber user.lastName Saw user.isActive true await user.save() // 删除该实体 await user.remove() // 加载实体集合 const users await User.find({ skip: 2, take: 5 }) const newUsers await User.findBy({ isActive: true }) const timber await User.findOneBy({ firstName: Timber, lastName: Saw })自定义业务查询在实体上编写静态方法当需要一个“按姓名查找用户”这类可复用查询时可直接将其实现为实体上的静态方法方法内部借助createQueryBuilder或标准查询 API 完成import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from typeorm Entity() export class User extends BaseEntity { PrimaryGeneratedColumn() id: number Column() firstName: string Column() lastName: string Column() isActive: boolean static findByName(firstName: string, lastName: string) { return this.createQueryBuilder(user) .where(user.firstName :firstName, { firstName }) .andWhere(user.lastName :lastName, { lastName }) .getMany() } }调用方式与其它实例/静态方法完全一致const timber await User.findByName(Timber, Saw)BaseEntity 到底提供了哪些能力源码解读BaseEntity位于 src/repository/BaseEntity.ts其设计目标是“几乎完整复刻标准Repository的对外 API”。实例方法部分L43-L105包括save(options?)若实体在数据库中不存在则插入否则更新L54-L57remove(options?)从数据库删除当前实体L64-L67softRemove(options?)软删除仅记录删除日期而非物理删除L74-L77recover(options?)恢复被软删除的实体L84-L87reload()从数据库重新加载实体数据并覆盖当前对象属性L92-L105hasId()检查实体是否已具备可能为复合的主键L43-L46。而静态方法则几乎与Repository一一对应包括find / findBy / findAndCount / findAndCountBy / findOne / findOneBy / findOneOrFail / findOneByOrFail、save / remove / softRemove / recover / insert / update / upsert / delete / clear、count / countBy、聚合函数sum / average / minimum / maximum、exists / existsBy以及create / merge / preload / query等还额外暴露了createQueryBuilderL168-L173用于自由拼装 SQL。可以看出绝大多数场景下Active Record 实体无需再显式接触Repository或EntityManager这也印证了文档中“BaseEntity 具备标准 Repository 的大部分方法”的说法。useDataSource 机制Active Record 的数据源从何而来静态方法能够工作前提是BaseEntity已被绑定到一个已初始化的DataSource。其底层通过static useDataSource(dataSource)L116-L118保存数据源引用再由getRepository静态方法L123-L130转发到数据源对应的 Repository 上若尚未设置会抛出 “DataSource is not set for this entity.” 错误。关键的自动绑定发生在DataSource.initialize()过程中初始化时会构建全部实体元数据并对继承自BaseEntity的实体目标逐一调用target.useDataSource(this)见 src/data-source/DataSource.ts L758-L765。也就是说只要实体通过entities配置注册进 DataSource 并被成功initialize其实体上的静态数据访问方法即可直接使用。仓库测试 test/functional/base-entity/base-entity.test.ts 专门验证了这一链路测试先调用User.useDataSource(null)清空绑定再创建 DataSource 并initialize()随后User.save(...)、User.findOneByOrFail(...)均能正常工作——证明绑定动作确实由 DataSource 初始化流程自动完成而非依赖用户手工设置。Data Mapper 模式把数据访问收进 RepositoryData Mapper 模式则相反所有查询方法定义在独立的“Repository仓库”类中实体的保存、删除与加载全部经由仓库对象完成。此时实体非常“笨”只负责声明属性最多附带一些无副作用的辅助方法。import { Entity, PrimaryGeneratedColumn, Column } from typeorm Entity() export class User { PrimaryGeneratedColumn() id: number Column() firstName: string Column() lastName: string Column() isActive: boolean }对应的数据访问统一通过dataSource.getRepository(User)获取的仓库实例执行const userRepository dataSource.getRepository(User) // 保存一条 Data Mapper 实体 const user new User() user.firstName Timber user.lastName Saw user.isActive true await userRepository.save(user) // 删除该实体 await userRepository.remove(user) // 加载实体集合 const users await userRepository.find({ skip: 2, take: 5 }) const newUsers await userRepository.findBy({ isActive: true }) const timber await userRepository.findOneBy({ firstName: Timber, lastName: Saw, })在 TypeORM 中DataSource.getRepository(target)最终委托给内部EntityManager的同名方法见 src/data-source/DataSource.ts L440-L444因此全局 dataSource、dataSource.manager与仓库之间共享同一套元数据与连接体系。另外若使用 MongoDB可改用getMongoRepository获取 Mongo 专用仓库。扩展标准 Repository自定义仓库模式Data Mapper 并不要求写样板胶水代码。当需要为UserRepository增加findByName(firstName, lastName)这样的自定义方法时可以结合custom repository自定义仓库模式完成详细用法见 working-with-entity-manager/4-custom-repository.md。最常见也最简洁的做法是把仓库实例导出为全局单例并在其上调用.extend()注入自定义方法// user.repository.ts export const UserRepository dataSource.getRepository(User).extend({ findByName(firstName: string, lastName: string) { return this.createQueryBuilder(user) .where(user.firstName :firstName, { firstName }) .andWhere(user.lastName :lastName, { lastName }) .getMany() }, }) // user.controller.ts export class UserController { users() { return UserRepository.findByName(Timber, Saw) } }从源码看Repository.extend()通过生成一个继承当前仓库类的子类并把自定义方法写入其原型实现src/repository/Repository.ts L815-L836因此方法内this仍是完整的仓库实例可继续访问createQueryBuilder等全部内建能力最终返回的是功能完备的仓库对象。需要注意事务边界事务拥有自己独立的 queryRunner、EntityManager 与仓库实例事务内必须使用事务回调提供的 manager并通过manager.withRepository(...)获得绑定到该事务的自定义仓库否则查询不会在事务作用域内执行await dataSource.transaction(async (manager) { // 事务内必须使用回调提供的 manager不能用全局 EntityManager/Repository const userRepository manager.withRepository(UserRepository) await userRepository.createAndSave(Timber, Saw) const timber await userRepository.findByName(Timber, Saw) })两种模式的 API 对照与等效替换两种模式在语法层面几乎一一对应理解这种映射关系有助于在项目内自由切换或统一团队风格操作语义Active Record继承 BaseEntityData MapperRepository保存单条/多条user.save()/User.save([...])repo.save(user)/repo.save([...])物理删除user.remove()repo.remove(user)软删除user.softRemove()repo.softRemove(user)批量查询分页等 Find 选项User.find({ skip, take })repo.find({ skip, take })纯条件查询User.findBy({ isActive: true })repo.findBy({ isActive: true })单条条件查询User.findOneBy({...})repo.findOneBy({...})自定义 SQLUser.createQueryBuilder(user)repo.createQueryBuilder(user)新增自定义方法实体上的static方法通过repo.extend({...})的 custom repository底层执行引擎由useDataSource绑定的仓库由getRepository返回的仓库实例到底该选哪一种可维护性与应用规模的权衡两种模式没有绝对的对错选择权最终在你自己手里。TypeORM 文档建议把“我们未来将如何长期维护这套应用”作为首要决策依据Data Mapper 更利于可维护性更适合大型应用。实体保持纯数据定义业务查询集中在仓库层遵循“单一职责”与“关注点分离”当团队规模、实体数量与领域逻辑增长时实体类不会逐渐膨胀为“上帝对象”测试也更容易针对仓库单独进行替换或隔离。文档原话即指出Data Mapper 方式对可维护性更有帮助在更大的应用中效果更好“more effective in larger apps”。Active Record 让一切保持简单适合中小型应用。无需在实体与仓库之间来回跳转增删改查就近写在模型上代码量最少、上手门槛最低。文档原话亦指出Active Record 方式有助于保持简单在小应用中表现出色“works well in smaller apps”。实践中还常看到第三种混合用法实体仍保持纯 Data Mapper 形态但通过全局导出的UserRepository dataSource.getRepository(User).extend(...)单例保持调用时的简洁性。无论最终倾向哪种TypeORM 的底层机制决定了它们最终都收敛到同一套 EntityManager/Repository 执行管线切换成本并不高——关键是先明确团队规模与长期维护策略再统一约定避免同一代码库内两种风格混杂导致认知负担。小结本文从 guides/1-active-record-data-mapper.md 出发系统梳理了 TypeORM 的两种持久化模式Active Record实体继承 BaseEntity把保存/删除/查询与自定义业务方法直接放在模型中适合追求简洁的中小型应用Data Mapper实体只声明属性所有数据库操作经由getRepository返回的 Repository自定义方法可用.extend()职责清晰、可维护性好适合规模化应用二者底层都由 DataSource 构建元数据后统一分发到 EntityManager/Repository 执行绑定流程见 DataSource.ts L758-L765测试佐证实际是“同一引擎的两种外观”。选型没有标准答案但维护成本是恒定标尺小项目追求简单选 Active Record大项目追求边界清晰选 Data Mapper。进一步学习自定义仓库与事务内使用仓库的细节可继续阅读 custom repository 指南。【免费下载链接】typeormTypeScript JavaScript ORM for Node.js — supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and more.项目地址: https://gitcode.com/GitHub_Trending/ty/typeorm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考