- 后端
【免费下载链接】mikro-orm
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
本指南以 MikroORM 7 的官方文档 read-connections.md 为核心,系统讲解如何通过replicas配置为 ORM 挂载多个只读数据库副本,掌握读操作(SELECT/COUNT)自动分流、connectionType显式路由、事务内强制写连接等完整行为。读完本文,你将能在一主多从的数据库架构下正确配置 MikroORM,理解随机读副本的解析机制及其底层源码实现,并安全地将裸 SQL 查询路由到读副本。
一、核心概念:写连接与读副本
MikroORM 的数据库驱动(IDatabaseDriver)内部维护两类连接:
- 写连接(write connection):即主连接(master),在初始化配置时通过
dbName、user、host等顶层选项指定,所有写操作(INSERT/UPDATE/DELETE)以及事务内的全部操作都在此连接上执行; - 读副本(read replicas):通过
replicas选项声明的额外连接,仅用于承担读流量,从而把查询压力从主库分流到只读从库。
从源码看,SQL 驱动在构造时会为每个副本配置创建独立的连接实例:
// packages/sql/src/AbstractSqlDriver.ts#L110-L120 protected constructor( config: Configuration, platform: Platform, connection: Constructor<Connection>, connector: string[], ) { super(config, connector); this.connection = new connection(this.config); this.replicas = this.createReplicas(conf => new connection(this.config, conf, 'read')); this.platform = platform; }createReplicas定义于 packages/core/src/drivers/DatabaseDriver.ts,它会读取配置中的replicas数组并逐一构造副本连接。
二、配置多个读副本:replicas选项
在MikroORM.init()时通过replicas数组声明读副本。关键设计是:副本只需提供与主连接不同的字段,其余配置(如dbName、port、password等)会自动从主连接继承。
const orm = await MikroORM.init({ entities: [Author, ...], dbName: 'my_database', user: 'master_user', host: 'master_host', preferReadReplicas: true, // 可选属性,默认值为 true replicas: [ { name: 'read-1', host: 'read_host_1', user: 'read_user' }, { name: 'read-2', host: 'read_host_2' }, // 未提供 user,将从主连接继承 ], });配置继承的源码证据
createReplicas中明确定义了从主连接继承的属性白名单(packages/core/src/drivers/DatabaseDriver.ts#L879-L904):
const props = [ 'dbName', 'clientUrl', 'host', 'port', 'user', 'password', 'multipleStatements', 'pool', 'name', 'driverOptions', ] as const; for (const conf of replicas) { const replicaConfig = Utils.copy(conf) as Dictionary; for (const prop of props) { if (conf[prop]) { continue; } // 从主连接配置中补齐缺失字段 } }注意两点实操细节:
- 显式提供
clientUrl的副本:源码中会对「已提供clientUrl但缺失host/port/user/password的副本」做特殊处理,不强行覆盖这些可推断字段,避免冲突; name字段用于日志标识:配置类型中name的注释明确说明其用途是「使用副本时用于日志记录的连接名」(packages/core/src/utils/Configuration.ts#L603)。副本的name与preferReadReplicas、replicas是否存在共同决定了日志系统是否启用副本标识(usesReplicas标志,见 Configuration.ts#L209)。
replicas的类型为ConnectionOptions[](Configuration.ts#L1179),因此每个副本都支持完整的连接配置项,包括pool池化选项与driverOptions驱动级参数。
三、默认路由策略:随机读副本
文档明确:当解析读连接时,默认策略是为所有不在事务内的读操作(SELECT、COUNT)随机分配一个读副本。
const connection = em.getConnection(); // 写连接 const readConnection = em.getConnection('read'); // 随机读副本连接这一随机逻辑在 packages/core/src/drivers/DatabaseDriver.ts#L225-L233 中实现:
getConnection(type: ConnectionType = 'write'): C { if (type === 'write' || this.replicas.length === 0) { return this.connection; } const rand = Utils.randomInt(0, this.replicas.length - 1); return this.replicas[rand]; }值得强调的两个边界行为:
- 未配置任何副本时,即使请求
'read'也会回退到写连接(this.replicas.length === 0分支); - 随机策略是每次调用随机抽取,多个副本间大致负载均衡,但不保证严格轮询。
em.getConnection(type)的type取值只有'read' | 'write'两种。
四、preferReadReplicas:反转默认策略
preferReadReplicas是全局配置开关,默认值为true(见 Configuration.ts#L175 的 DEFAULTS 定义)。当设置为false时,默认连接永远是写连接,除非显式请求读副本。这在「读副本存在但只希望特定场景使用」的应用中非常有用。
配置为false后,即使执行的是纯读操作(findOne),默认也会落在写连接上:
// 假设配置了 preferReadReplicas: false const res5 = await em.findOne(Author, 1); // 写连接——即使是读操作 const res6 = await em.findOne(Author, 1, { connectionType: 'read' }); // 除非显式要求读副本底层解析函数
连接类型的最终裁决逻辑集中在 SQL 驱动的resolveConnectionType(packages/sql/src/AbstractSqlDriver.ts#L3140-L3154),其优先级为:
- 事务上下文优先:若存在
ctx(事务),无条件返回'write'; - 显式
connectionType次之:调用方显式指定时直接采用; preferReadReplicas兜底:为true时读操作默认走'read',否则走'write'。
protected resolveConnectionType(args: { ctx?: Transaction; connectionType?: ConnectionType }): ConnectionType { if (args.ctx) { return 'write'; } if (args.connectionType) { return args.connectionType; } if (this.config.get('preferReadReplicas')) { return 'read'; } return 'write'; }这段源码同时印证了本文第五节、第六节的行为:无论connectionType如何显式声明,事务都会覆盖它。
五、操作级显式路由:connectionType选项
你可以通过各操作 Options 参数上的connectionType属性为单个操作指定连接类型(如FindOptions、CountOptions,以及 QueryBuilder 的第三个参数)。
EntityManager 层面
const res4 = await em.findOne(Author, 1, { connectionType: 'write' }); // 显式写连接 const res5 = await em.findOne(Author, 1, { connectionType: 'read' }); // 显式读副本FindOptions等选项类型中均包含可选的connectionType?: ConnectionType字段(例如 packages/core/src/entity/EntityLoader.ts#L73),并会传递给底层驱动参与连接解析。find、count、countBy、findOne等操作全部适用。
QueryBuilder 层面
createQueryBuilder的第三个参数用于声明连接类型:
const qb1 = em.createQueryBuilder(Author); const res1 = await qb1.select('*').execute(); // 随机读副本 const qb2 = em.createQueryBuilder(Author, 'a', 'write'); const res2 = await qb2.select('*').execute(); // 写连接 const qb3 = em.createQueryBuilder(Author); const res3 = await qb3.update(...).where(...).execute(); // 写连接(写操作永远走写连接)注意第三个示例:即使没有显式指定,UPDATE/DELETE 等写操作也必然路由到写连接,这是由操作语义决定的,与preferReadReplicas无关。
六、事务内一律使用写连接
所有在事务内执行的查询(无论读还是写)都固定使用写连接,这是保证事务隔离性与数据一致性的硬性约束:
// 事务内的所有查询都会使用写连接 await em.transactional(async em => { const a = await em.findOne(Author, 1); // 写连接 const b = await em.findOne(Author, 1, { connectionType: 'read' }); // 仍是写连接——我们处于事务中 a.name = 'test'; // 将在 flush 时于写连接上触发 update });即使在事务内显式传入{ connectionType: 'read' },也不会路由到读副本。原因已在第四节源码中说明:resolveConnectionType首先检查args.ctx,一旦存在事务上下文就直接返回'write'。这避免了「事务中的修改未同步到从库、又从从库读到旧数据」这类经典脏读/不一致问题。测试用例 tests/features/read-replicas.test.ts 中专门验证了事务内connectionType: 'read'仍落在写连接上的行为。
七、裸 SQL 查询(Raw SQL)的读副本路由
MikroORM 7 文档进一步补充了裸 SQL 的处理规则:em.execute()默认走写连接,因为裸 SQL 无法静态判断是读还是写——无论其返回结果是行集还是影响行数,都可能包含写入语句。
要显式将裸 SQL 路由到读副本,需要在 options 中传入connectionType: 'read',同时保留 EntityManager 会话上下文与取消(cancellation)选项:
const rows = await em.execute('select * from author where age > ?', [18], { connectionType: 'read', }); const author = await em.execute('select * from author where id = ?', [1], { method: 'get', connectionType: 'read', });裸 SQL 路由的三条边界规则
- 事务优先:处于活动事务中时,事务连接始终优先于
connectionType,查询被固定在事务绑定的连接上; - RLS 会话设置:使用事务级行级安全(row-level security)时,隐式事务与 session 设置会在所选副本上应用;
- 无副本回退:未配置副本时,显式的
read请求会回退到写连接。
最重要的一条安全提醒:只有当 SQL 确定可以在读副本上安全执行时才应请求读连接——MikroORM 不会解析 SQL 文本来判断其安全性。若把写语句发往只读副本,将由数据库自身的只读限制来拒绝,而非 ORM 帮你把关。
八、测试验证与完整行为矩阵
仓库测试 tests/features/read-replicas.test.ts 覆盖了preferReadReplicas为true(默认)与false两条路径下的完整行为,包括findOne、count、countBy的显式connectionType: 'read' | 'write'路由,以及事务内强制写连接的行为(测试中通过orm.config.set('preferReadReplicas', false)动态切换配置,说明该选项运行时亦可调整)。
综合文档与源码,连接路由的完整决策矩阵如下:
| 场景 | 默认行为 | 显式connectionType行为 |
|---|---|---|
| 非事务读操作(SELECT/COUNT/find/count) | preferReadReplicas: true时随机读副本 | 按指定类型路由(read/write) |
| 非事务写操作(UPDATE/DELETE/insert) | 写连接 | 始终写连接 |
| 事务内任意操作 | 写连接 | 写连接(事务优先) |
preferReadReplicas: false时非事务读操作 | 写连接 | 按指定类型路由 |
裸 SQL(em.execute) | 写连接 | 显式connectionType: 'read'可路由到副本 |
| 未配置任何副本 | 写连接 | 请求read时回退写连接 |
九、小结
MikroORM 7 的读副本体系围绕「默认随机分流 + 显式覆盖 + 事务强制写」三条原则设计:
- 通过
replicas数组声明从库,缺失的连接字段自动从主连接继承; preferReadReplicas(默认true)控制全局默认策略,可整体反转;connectionType在操作级提供精确控制,适用于 find/count/QueryBuilder/裸 SQL;- 事务上下文在
resolveConnectionType中拥有最高优先级,保证一致性; - 裸 SQL 默认走写连接,如需走读副本必须显式声明且自行确保语句为只读。
在配置生产环境时,建议为每个副本设置清晰的name以便日志排查,并依据实际从库延迟与一致性要求决定是否全局开启preferReadReplicas。更完整的配置项说明可参考 Configuration.ts 的类型定义与 docs/docs/read-connections.md(当前主线文档版本)。
- 后端
【免费下载链接】mikro-orm
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
相关推荐
MikroORM 只读副本连接(Read Replica Connections)完整指南:配置、解析策略与源码实现
MikroORM 只读副本连接(Read Replica Connections)完整指南:配置、解析策略与源码实现 导读 本文聚焦 MikroORM 的 只读
后端MikroORM 只读副本连接(Read Replicas)实战指南:配置、连接路由策略与源码级原理
MikroORM 只读副本连接(Read Replicas)实战指南:配置、连接路由策略与源码级原理 MikroORM 是构建在 Data Mapper、Uni
后端Simple USB Terminal高级应用:如何利用前台服务实现后台数据缓冲
Simple USB Terminal高级应用:如何利用前台服务实现后台数据缓冲 Simple USB Terminal是一款专为Android设备设计的串口终
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考