thinkphp中readonly字段不触发关联缓存,需在关联定义中显式调用cache()方法并设置唯一缓存键(如['user_profile', $this->id]),否则with()加载仍查库。

模型字段只读时缓存失效怎么办
ThinkPHP 的 readonly 字段设置本身不触发缓存,它只是阻止写入;但很多人误以为「只读字段 + 关联查询」会自动走缓存,结果发现 with() 加载的关联数据每次都在查库。
根本原因是:ThinkPHP 默认对关联结果不做持久化缓存,无论主模型是否只读。缓存只作用于主模型查询(如 find()),不延伸到关联对象实例。
-
readonly是模型层的写保护机制,和缓存系统完全解耦 - 关联加载(
hasOne、hasMany等)默认走实时查询,不会复用主模型缓存中的字段值 - 若想让关联结果也缓存,必须显式调用
cache()方法,并确保缓存键能区分不同主记录
正确做法是在关联定义里加缓存配置,而不是依赖字段只读:
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class)->cache(true);
}
}
with() 关联预加载后如何强制走缓存
直接写 User::with('profile')->find(1) 不会缓存关联结果——with() 本身不带缓存能力,它只是语法糖,底层仍是多次查询或 JOIN。
真正起作用的是在关联方法返回的查询对象上调用 cache(),且推荐使用带键名的缓存(避免冲突):
- 用字符串键:例如
cache('user_profile_1'),适合已知主键场景 - 用数组键:例如
cache(['user_profile', $this->id]),更安全,支持序列化生成唯一键 - 注意缓存有效期:关联数据变化频率高时,别设太长的
$expire,否则容易脏读
示例(在模型中重写关联方法):
public function profile()
{
return $this->hasOne(Profile::class)
->cache(['user_profile', $this->id], 3600);
}
关联缓存与事务、软删除的兼容性问题
缓存和数据库状态不同步是高频翻车点。比如开启软删除后,缓存里可能还存着已被逻辑删除的关联记录;或者事务中更新了关联表,但缓存没刷新。
ThinkPHP 的 cache(true) 默认不感知事务回滚,也不监听 delete() 或 restore() 操作。
- 软删除字段(如
delete_time)变更不会自动清除对应缓存 - 手动清除缓存要用
Cache::tag()配合标签,否则只能靠 key 命名规则硬删 - 事务内写入后立即读取关联,大概率拿到旧缓存——此时应临时禁用缓存:
cache(false)
推荐方案:给关联缓存打标签,便于批量清理:
return $this->hasOne(Profile::class)
->cache(['user_profile', $this->id], 3600)
->tag('user_profile');
之后删用户时可一键清空:Cache::tag('user_profile')->clear();
为什么 model->getAttr() 不走关联缓存
很多人在控制器里写 $user->profile,以为会触发带缓存的关联查询,结果发现还是查库。这是因为 __get() 访问动态属性时,ThinkPHP 默认不启用缓存,除非你在关联定义里明确写了 cache()。
更隐蔽的问题是:如果关联模型自己也定义了 cache(true),但主模型没配,依然无效——缓存开关必须落在「发起查询的那个关联关系实例」上。
- 访问
$user->profile本质是执行$user->profile()->find(),所以缓存要挂在profile()方法返回的对象上 - 不能指望模型类全局设置影响所有关联,每个关联需单独配置
- 调试时可用
Db::getLastSql()看是否真走了缓存(没 SQL 就对了)
一句话:缓存不是开关,是绑定在查询链路上的行为。漏掉任意一环,就回到查库。
最常被忽略的是缓存键的上下文依赖——比如用 cache(true) 而不是 cache(['xxx', $id]),会导致所有用户的 profile 缓存在同一个 key 下,互相覆盖。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










