剛開始在 NestJS 中整合 TypeORM 時,我們通常會先完成資料庫連線,接著建立第一個實體(Entity),並在 Service 中透過 @InjectRepository() 操作資料。結果程式真正存取資料時,TypeORM 卻拋出:
EntityMetadataNotFoundError: No metadata for "User" was found.
明明 User 已經加上 @Entity(),UsersModule 也有正常載入,為什麼 TypeORM 還是說找不到它的 metadata?
這篇就來拆解這個很容易被忽略的 entities[] 設定,以及在 NestJS 中有哪些方式可以避免手動維護實體清單。
先從一個常見的 NestJS + TypeORM 設定開始。
在根模組 app.module.ts 裡建立資料庫連線:
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
synchronize: true,
// 地雷:這裡 entities 為空陣列,也沒有其他載入設定
entities: [],
}),
UsersModule,
],
})
export class AppModule {}
接著定義出 User 實體:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
建立 UsersModule,並透過 forFeature 註冊它:
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
最後,在 UsersService 中透過 @InjectRepository(User) 注入 Repository:
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.userRepository.find();
}
}
為了實際觸發查詢,加上一個 UsersController,透過 GET /users 呼叫 UsersService.findAll():
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
}
UsersModule 有正常載入,UserRepository 也能成功注入,NestJS 啟動時並沒有立即出現錯誤。
但當程式真的執行到 this.userRepository.find();,卻拋出 EntityMetadataNotFoundError。
明明依賴注入(DI)沒有報錯,模組也有掛載,為什麼 TypeORM 就是說找不到 metadata 呢?
這其實是因為 NestJS 的「模組依賴注入」與 TypeORM 的「連線初始化」有著不同的職責界線:
雖然 NestJS 負責幫我們把 Repository 注入到需要的 Service 裡,但對 TypeORM 底層來說,它必須在「建立資料庫連線」的當下,就拿到一份完整的實體清單。
在 NestJS 裡,TypeOrmModule.forRoot() 才是真正負責建立那條首要連線(DataSource)的地方。我們在上面的連線設定中,並沒有在這條主連線上提供「有哪些實體要管」的清單。
雖然你在子模組 users.module.ts 中使用了 TypeOrmModule.forFeature([User]),但這個動作是在目前模組的作用域內註冊可供注入的 User Repository,不代表 User 實體已經被加入 TypeORM 的資料來源設定中。
也就是說,底層的 TypeORM 資料來源並不知道 User 實體的存在,因此當 Service 嘗試透過 Repository 查詢 User 資料時,TypeORM 無法找到對應的實體 metadata 時,便會拋出錯誤。
最直接的做法,是回頭在 forRoot 裡補上 entities 陣列:
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
synchronize: true,
entities: [User], // 把 Entity 明確加進來給連線看
})
這樣 TypeORM 連線初始化時就能成功讀出 User 的 metadata,順利啟動。
⚠️ 注意:
隨著專案成長、需要管理的資料表越來越多,你的根模組會逐漸冒出一長串的import清單。只要每新增一個資料表,開發者都要記得跑回根模組去加上,不僅造成維護困難,也增加了團隊開發時合併衝突的機會。
autoLoadEntities 配置屬性為了解決手動維護的麻煩,NestJS 提供了一個參數:autoLoadEntities: true。
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
synchronize: true,
autoLoadEntities: true, // 自動載入註冊過的實體
})
當你開啟此功能後,NestJS 會將透過 TypeOrmModule.forFeature() 註冊的實體,自動加入 DataSource 的 entities 設定中。
⚠️ 注意:
如果某個實體只是出現在@OneToMany、@ManyToOne等關聯中,但本身從未透過forFeature()註冊,autoLoadEntities並不會因此載入它。官方文件原文:
Note that entities that aren't registered through theforFeature()method, but are only referenced from the entity (via a relationship), won't be included by way of theautoLoadEntitiessetting.
在許多 TypeORM 教學或討論區中,經常可以看到這種寫法:
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
synchronize: true,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
})
這種方式會在執行時掃描符合路徑規則的 .entity.ts 或 .entity.js 檔案,省去逐一匯入實體的麻煩。
不過,它有一個重要限制:實體是否能被載入,取決於實際部署後的檔案結構是否仍符合 Glob 路徑。
⚠️ 注意:
如果專案經過 Webpack 等工具打包,原本分散的.entity.js檔案可能被整併進 bundle,導致原先設定的 Glob 路徑無法再找到實體,進而產生EntityMetadataNotFoundError。
因此,在會進行 bundle 打包的 NestJS 專案中,不建議把 Glob 掃描作為主要的實體載入方式。
forFeature() 會怎樣?即使 DataSource 已經能正確載入實體,Feature Module 這一側仍然可能遇到另一個常見問題:忘記呼叫 TypeOrmModule.forFeature()。
例如,你已經在根模組開啟:
autoLoadEntities: true
但 UsersModule 中卻漏掉了 forFeature([User]):
@Module({
imports: [
// 漏了這行:
// TypeOrmModule.forFeature([User])
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
如果 UsersService 中仍然使用:
@InjectRepository(User)
private readonly userRepository: Repository<User>
NestJS 在建立 UsersService 時,就會因為找不到對應的 Repository Provider,而拋出依賴注入錯誤:
Nest can't resolve dependencies of the UsersService (?).
Please make sure that the argument "UserRepository" is available in the UsersModule context.
原因在於,@InjectRepository(User) 只負責告訴 NestJS:
「這裡需要一個 User 對應的 Repository。」
但真正負責在目前模組中註冊這個 Repository Provider 的是:
TypeOrmModule.forFeature([User])
因此,只開啟 autoLoadEntities: true 並不代表 UsersModule 就能直接注入 UserRepository。如果 Service 要透過 @InjectRepository(User) 注入 Repository,forFeature([User]) 還是必須存在。
⚠️ 注意:分清
forFeature()與autoLoadEntities的職責
TypeOrmModule.forFeature([User]):負責在當前的 Feature Module 中註冊 Repository Provider,讓@InjectRepository(User)可以正常注入。autoLoadEntities: true:負責將透過forFeature()註冊的實體自動加入 TypeORM 的 DataSource 設定。簡單來說,前者處理的是 Repository 能不能被 NestJS 注入,後者處理的是 實體能不能被 TypeORM DataSource 載入。
TypeOrmModule.forFeature([User]) 負責在目前 Module 中註冊 Repository Provider;而 entities 或 autoLoadEntities 則負責讓 TypeORM 的 DataSource 載入實體。entities 最直觀,但維護成本會隨專案規模增加:使用 entities: [User] 雖然明確,但每新增一個實體都必須同步修改 DataSource 設定,容易產生遺漏。__dirname + '/**/*.entity{.ts,.js}' 可以省去逐一列出實體,但它依賴實際檔案結構,在使用 Webpack 等工具打包部署時極易失效,需謹慎使用。