iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記系列 第 19 篇

Day 19|遺失的實體:為什麼 TypeORM 找不到 Entity?

  • 分享至 

  • xImage
  •  

剛開始在 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 呢?

根因:TypeORM 建立連線時,沒收到實體清單

這其實是因為 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 the forFeature() method, but are only referenced from the entity (via a relationship), won't be included by way of the autoLoadEntities setting.

方案三:使用 Glob 路徑自動載入

在許多 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 載入。

總結

  1. TypeORM 連線需要實體清單:TypeORM 建立 DataSource 時,需要取得要管理的實體。
  2. 依賴注入與載入實體是兩回事:TypeOrmModule.forFeature([User]) 負責在目前 Module 中註冊 Repository Provider;而 entities 或 autoLoadEntities 則負責讓 TypeORM 的 DataSource 載入實體。
  3. 手動維護 entities 最直觀,但維護成本會隨專案規模增加:使用 entities: [User] 雖然明確,但每新增一個實體都必須同步修改 DataSource 設定,容易產生遺漏。
  4. Glob 掃描方便,但要留意部署環境:使用 __dirname + '/**/*.entity{.ts,.js}' 可以省去逐一列出實體,但它依賴實際檔案結構,在使用 Webpack 等工具打包部署時極易失效,需謹慎使用。

參考資料


上一篇
Day 18|伺服器崩潰危機:FileInterceptor() 如何在尖峰時刻榨乾你的記憶體
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言