基于请求来源用Express.js&Prisma动态切换数据库遇this.DB未定义问题
问题分析与解决方案
问题背景
我想根据请求来源动态切换数据库,于是写了全局中间件处理,但设置完数据库实例后,控制器里的this.DB始终是undefined。
相关代码
全局中间件:
// middlwares/global.middleware.js import DBController from "../controllers/db.controller.js"; import { db1, db2 } from "../prisma/prismaClient.js"; export default (req, res, next) => { const dbcontroller = DBController(); const domain = req.get("origin"); switch (domain) { case "http://localhost:3000": dbcontroller.setDB(db1); break; case "http://localhost:3001": dbcontroller.setDB(db2); break; } next(); };
DB控制器基类:
// controller/db.controller.js import autoBind from "auto-bind"; class DBController { constructor() { this.DB; autoBind(this); } setDB(prismaClient) { this.DB = prismaClient; } } export default DBController;
业务控制器基类:
// conrtoller/controller.js import { generateResponse } from "./../util/public.util.js"; import DBController from "./db.controller.js"; import autoBind from "auto-bind"; import createError from "http-errors"; class Controller extends DBController { constructor() { super(); this.generateResponse = generateResponse; this.createError = createError; autoBind(this); } } export default Controller;
文章控制器:
// controller/article.controller.js import Controller from "./controller.js"; class ArticleController extends Controller { async get(req, res, next) { try { const articles = await this.DB.article.findMany(); // this.DB 是 undefined const response = this.generateResponse("success", articles); res.send(response); } catch (error) { next(error); } } } export default new ArticleController();
核心原因
你遇到的问题本质是实例不共享:
ArticleController是提前实例化(export default new ArticleController())的,整个应用生命周期里只有这一个实例。- 全局中间件每次处理请求时都会创建一个全新的
DBController实例(哪怕你写的DBController()改成new DBController(),依然是新实例),你在这个新实例上设置DB,和控制器里的实例完全无关,所以控制器里的this.DB自然取不到值。
解决方案
方案一:将DB实例挂载到req对象(推荐)
每个请求的req对象是独立的,把DB实例挂载到req上能完美避免并发冲突,实现请求级别的动态切换:
- 修改全局中间件:
// middlwares/global.middleware.js import { db1, db2 } from "../prisma/prismaClient.js"; export default (req, res, next) => { const domain = req.get("origin"); // 根据来源设置对应的DB实例到req上 switch (domain) { case "http://localhost:3000": req.db = db1; break; case "http://localhost:3001": req.db = db2; break; default: // 可设置默认DB或抛出错误 req.db = db1; } next(); };
- 修改文章控制器,从
req中获取DB实例:
// controller/article.controller.js import Controller from "./controller.js"; class ArticleController extends Controller { async get(req, res, next) { try { const articles = await req.db.article.findMany(); const response = this.generateResponse("success", articles); res.send(response); } catch (error) { next(error); } } } export default new ArticleController();
- 简化DB控制器(不需要setDB逻辑了):
// controller/db.controller.js import autoBind from "auto-bind"; class DBController { constructor() { autoBind(this); } } export default DBController;
方案二:使用单例模式的DBController
如果坚持要用DBController的方式,可以改成单例模式,确保中间件和控制器共享同一个实例,但要注意并发请求可能覆盖DB实例的问题:
- 修改DB控制器为单例:
// controller/db.controller.js import autoBind from "auto-bind"; class DBController { constructor() { this.DB; autoBind(this); } // 单例方法:全局只创建一个实例 static getInstance() { if (!DBController.instance) { DBController.instance = new DBController(); } return DBController.instance; } setDB(prismaClient) { this.DB = prismaClient; } getDB() { return this.DB; } } export default DBController;
- 修改中间件使用单例:
// middlwares/global.middleware.js import DBController from "../controllers/db.controller.js"; import { db1, db2 } from "../prisma/prismaClient.js"; export default (req, res, next) => { const dbcontroller = DBController.getInstance(); // 获取全局唯一实例 const domain = req.get("origin"); switch (domain) { case "http://localhost:3000": dbcontroller.setDB(db1); break; case "http://localhost:3001": dbcontroller.setDB(db2); break; } next(); };
- 修改业务控制器基类,绑定单例实例:
// conrtoller/controller.js import { generateResponse } from "./../util/public.util.js"; import DBController from "./db.controller.js"; import autoBind from "auto-bind"; import createError from "http-errors"; class Controller extends DBController { constructor() { super(); this.generateResponse = generateResponse; this.createError = createError; this.dbInstance = DBController.getInstance(); // 绑定单例 autoBind(this); } } export default Controller;
- 修改文章控制器调用方式:
// controller/article.controller.js import Controller from "./controller.js"; class ArticleController extends Controller { async get(req, res, next) { try { const articles = await this.dbInstance.getDB().article.findMany(); const response = this.generateResponse("success", articles); res.send(response); } catch (error) { next(error); } } } export default new ArticleController();
注意:单例模式存在并发风险,如果两个请求同时处理,后一个请求的setDB会覆盖前一个的设置,导致前一个请求用错数据库,因此更推荐方案一。
内容的提问来源于stack exchange,提问作者Alireza Kordkheili
相关产品推荐
相关产品推荐

