Nest.Js 參數由資料庫讀取

前言

最單純直覺取得參數的方式,就是寫在env檔
而Nest.Js官方也提供了config module可以直接使用
優點是快速、簡單,缺點就是需要異動參數時,就得重啟服務了

有些偏業務面使用的參數,平常不太會異動
但又希望可以做成頁面讓user自行管理,達到不必透過重啟服務的話
將其保存在資料庫是最方便的事了

但有些參數是服務初始化時就要先用到的該怎麼做呢?
就需要配合Nest.Js的生命週期來處理了!

Nest.Js life cycle

nestjs-life-cycle

由上圖可見,Nest.Js提供了OnModuleInitonApplicationBootstrap,可以在服務啟動前先取得參數
相信從名字就可以直接理解用途了!

像port、token等最底層的參數,我仍是使用env來處理
因為這些一旦有異動需求,通常都要開發人員協助處理,無法讓user自行處理

而參數日常業務使用量大,也不適合每次都從資料庫重新取得
故直接存在memory裡
提供api供前端異動,再輔以interceptor
當參數異動後,除了DB寫入參數之外,同步將新的參數更新至memory裡,以維持日常業務的高效使用

直接看程式碼

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Injectable()
export class ConfigService implements OnModuleInit {

private readonly logger = new Logger('config');

private dbConfig: ConfigEntity[] = []; // 將參數清單儲存在memory,便於業務直接取用,不必一直反覆撈資料庫

constructor(@InjectRepository(ConfigEntity)
private readonly repo :Repository<ConfigEntity>) { }

/**
* 服務初始化時,透過Nest.Js life cycle,先行載入參數
*/
async onModuleInit() {
this.dbConfig = await this.repo.find();
this.logger.log(`-------------Initialized Database Config-------------\n` +
`${inspect(this.dbConfig, false, null, false)}\n` +
`-----------------------------------------------------`);
}

/**
* 給其他service取得參數使用
* @param id 參數ID
* @return 參數值
*/
get(id: string) : string {
const value = this.dbConfig.find(c => c.id === id)?.value;
if (value) {
return value;
}

this.logger.error(`查詢${id},記憶體內查無資料!以下為目前記憶體內的資料`);
this.logger.error(inspect(this.dbConfig, false, null, false));
throw new InternalServerErrorException(`config查無「${id}」參數!`);
}

/**
* 前端異動參數後,供interceptor更新記憶體資料
* @param config
*/
updateConfig(config: ConfigEntity): void {
const index = this.dbConfig.findIndex(c => c.id === config.id);
if (index === -1) {
this.logger.error(`更新記憶體資料失敗!查無${config.id}!以下為目前記憶體內的資料`);
this.logger.error(inspect(this.dbConfig, false, null, false));
throw new InternalServerErrorException('更新記憶體資料config失敗!請查閱config log檢視相關資訊');
}

this.dbConfig[index] = config;
this.logger.debug(`更新記憶體資料成功!${config.id}=${config.value}`);
}
}

以下順便提供interceptor,供參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Injectable()
export class UpdateLoggerInterceptor implements NestInterceptor {

private readonly logger = new Logger('config');

constructor(private readonly configService: ConfigService) { }

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp().getRequest();
const user = ctx.user as UsersEntity;
const userIp = ctx.headers['x-forwarded-for'] || ctx.connection.remoteAddress;

const updateId = ctx.params['id'];
const beforeUpdateValue = this.configService.get(updateId);
return next.handle().pipe(
tap((response: ConfigEntity) => {
this.logger.log(`[${userIp}] [${user.username}-> ${user.name}] 更新參數:${updateId}${beforeUpdateValue} -> ${response.value}`);
this.configService.updateConfig(response);
}),
);
}
}

心得

我的專案是在Nest.Js官方提供 config module 以前就建立完成的
當時官方教學,config讀env檔,是直接寫在constructor裡
又以服務上線為主,並沒花太多心力在處理底層,直接官方文件Copy就先用了
所有的參數都寫在env,只要有異動就要重啟…

服務穩定後,想要把業務參數放給user自己維護
希望能異動後立即生效、不必中斷服務
於是開始了這次的改寫工程

而JavaScript 的 constructor 無法執行async, await的異步函式
所幸有life cycle,可以很輕鬆地在服務完全啟動前載入參數
再配合interceptor同步更新記憶體資料,不必花費太大心力,就改寫完成