在 PHP 面试中,ThinkPHP 的模型 ORM 几乎是绕不开的考点。很多候选人能说出“模型是比 Db 类更高级的封装”,但一旦追问查询作用域、关联预载入、批量写入的细节,答案就变得模糊。本文从面试官常问的三个维度——查询、写入、关联预载入——展开精讲,帮助你系统掌握 ThinkPHP 模型 ORM 的核心机制。
一、模型查询:不只是换一种写法
ThinkPHP 模型查询与 Db 查询最大的区别在于:模型自带业务语义。它可以通过获取器、修改器、查询作用域、全局作用域等机制,把数据层的逻辑收敛到模型内部,而不是散落在控制器里。
1. 基础查询与链式调用
// 推荐:通过模型静态方法开启查询
$user = User::where('status', 1)
->where('score', '>', 90)
->order('create_time', 'desc')
->limit(10)
->select();
面试中常被追问的一点是:User::where() 与 Db::name('user')->where() 有什么区别?核心差异在于前者返回的是模型查询对象,查询结果会被自动实例化为模型对象,从而可以调用模型方法、触发获取器;后者返回的是数组或 Db 查询对象,不具备模型能力。
2. 查询作用域
查询作用域是模型查询的加分项。它允许你把常用查询条件封装成方法:
class User extends Model
{
// 本地作用域:scope 前缀
public function scopeActive($query)
{
$query->where('status', 1);
}
public function scopeVip($query)
{
$query->where('level', '>=', 5);
}
}
// 调用方式
User::active()->vip()->select();
如果面试官问“如何优雅地复用查询条件”,回答查询作用域比回答“写个公共方法拼 where”要专业得多。
3. 全局作用域
全局作用域会在模型的所有查询中自动生效,常用于软删除、多租户隔离等场景。ThinkPHP 通过 db 配置或模型事件实现,面试中能提到“全局作用域要谨慎使用,避免在后台管理场景中误过滤数据”会显得更有实战经验。
二、模型写入:save、create 与批量操作
写入是面试中区分“会用”和“懂原理”的分水岭。
1. save 与 create 的区别
// save:需要先实例化模型,可指定是否强制写入
$user = new User();
$user->name = '张三';
$user->save();
// create:静态方法,直接返回模型实例
$user = User::create([
'name' => '张三',
'email' => 'zhangsan@example.com',
]);
关键区别在于:create 内部同样调用 save,但它会先过滤非表字段数据(受 $field 或 $schema 影响)。面试中如果被问到“为什么 create 会报字段不存在”,答案通常是模型未定义 $schema 或 $field,导致自动过滤失效。
2. 修改器与自动时间戳
写入时,修改器(Mutator)会自动生效:
public function setPasswordAttr($value)
{
return password_hash($value, PASSWORD_DEFAULT);
}
同时,如果模型开启了 $autoWriteTimestamp = true,create_time 和 update_time 会自动填充。面试中常问的一个坑是:批量写入(saveAll)是否会触发修改器? 答案是会,但不会触发模型事件中的 before_write 以外的部分验证逻辑,需要根据版本确认。
3. 批量写入的性能考量
$list = [
['name' => 'A'],
['name' => 'B'],
];
User::saveAll($list);
saveAll 底层会逐条调用 save,并非真正的批量 SQL。如果数据量很大,面试中应主动提到“高并发批量写入建议用 Db::name()->insertAll(),但会绕过模型修改器和事件”。这种对比回答能体现你对性能与业务语义权衡的理解。
三、关联预载入:解决 N+1 的利器
关联查询是 ORM 面试的高频区,而预载入(Eager Loading)是其中最重要的优化手段。
1. 关联定义
class User extends Model
{
public function articles()
{
return $this->hasMany(Article::class, 'user_id');
}
}
2. N+1 问题与预载入
如果没有预载入,循环获取关联数据会产生 N+1 查询:
$users = User::select();
foreach ($users as $user) {
// 每次循环都查一次数据库
echo $user->articles->count();
}
使用 with 预载入后,ThinkPHP 会先查主表,再用 IN 查询一次性取出所有关联数据:
$users = User::with('articles')->select();
面试中要能说清楚:with 的本质是两次查询 + 内存关联,而不是 JOIN。这也是它与 join 的核心区别——with 保留主模型结构,join 会打平数据并可能产生重复行。
3. 延迟预载入与嵌套预载入
// 延迟预载入:先查主表,再决定是否加载关联
$users = User::select();
$users->load('articles');
// 嵌套预载入
User::with(['articles.comments'])->select();
// 带条件的预载入
User::with(['articles' => function ($query) {
$query->where('status', 1);
}])->select();
面试官如果追问“预载入条件写在 with 里和写在关联定义里有什么区别”,你可以回答:写在 with 里只影响本次查询,写在关联定义里是全局默认条件,后者容易造成隐式过滤,排查问题时更隐蔽。
四、面试答题思路总结
面对 ThinkPHP 模型 ORM 的面试题,建议按以下结构作答:
- 先定性:说明模型与 Db 类的本质区别——业务语义封装。
- 再展开:查询讲作用域,写入讲修改器与批量差异,关联讲预载入与 N+1。
- 补边界:主动提到性能取舍、全局作用域的副作用、
saveAll与insertAll的差异。
能把这三点讲清楚,基本可以证明你不是只会写 CRUD,而是理解 ORM 背后的设计取舍。这也是面试官真正想听到的答案。
未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:模型 ORM 的查询、写入与关联预载入

