ci中不能直接用self::table_name定义表名,因ci3不支持属性中使用self,ci4虽支持但要求常量纯静态;动态表名须延迟求值,在方法内拼接并校验,避免硬编码导致分库分表失效。

CI里用类常量定义表名,为什么不能直接写 self::TABLE_NAME?
因为 CI 的 BaseModel(尤其是 CI4)在构造时会立即读取 $table 属性,而此时类常量若依赖未初始化的上下文(比如分库后缀、租户前缀),就会出错或返回空字符串。更关键的是:CI3 的 $this->db->from() 和 CI4 的 Builder 都不自动解析类常量——它们只认字符串字面量或已计算好的变量。
- CI3 中写
public $table = self::TABLE_NAME;会报错:Parse error: syntax error, unexpected 'self' (T_STRING)—— PHP 不允许在属性声明中直接用self:: - CI4 的
BaseModel允许protected $table = self::TABLE_NAME;,但前提是该常量已在类定义顶部静态声明且不含运行时逻辑(如date()或env()) - 一旦表名含动态部分(如
user_2024),必须放弃$table属性,改在每个方法里手动拼接,否则迁移命令(php spark migrate)、验证器、forge全部失效
CI3 和 CI4 中安全定义表名常量的写法差异
CI3 没有原生支持类常量驱动表名的机制,CI4 虽支持但限制极多。真正能落地的方式是「延迟求值」——把表名生成逻辑从属性移到方法里。
- CI3 推荐写法:在模型方法内用
$this->db->from($this->get_table_name()),get_table_name()返回拼接后的字符串 - CI4 正确姿势:重写
find()、insert()等方法,在内部调用$this->getTable()获取真实表名;getTable()可基于config('MySettings')->tenant_id或路由参数动态计算 - 禁止在
__construct()里提前赋值$this->table—— 构造时还拿不到请求上下文,比如当前用户租户 ID - 如果真要用常量,CI4 只接受纯静态定义:
private const TABLE_NAME = 'users';,然后protected $table = self::TABLE_NAME;
调用数据库时,表名拼接容易踩的坑
很多人以为 $this->db->from('prefix_' . self::TABLE_NAME) 就万事大吉,其实漏掉了三件事:前缀处理、大小写一致性、SQL 注入防护。
-
$this->db->dbprefix('users')是 CI3 安全拼前缀的唯一方式;CI4 已移除该方法,需手动拼:config('Database')->default->DBPrefix . 'users' - 所有表名必须小写(MySQL 默认不区分,但开启
lower_case_table_names=0时大小写敏感),常量定义也应全小写:const USER_LOG = 'user_log';,而非USER_Log - 若表名来自用户输入(如租户标识),必须白名单校验:
in_array($tenant, ['cn', 'us', 'jp'], true),绝不能直接拼进 SQL 字符串 - CI4 的 Query Builder 会对字段名加反引号,但对表名不自动加——手动拼表名时,记得用
escapeIdentifiers()包一层:$builder->from($this->db->escapeIdentifiers($table))
为什么建议放弃「一个模型对应一张物理表」的思维?
不是技术做不到,而是它会让分库分表、租户隔离、归档表切换全部变成硬编码灾难。真正的规范写法是把表名当作「数据路由结果」,而不是「配置项」。
比如用户行为日志要按月分表:log_202608,这个 202608 应该来自 Service 层传入的时间参数,DAO 层据此算出目标表,再 new 出对应 Connection 实例执行——而不是让 UserLogModel 自己猜该查哪张表。一旦模型开始承担路由职责,就再也无法做单元测试、无法 mock、无法复用。











