CodeIgniter4读写分离配置与单元测试

云浩吖_7732

云浩吖_7732

2026-08-09

464人浏览

原创

CodeIgniter 4 原生不支持读写分离,需手动定义 read/write 数据库组并在 BaseModel 中按 SQL 类型动态选择连接,事务不跨连接,SELECT FOR UPDATE 等特殊语句须人工标注走主库。

codeigniter4读写分离配置与单元测试

CodeIgniter 4 原生不支持读写分离,必须通过自定义数据库组 + 手动路由 SQL 类型来实现;它没有像 Yii2 那样内置 slave 属性或自动识别 SELECT / INSERT 的机制,所以“透明读写分离”只能靠代码层拦截和判断,不能靠框架自动切换。

怎么在 database.php 中定义主从连接组

CI4 的多库配置比 CI3 更清晰:每个连接组是独立数组,不依赖全局 $db 变量。你得在 app/Config/Database.php 的 public $default 类属性之外,显式声明新连接实例。

  • 复制一份 MySQLiConnection 或 PDOConnection 配置,改名为 read 和 write(命名任意,但建议语义化)
  • hostname、username、password、database 必须分别指向从库和主库,不要复用同一套凭据
  • DB_DEBUG 建议设为 ENVIRONMENT !== 'production',避免生产环境暴露敏感错误
  • 不要设 autoConnect => true —— 否则每次请求都会连两次库(主+从),哪怕只执行写操作

示例片段(放在 app/Config/Database.php 类内部):

public $read = [
    'DSN'      => '',
    'hostname' => $_ENV['DB_READ_HOST'] ?? '192.168.1.10',
    'username' => $_ENV['DB_READ_USER'] ?? 'reader',
    'password' => $_ENV['DB_READ_PASS'] ?? '',
    'database' => $_ENV['DB_READ_NAME'] ?? 'app_db',
    'DBDriver' => 'MySQLi',
    'DBPrefix' => '',
    'pConnect' => false,
    'DBDebug'  => (ENVIRONMENT !== 'production'),
    'charset'  => 'utf8mb4',
    'DBCollat' => 'utf8mb4_unicode_ci',
    'swapPre'  => '',
    'encrypt'  => false,
    'compress' => false,
    'strictOn' => false,
    'failover' => [],
    'port'     => 3306,
];

public $write = [
    'DSN'      => '',
    'hostname' => $_ENV['DB_WRITE_HOST'] ?? '192.168.1.5',
    'username' => $_ENV['DB_WRITE_USER'] ?? 'writer',
    'password' => $_ENV['DB_WRITE_PASS'] ?? '',
    'database' => $_ENV['DB_WRITE_NAME'] ?? 'app_db',
    'DBDriver' => 'MySQLi',
    'DBPrefix' => '',
    'pConnect' => false,
    'DBDebug'  => (ENVIRONMENT !== 'production'),
    'charset'  => 'utf8mb4',
    'DBCollat' => 'utf8mb4_unicode_ci',
    'swapPre'  => '',
    'encrypt'  => false,
    'compress' => false,
    'strictOn' => false,
    'failover' => [],
    'port'     => 3306,
];

如何在 Model 中按 SQL 类型动态选择连接

CI4 没有 $this->db->read() 这种语法,也不能靠重写 Loader 或 DB_driver(因为核心类已改为可注入式服务)。最稳妥的做法是:在基类 Model 中封装两个连接实例,并根据方法名或显式标记决定走哪条路。

  • 在 app/Models/BaseModel.php 构造函数中加载两个连接:$this->readDB = \Config\Database::connect('read'); 和 $this->writeDB = \Config\Database::connect('write');
  • 所有 insert()、update()、delete()、replace() 必须调用 $this->writeDB 实例
  • 所有 find()、findAll()、select()->get() 默认走 $this->readDB,除非显式传参 ['force_write' => true]
  • 避免在同一个事务里混用两个连接——CI4 的事务不跨连接,$this->writeDB->transStart() 对 $this->readDB 完全无效

简单判断逻辑示例(放在 BaseModel 的查询方法中):

CodeIgniter 4.7.0
CodeIgniter 4.7.0

CodeIgniter下载页提供 CodeIgniter 4.7.0 官方源码包,并整理框架安装、数据库配置与入门开发说明。

下载
protected function getQueryConnection(array $options = []): BaseConnection
{
    if ($options['force_write'] ?? false) {
        return $this->writeDB;
    }

    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    $caller = $backtrace[1]['function'] ?? '';

    // 写操作方法名特征
    if (in_array($caller, ['insert', 'update', 'delete', 'replace', 'save'], true)) {
        return $this->writeDB;
    }

    return $this->readDB;
}

单元测试怎么验证读写分离是否生效

CI4 的测试默认只连 default 连接,而你定义的 read / write 组不会被自动加载。不 mock 连接对象的话,测试会直接连真实数据库,既慢又不可控。

  • 在 tests/_support/DatabaseTestCase.php 中重写 setUp(),手动注册测试专用连接(比如都指向 SQLite 内存库)
  • 用 Mockery::mock() 替换 BaseConnection 子类,断言 query() 被调用时传入的 SQL 是否含 INSERT 或 SELECT
  • 关键点:不要断言“是否连了某 IP”,而要断言“某个 Model 方法是否调用了 $this->writeDB->query()”
  • 容易漏掉的场景:关联查询(with())、子查询(subquery())、原生 SQL(query() 直接调用)——这些必须显式指定连接,否则默认走 default

测试片段示意:

public function testInsertUsesWriteConnection()
{
    $writeMock = Mockery::mock(BaseConnection::class);
    $writeMock->shouldReceive('query')->with(Mockery::pattern('/INSERT/i'))->once();

    $model = new TestModel();
    $model->writeDB = $writeMock; // 强制注入

    $model->insert(['name' => 'test']);
}

真正麻烦的不是配置,而是当业务逻辑里出现 SELECT ... FOR UPDATE 或存储过程调用时——这类语句语义上是“读”,但行为上必须走主库。这种边界情况没法靠正则匹配 SQL 自动识别,必须人工标注或统一收口到特定 Service 方法里。别指望框架替你做这个判断。

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
PHP CodeIgniter框架
PHP CodeIgniter框架

本专题专注于 PHP 轻量级框架 CodeIgniter 的学习与应用,系统讲解 MVC 模型、路由与控制器、数据库操作、表单验证、安全机制以及 REST API 开发。通过博客系统、内容管理平台和小型电商等实战项目,帮助学员快速掌握 CodeIgniter 的开发方法与实践技能。

2025.08.28

3839

16

Vite 项目如何打包和部署
Vite 项目如何打包和部署

Vite项目打包部署专题汇集生产构建、产物预览、输出目录、资源路径、Nginx与GitHub Pages部署及常见白屏和路由404问题的已审核文章,帮助读者完成上线流程。

2026.10.28

100

16

Vite 如何搭配 Vue.js
Vite 如何搭配 Vue.js

Vite搭配Vue.js专题汇集创建Vue 3项目、配置开发环境、路径别名、Vue插件、代理、多环境构建与调试的已审核文章,帮助读者完成从初始化到日常开发的配置。

2026.10.28

60

15

Vite 项目目录怎么看
Vite 项目目录怎么看

Vite项目目录专题汇集入口文件、src源码、public静态资源、路径别名、项目根目录和构建输出目录相关的已审核文章,帮助读者理解各文件夹的用途与引用方式。

2026.10.28

220

10

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

2026.10.22

140

11

Gradle Wrapper配置教程
Gradle Wrapper配置教程

本专题介绍Gradle Wrapper的基本作用,涵盖文件组成、生成方法、版本配置和任务调用,帮助你理解项目如何使用指定的Gradle版本,减少不同开发环境带来的构建差异。

2026.10.22

120

10

Golangci-lint检查器使用教程
Golangci-lint检查器使用教程

本专题整理Golangci-lint常用Linters的用途与配置方法,覆盖错误处理、无效代码、安全、复杂度、命名和依赖检查,帮助项目合理选规则。

2026.10.22

80

15

Gradle怎么构建Java项目
Gradle怎么构建Java项目

本专题介绍Gradle Java项目构建方法,涵盖Java插件配置、依赖声明、源码编译、测试执行和打包检查,帮助你理解各阶段的任务关系,逐步完成项目构建。

2026.10.22

60

10

PHP时间戳漏洞修复教程
PHP时间戳漏洞修复教程

本专题汇总PHP常见时间戳漏洞,包括2038年溢出、时区漂移、时间戳伪造、重放攻击、时间盲注与日志审计失效等,并给出升级64位环境、使用 DateTimeImmutable、统一UTC、严格超时校验、预处理绑定、hash_equals 比对等修复方案,帮助开发者快速排查和加固时间相关代码。

2026.10.22

100

25

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程