
本文详解如何将含子查询、关联统计与条件过滤的原生 sql 转换为可维护、可读性强的 laravel eloquent 写法,涵盖关系定义、withcount()、wherehas() 及模型作用域(scopes)的最佳实践。
本文详解如何将含子查询、关联统计与条件过滤的原生 sql 转换为可维护、可读性强的 laravel eloquent 写法,涵盖关系定义、withcount()、wherehas() 及模型作用域(scopes)的最佳实践。
在 Laravel 开发中,将原生 SQL 迁移至 Eloquent 或 Query Builder 不仅提升代码可维护性,更能充分利用框架的自动关联、软删除、作用域等特性。以下以你提供的原始 SQL 为例,逐步完成专业级重构。
✅ 第一步:补全缺失的关系定义
你当前的 Server 和 User 模型已定义多对多关系(server_users),但原始 SQL 中还涉及 server_user_cancellations 表的计数逻辑,因此需补充 一对多 关系(因每条 cancellation 记录属于一个 Server):
// app/Models/Server.php
public function users()
{
return $this->belongsToMany(User::class, 'server_users');
}
// 新增:一对多关系(一个 Server 可有多个 cancellation 记录)
public function cancellations()
{
return $this->hasMany(ServerUserCancellation::class);
}
// app/Models/User.php
public function servers()
{
return $this->belongsToMany(Server::class, 'server_users');
}
// 注意:无需额外定义 cancellations 关系,因统计目标是「当前用户在某 Server 下是否有 cancellation」,由 Server 端反查更自然
⚠️ 提示:确保已创建 ServerUserCancellation 模型(如未创建,请运行 php artisan make:model ServerUserCancellation),并设置对应表名和主键:
protected $table = 'server_user_cancellations';
✅ 第二步:使用 whereHas() 替代 IN (SELECT ...) 子查询
原始 SQL 中的 WHERE s.id IN (SELECT server_id FROM server_users WHERE user_id = ?) 是典型的「存在关联用户」逻辑,Eloquent 提供 whereHas() 完美替代:
->whereHas('users', fn ($q) => $q->where('user_id', auth()->id()))
该语句会自动生成内连接 + 条件,语义清晰且避免 N+1。
✅ 第三步:用 withCount() 替代相关子查询统计
原始 SQL 中的 (SELECT count(id) FROM server_user_cancellations WHERE server_id = s.id AND user_id = ?) as exist 是典型的「按关联条件统计数量」场景。Eloquent 的 withCount() 支持闭包定制条件,并支持别名映射:
->withCount(['cancellations as exist' => fn ($q) => $q->where('user_id', auth()->id())])
执行后,每个 Server 实例将自动附加 exist 属性(整型,即匹配的 cancellation 数量)。
✅ 第四步:整合过滤条件,启用软删除与作用域优化
原始 SQL 包含三个关键过滤:
- s.is_active = true
- s.is_installed = true(注意:你的迁移中未定义该字段,请确认是否遗漏;若存在,需添加)
- s.deleted_at IS NULL
Laravel 的 SoftDeletes trait 可自动处理 deleted_at,而 local scopes 则让业务条件复用更优雅:
// app/Models/Server.php
use Illuminate\Database\Eloquent\SoftDeletes;
class Server extends Model
{
use SoftDeletes; // 自动排除软删除记录,无需手动 whereNull('deleted_at')
protected $fillable = ['name', 'ip', 'username', 'is_active', 'is_installed'];
// 本地作用域
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeInstalled($query)
{
return $query->where('is_installed', true);
}
}
? 验证:请检查 servers 表迁移中是否包含 is_installed 字段。若尚未添加,执行迁移:
php artisan make:migration add_is_installed_to_servers_table并在 up() 方法中添加 $table->boolean('is_installed')->default(false);
✅ 最终 Eloquent 查询(控制器中)
use App\Models\Server;
$servers = Server::active()
->installed()
->whereHas('users', fn ($q) => $q->where('user_id', auth()->id()))
->withCount(['cancellations as exist' => fn ($q) => $q->where('user_id', auth()->id())])
->select('id', 'name', 'ip') // 显式指定字段,避免加载冗余列
->get();
// 返回 JSON 响应(适配 $request->wantsJson())
return response()->json($servers);
? 输出结构说明
结果集合中每个 Server 对象将包含:
{
"id": 1,
"name": "SM",
"ip": "192.168.1.100",
"exist": 2 // 当前用户在该 Server 下的 cancellation 记录数(0 表示不存在)
}
✅ 进阶建议
- 性能优化:若数据量大,可在 server_user_cancellations(server_id, user_id) 和 server_users(server_id, user_id) 上添加复合索引。
- API 封装:将上述查询提取为 Server 模型的静态方法(如 forCurrentUser()),进一步提升复用性。
- 类型安全:配合 Laravel 9+ 的 PHP 8.1+ 属性类型声明与返回类型提示,增强 IDE 支持与可维护性。
通过以上重构,你不仅完成了 SQL 到 Eloquent 的精准转换,更构建了符合 Laravel 最佳实践、易于测试与扩展的业务逻辑层。











