ThinkPHP 作为国内使用最广泛的 PHP 框架之一,几乎是 PHP 中高级岗位面试的必考内容。本文聚焦模型、关联和事务三大高频考点,结合面试官常问的问题和实际代码场景,帮你系统梳理知识盲区,从容应对技术面。
一、模型篇:不只是 CURD 的封装
1. 模型和 Db 类的区别是什么?什么时候用模型?
这是面试中的开胃菜,但能区分出候选人是否真正理解 ThinkPHP 的设计哲学。
Db 类是查询构造器,面向数据库表,返回数组,适合简单的、无需业务逻辑的查询。模型是面向业务对象的封装,返回模型实例,支持获取器、修改器、自动时间戳、软删除、关联等高级特性。
面试官想听到的核心答案是:模型承载业务逻辑,Db 只做数据操作。当你的数据需要业务规则(如状态转换、字段格式化、关联操作)时,就应该用模型。
2. 获取器和修改器的工作原理
// 获取器:读取时自动处理
public function getStatusTextAttr($value, $data)
{
$status = [0 => '禁用', 1 => '启用', 2 => '待审核'];
return $status[$data['status']] ?? '未知';
}
// 修改器:写入时自动处理
public function setPasswordAttr($value)
{
return password_hash($value, PASSWORD_DEFAULT);
}
面试官常追问:获取器什么时候不生效? 答案:使用 Db 类查询时不生效;模型内部 $model->getData() 获取原始数据时不触发获取器;toArray() 默认会触发,但可以通过 hidden()、visible() 控制输出字段。
3. 软删除的实现与陷阱
use think\model\concern\SoftDelete;
class User extends Model
{
use SoftDelete;
protected $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
}
高频追问:软删除后如何查询已删除数据? 使用 withTrashed();如何只查询已删除数据? 使用 onlyTrashed();如何真正删除? 使用 force()->delete()。
陷阱:软删除字段默认值要设为 null 或 0,如果设为 0 且字段类型为 int,需注意 defaultSoftDelete 的配置,否则可能导致普通查询过滤异常。
二、关联篇:一对一、一对多、多对多的实战要点
1. 关联定义与预载入
class User extends Model
{
// 一对多:一个用户有多篇文章
public function articles()
{
return $this->hasMany(Article::class, 'user_id', 'id');
}
// 一对一:用户有一个 profile
public function profile()
{
return $this->hasOne(Profile::class, 'user_id', 'id');
}
// 多对多:用户有多个角色
public function roles()
{
return $this->belongsToMany(Role::class, 'user_role', 'role_id', 'user_id');
}
}
面试必问:N+1 问题怎么解决? 答案:使用 with() 预载入。
// 错误示范:N+1
$users = User::select();
foreach ($users as $user) {
echo $user->profile->phone; // 每次循环查一次
}
// 正确示范:预载入
$users = User::with(['profile', 'articles'])->select();
2. 关联统计与延迟关联
// 统计关联数量
$users = User::withCount('articles')->select();
// 输出 $user->articles_count
// 延迟关联:先查主表,需要时再查关联
$user = User::find(1);
$articles = $user->articles()->where('status', 1)->select();
面试官常问:with 和 cache 能一起用吗? 可以,但要注意缓存键的生成,关联数据变化时缓存不会自动失效,需要手动处理。
3. 多对多关联的中间表操作
$user = User::find(1);
// 追加关联
$user->roles()->attach([2, 3]);
// 解除关联
$user->roles()->detach([2]);
// 同步关联(覆盖)
$user->roles()->sync([1, 3]);
高频追问:attach 和 sync 的区别? attach 追加,不删除已有;sync 覆盖,删除不在参数中的关联,只保留参数内的。
三、事务篇:保证数据一致性的关键
1. 三种事务写法
// 方式一:自动事务(推荐)
Db::transaction(function () {
Db::name('user')->where('id', 1)->dec('balance', 100)->update();
Db::name('user')->where('id', 2)->inc('balance', 100)->update();
});
// 方式二:手动事务
Db::startTrans();
try {
Db::name('user')->where('id', 1)->dec('balance', 100)->update();
Db::name('user')->where('id', 2)->inc('balance', 100)->update();
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
// 方式三:模型事务
$user = User::find(1);
$user->startTrans();
// ...
$user->commit();
2. 事务中的常见陷阱
陷阱一:事务中使用了不同的数据库连接。 ThinkPHP 中 Db::name() 和模型默认使用同一连接,但如果配置了多个数据库连接,事务不会跨连接生效。
陷阱二:事务中嵌套事务。 ThinkPHP 支持事务嵌套,通过 savepoint 实现,但 MySQL 的 savepoint 需要 InnoDB 引擎支持。
陷阱三:事务中执行了 DDL 语句。 如 CREATE TABLE、ALTER TABLE 会导致隐式提交,破坏事务原子性。
陷阱四:异常被 catch 后没有重新抛出。 如果捕获异常后不 throw,外层无法感知错误,事务可能被错误提交。
3. 事务与锁的配合
Db::transaction(function () {
// 悲观锁:锁定行
$user = Db::name('user')->where('id', 1)->lock(true)->find();
if ($user['balance'] >= 100) {
Db::name('user')->where('id', 1)->dec('balance', 100)->update();
}
});
面试官可能问:乐观锁怎么实现? ThinkPHP 没有内置乐观锁,需要手动实现,通常用版本号字段:
$affected = Db::name('user')
->where('id', 1)
->where('version', $version)
->update(['balance' => $newBalance, 'version' => $version + 1]);
if (!$affected) {
throw new \Exception('数据已被修改,请重试');
}
四、面试实战建议
- 不要只背 API,要理解设计意图。比如为什么模型比 Db 重?因为它承载了业务逻辑。
- 结合项目经验回答。面试官更想听到你在实际项目中如何用关联优化查询、如何用事务保证一致性。
- 注意版本差异。ThinkPHP 5.1、6.0、8.0 在模型和事务 API 上有细微差别,面试前确认目标公司使用的版本。
- 准备一个踩坑案例。比如软删除导致唯一索引冲突、事务中缓存不一致等,展示你的排查思路。
掌握以上内容,你在 ThinkPHP 面试中的模型、关联和事务环节基本可以做到对答如流。建议结合官方文档和实际项目反复练习,把知识转化为肌肉记忆。
未经允许不得转载:任鹏个人博客 » ThinkPHP 面试精讲:模型、关联与事务高频面试题

