基于查询字符串参数将LoopBack v3端点请求路由至指定数据库
看起来你已经搭好了基础的LoopBack服务,现在要实现通过client查询参数动态切换数据库,我分两种常见的LoopBack版本来给你讲具体实现步骤:
针对LoopBack 3.x的实现
第一步:配置多数据源
首先在server/datasources.json里为每个客户端配置对应的数据库连接,比如:
{ "defaultDB": { "name": "defaultDB", "connector": "mysql", "host": "localhost", "database": "default_db", "user": "root", "password": "xxx" }, "clientxyzDB": { "name": "clientxyzDB", "connector": "mysql", "host": "localhost", "database": "clientxyz_db", "user": "root", "password": "xxx" }, "clientabcDB": { "name": "clientabcDB", "connector": "mysql", "host": "localhost", "database": "clientabc_db", "user": "root", "password": "xxx" } }
第二步:给所有数据源绑定你的Model
创建一个boot脚本server/boot/attach-models.js,确保每个数据源都关联上你的MyEndpoint模型,避免切换时找不到对应模型:
module.exports = function(app) { const myEndpointModel = app.models.MyEndpoint; // 遍历所有以DB结尾的数据源,绑定模型 Object.keys(app.dataSources).forEach(dsName => { if (dsName.endsWith('DB')) { app.dataSources[dsName].attachModel(myEndpointModel); } }); };
第三步:用远程钩子或自定义远程方法处理参数与数据源切换
方案一:用beforeRemote钩子拦截所有请求(保留默认CRUD接口)
在common/models/myendpoint.js里添加钩子,动态切换数据源:
module.exports = function(MyEndpoint) { // 拦截所有远程方法调用 MyEndpoint.beforeRemote('**', function(ctx, _, next) { // 从查询参数里获取client值 const client = ctx.req.query.client; // 如果没有传client参数,用默认数据源继续执行 if (!client) return next(); // 根据client名称获取对应的数据源 const targetDataSource = app.dataSources[`${client}DB`]; if (!targetDataSource) { return next(new Error(`找不到对应client: ${client}的数据源`)); } // 注意:LoopBack 3的Model数据源是静态属性,并发场景下可能有冲突 // 如果你要处理高并发,更推荐用下面的自定义远程方法方案 MyEndpoint.setDataSource(targetDataSource); next(); }); };
方案二:自定义远程方法(更安全的并发处理)
如果你的服务有高并发需求,全局切换Model的数据源会导致请求互相干扰,推荐自定义一个远程方法来处理:
module.exports = function(MyEndpoint) { // 自定义GET接口,覆盖默认的find方法 MyEndpoint.remoteMethod('findByClient', { accepts: [ {arg: 'client', type: 'string', required: false, http: {source: 'query'}} ], returns: {arg: 'data', type: 'array', root: true}, http: {path: '/', verb: 'get'} }); MyEndpoint.findByClient = function(client, callback) { // 确定要使用的数据源 const targetDSName = client ? `${client}DB` : 'defaultDB'; const targetDataSource = app.dataSources[targetDSName]; if (!targetDataSource) { return callback(new Error(`无效的client参数: ${client}`)); } // 获取该数据源对应的Model实例,避免全局切换 const modelInstance = targetDataSource.models.MyEndpoint; modelInstance.find({}, callback); }; };
针对LoopBack 4.x的实现
LoopBack 4用的是依赖注入和拦截器体系,实现方式更优雅:
第一步:配置多数据源
在src/datasources下创建多个数据源文件,比如clientxyz.datasource.ts、default.datasource.ts,每个文件对应一个数据库连接。
第二步:创建数据源切换拦截器
在src/interceptors/client-ds.interceptor.ts里定义拦截器,用于获取查询参数并切换Repository的数据源:
import {Interceptor, InvocationContext, Provider, ValueOrPromise} from '@loopback/core'; import {MyEndpointRepository} from '../repositories'; export class ClientDataSourceInterceptor implements Provider<Interceptor> { value() { return this.intercept.bind(this); } async intercept( invocationCtx: InvocationContext, next: () => ValueOrPromise<InvocationResult>, ) { // 从请求查询参数里获取client const client = invocationCtx.request.query.client; if (!client) { // 无参数时用默认数据源 return next(); } // 获取当前请求的Repository实例 const repo = await invocationCtx.get<MyEndpointRepository>(MyEndpointRepository); // 拼接目标数据源的绑定键 const dsBindingKey = `datasources.${client}DB`; try { // 获取目标数据源并切换 const targetDataSource = await invocationCtx.get(dsBindingKey); repo.dataSource = targetDataSource; } catch (err) { throw new Error(`找不到client: ${client}对应的数据源`); } return next(); } }
第三步:给Controller方法绑定拦截器
在你的MyEndpointController里,给需要切换数据源的方法添加拦截器装饰器:
import {intercept} from '@loopback/core'; import {get} from '@loopback/rest'; import {MyEndpointRepository} from '../repositories'; import {ClientDataSourceInterceptor} from '../interceptors/client-ds.interceptor'; export class MyEndpointController { constructor(protected repository: MyEndpointRepository) {} @get('/myendpoint') @intercept(ClientDataSourceInterceptor) async find() { return this.repository.find(); } }
这样配置后,当你访问https://localhost:3000/api/myendpoint?client=clientxyz时,服务就会自动切换到clientxyzDB对应的数据库查询数据;如果不传client参数,就用默认数据源返回全部数据。
内容的提问来源于stack exchange,提问作者Blake Rivell

