Nest.Js透過env檔載入TypeOrmModule參數

前言

在剛開始學習時,很自然地都照的官方文件就寫在forRoot裡,在學習過程還不會想太多。
但真的要嘗試在自己專案使用時,就會發現所有參數都寫死在程式裡,絕對不是一種好的作法
尤其又像資料庫ip、帳號、密碼,這種有可能有其他原因而有變動的情況
如果環境比較新穎,使用docker的話,這問題還算好處理。寫在process.ENV裡面,基本上還可以做到動態載入,那就不必特別修改了。
但若還沒打算用到微服務的架構,則將這些參數拉出來放在env檔裡,再透過configService統一載入,會是較好的作法!

TypeOrmModule透過ENV載入參數

所幸已有人提問,且原作者也做了相應更新
TypeOrmModule裡,已提供forRootAsync可動態載入參數。

ConfigService寫法,與官方文件一樣。就是直接載入dev檔

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  // name: 'MY_DB_NAME',
  useFactory: async (configService: ConfigService) => ({
    retryAttempts: 10,
    retryDelay: 30 * 1000,
    type: 'postgres',
    host: configService.get('DB_HOST')         || 'localhost',
    port: Number(configService.get('DB_PORT')) || 5432,
    username: configService.get('DB_USERNAME') || 'dev',
    password: configService.get('DB_PASSWORD') || 'dev',
    database: process.env.POSTGRES_DB          || 'MY_DEMO_DB',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: configService.get('DB_SYNCHRONIZE') === 'true' || configService.get('DB_SYNCHRONIZE') === 'TRUE',
    logging: true,
    logger: 'file',
  }) as TypeOrmModuleOptions,
  inject: [ConfigService],
}),

除了useFactory的寫法之外,也可使用useClass可參閱此篇Nest.Js原作者提供的示例

使用forRootAsync幾個要注意的地方

如果事情有這麼簡單直接設定就好了
尤其專案已經寫的差不多,才想到要把這塊抽出來。一改完絕對是滿滿的ERROR噴發
官方文件沒特別寫到,勢必裡面就有不少坑…
分享一下剛從坑裡爬出來的經驗Q_Q

傳入的參數要做型別轉換

因由env載入的資料,都是string
像port、boolean,就需要再做型別轉換

轉數字:Number(configService.get('DB_PORT'))
轉boolean:configService.get('DB_SYNCHRONIZE') === 'true'

因JavaScript本身有許多奇妙動態轉換,直接使用Boolean(configService.get('DB_SYNCHRONIZE')),會因字串有值而永遠回傳true
所以需要透過===做精確判斷,若要顧慮大小寫問題,則如上方範例這樣書寫即可。

明明參數是從forRoot複製貼上,TypeScript卻報錯?

請留意程式碼最後方,多了強制轉型as TypeOrmModuleOptions
不知什麼原因,TypeScript居然無法正確解析出他的參數!
而Nest.Js作者也沒特別去改善,也是透過強制轉型將報錯的部份轉型過去(見此處留言)

測試發現,typesynchronizelogger,都需要特別轉型才不會報錯。
後來該留言下方有人提出直接將整包參數轉型TypeOrmModuleOptions,算是一個比較好的解法。也好讀許多!

name的參數,無法透過env動態載入

若專案需要透過typeOrm連接多個資料庫的需求,隨便Google一下也會發現可設定name來做到
但用forRootAsync,就沒辦法將此參數由外部傳入了
因為是設定在useFactory之外

另外,若沒有連接多個資料庫的需求,就不必特別設定name,由Nest.Js自動處理就好。
name的目的是當有多資料庫時,做為區分資料庫使用
會在@InjectRepository(Photo, 'ANOTHER_DB')中,傳入不同資料庫的名字
有設定的話,就必須在每個@InjectRepository中傳入相應名字,才會正確解析資料表

後記

我自己有些transaction寫法是透過typeOrm注入connection來做的
connection.transaction(async (entityManager) => { ... })
發現改成forRootAsync時,會出現一直找不到connection,弄了好久還是搞不定
最後直接統一改成注入@InjectEntityManager(),使用entityManager.transaction({...})來處理這塊,就不在報錯了
具體這兩個有什麼差別也不太清楚,但總算順利透過env檔傳遞typeOrm的參數了!

參考資料

github / Can’t init TypeOrmModule using factory and forRootAsync
github / Proper way to initialize config
TypeOrmModule.forRootAsync can’t read name property correctly