ThinkPHP 面试精讲:模型 ORM 的查询、写入与关联预载入

在 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 的面试题,建议按以下结构作答:

  1. 先定性:说明模型与 Db 类的本质区别——业务语义封装。
  2. 再展开:查询讲作用域,写入讲修改器与批量差异,关联讲预载入与 N+1。
  3. 补边界:主动提到性能取舍、全局作用域的副作用、saveAll 与 insertAll 的差异。

能把这三点讲清楚,基本可以证明你不是只会写 CRUD,而是理解 ORM 背后的设计取舍。这也是面试官真正想听到的答案。

未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:模型 ORM 的查询、写入与关联预载入

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏