如果你是第一次接触 依赖注入(Dependency Injection) 或 NestJS,感觉概念比较抽象、难以理解是很正常的。下面我会用更通俗、分步骤的方式,来帮助你理清 NestJS DI 容器和 TypeORM 的集成原理。希望能让你对"为什么要这样做"和"怎么做"有更直观的认识。
1. 为什么要用依赖注入(DI)?
1.1 传统做法(手动 new 对象)
在没有 DI 的情况下,如果一个类 A
需要用到类 B
,我们通常会在 A
里面手动写 const b = new B()
。问题是:
- 如果
B
还依赖C
,那就要在B
里写new C()
。层层嵌套会变得很乱。 - 想测试
A
的时候,如果想换个"假"B
(比如一个 mock),就要改代码,把new B()
换成new FakeB()
。
这样会导致耦合度很高,代码难以维护和测试。
1.2 有了 DI 的好处
- 解耦 :在类
A
中只需要"声明"自己需要B
,而不需要手动创建B
。框架(NestJS)会帮我们找来B
的实例。 - 易测 :测试时,我们可以给
A
注入一个假的B
(mock),而不用改A
的代码。 - 灵活 :一旦想换成另一个实现,只需要在配置或模块里改一下提供者,就能把
B
换成B2
。
2. NestJS DI 容器做了什么?
你可以把 NestJS DI 容器 想象成一个"大管家":
- 扫描 :NestJS 启动时,会扫描所有标记了
@Injectable()
、@Controller()
等装饰器的类,把它们当做可以被"管理"的对象(Provider)。 - 登记:管家会记下"这些类是 Provider,分别叫什么名字(令牌/Token),需要用什么东西来创建"。
- 检查依赖 :如果某个 Provider(比如
A
)的构造函数里写了constructor(private b: B) {}
, 管家就知道A
需要B
。 - 实例化 :管家会先去创建(或获取)
B
的实例,然后再创建A
的实例,并把B
塞给A
。 - 缓存 :默认情况下,NestJS 会把创建好的对象缓存起来(单例模式)。后面要用到
A
的地方,直接复用已经创建好的A
。
一个极简的例子
typescript
@Injectable()
class B {
sayHello() {
return 'Hello from B';
}
}
@Injectable()
class A {
constructor(private b: B) {}
greet() {
return this.b.sayHello();
}
}
- 当 NestJS 看到
A
需要B
时,就会先创建B
,然后创建A
,并把B
的实例注入到A
里面。 - 于是
A.greet()
就能调用B.sayHello()
了,但我们自己从来没有写new B()
。
3. 那 TypeORM 又是怎么回事?
3.1 TypeORM 和 NestJS 的集成
- TypeORM 是一个数据库操作库,可以帮我们用面向对象的方式(Entity/Repository)来增删改查数据。
- NestJS 提供了一个官方模块
@nestjs/typeorm
,把 TypeORM 的"连接"、"实体(Entity)"、"仓库(Repository)"也当做 Provider 注册进了 NestJS 的 DI 容器。
3.2 Repository 是怎样被注入的?
-
你在某个模块里写
TypeOrmModule.forFeature([User])
,NestJS 就会为User
这个实体创建一个UserRepository
Provider。 -
当你的服务(Service)中需要用到
UserRepository
时,你只要写:typescript@Injectable() export class UserService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} findAll() { return this.userRepository.find(); } }
- NestJS 看到
@InjectRepository(User)
,就知道要注入对应的UserRepository
。 - 因为在同一个模块(或导入的模块)里,
TypeOrmModule.forFeature([User])
已经把这个仓库注册到 DI 容器了。 - 最终,NestJS 会把已经创建好的
UserRepository
对象塞到userRepository
这个参数里。
- NestJS 看到
3.3 为什么要把 Repository 也放进 DI 容器?
- 这样一来,Service 只需要"声明"需要哪个 Repository,而不用自己写
new Repository()
. - 另外,如果想在测试中换个假的 Repository,也很容易,只要在测试模块里提供一个"假仓库"就行了。
4. 结合起来:一步一步发生了什么?
- NestJS 启动
- 读取所有模块和类,发现哪些类用了
@Injectable()
、哪些实体放进了TypeOrmModule.forFeature(...)
。
- 读取所有模块和类,发现哪些类用了
- 注册 Provider
- DI 容器把每个"可注入"的类和 Repository 都当做一个 Provider 记录下来。
- 分析依赖
- 哪些服务需要哪些 Repository?哪些控制器需要哪些服务?
- 实例化
- 从底层依赖开始往上实例化。
- 先创建数据库连接 -> 创建 Repository -> 创建 Service -> 注入到 Controller。
- 缓存并复用
- 全部默认单例,如果别的地方也需要同一个 Service,就直接拿缓存好的实例。
- 响应请求
- 当请求进来时,Controller 被调用 -> 调用 Service -> Service 调用 Repository -> 操作数据库 -> 返回结果。
- 全过程里,你都没写
new Service()
或new Repository()
;都是由 NestJS 的 DI 容器自动完成。
5. 我还是觉得抽象,能再直观一点吗?
5.1 "大管家" 比喻
- 管家手里有个"字典":记下"某个名称" -> "对应的类" -> "怎么创建它"。
- 有人来问管家:"我要一个 UserService 的对象"。
- 管家先看字典:UserService 需要 UserRepository,对吗?那就去看"UserRepository"的条目;UserRepository 需要数据库连接,对吗?那就先把数据库连接弄好,再生成一个 UserRepository。
- 有了 UserRepository 后,就能生成 UserService。
- 生成好后管家会说:"以后谁要 UserService,就直接拿这份现成的。"
5.2 如果要测试?
- 你可以告诉管家:"在测试环境里,不要用真的 UserRepository,用一个假的 FakeUserRepository"。只要管家"字典"里的映射改了,就可以把假对象注入到 Service 中,测试起来就很方便。
6. 需要多少预备知识?
- TypeScript 装饰器和反射
- 了解
@Injectable()
、@Controller()
、@InjectRepository()
等装饰器的作用原理。 - 了解
reflect-metadata
在 NestJS 中是如何用来读取构造函数的参数类型的。
- 了解
- 模块化概念
- 明白 NestJS 中"模块"(Module)是怎么把一堆相关的 Provider(服务、控制器、仓库)组织到一起的。
- 面向对象和设计模式
- 依赖注入(DI)本质是"控制反转(IoC)"的一种实现,需要对面向对象、单例模式等有些基本理解。
如果这些概念还很陌生,建议先花点时间熟悉一下 面向对象编程 、DI / IoC 、TypeScript 装饰器 等,这样再回过头看 NestJS 的依赖注入,就会豁然开朗。
结语
- DI 容器 = 一个专门管理对象实例创建和依赖关系的"管家"。
- TypeORM 集成 = NestJS 把 TypeORM 的仓库也纳入管家管理,通过
TypeOrmModule.forFeature([Entity])
注册,之后任何服务想用仓库,只要声明依赖就行。 - 理解关键 :你不用再写
new
去手动创建对象,NestJS 帮你干了这件事;而你只要专注于"我需要什么"------在构造函数里写上即可。
如果现在还是有点懵,不用急,这些概念最初确实比较抽象,多写几个示例、动手调试看看,就会慢慢明白"哦,原来 NestJS 启动的时候就已经替我把那些对象都创建好了,我只管在代码里声明需求就行。" 这是依赖注入带来的巨大好处,也是 NestJS 之所以容易扩展、易测试、结构清晰的根本原因。