
本文深入探讨了在 Laravel Eloquent 多对多关系中,如何高效地识别并删除那些没有关联任何子模型的父级记录。我们将介绍使用 `whereDoesntHave` 方法进行关系筛选的直接方案,并进一步提供通过引入计数缓存列来优化大规模数据查询性能的高级策略,确保数据一致性与系统效率。
在复杂的数据库应用中,我们经常会遇到多对多关系。例如,一个 Order(订单)可以包含多个 Aircon(空调),反之亦然。在这种场景下,有时我们需要清理数据库,删除那些已经没有任何关联 Aircon 的 Order 记录。这不仅有助于保持数据整洁,还能提升查询效率。本文将详细介绍两种实现此目标的方法。
方法一:使用 whereDoesntHave 筛选无关联记录
Laravel Eloquent 提供了一系列强大的关系查询方法,其中 whereDoesntHave 是解决此类问题的理想选择。它允许我们筛选出不包含任何指定关联模型的父级模型实例。
理解 whereDoesntHave
whereDoesntHave 方法接收一个关系名称作为参数。当此方法被调用时,Eloquent 会生成一个子查询,用于检查父模型是否没有任何关联的子模型。如果父模型没有对应的子模型记录,则该父模型将被包含在结果集中。
实施删除操作
假设我们有一个 Order 模型和一个 Aircon 模型,它们之间通过 aircons 方法定义了多对多关系。我们希望删除当前用户下所有没有关联任何空调的订单。
use App/Models/Order;
use Illuminate/Support/Facades/Auth;
/**
* 识别并删除当前用户下没有关联任何空调的订单。
*
* @return int 被删除的订单数量
*/
function deleteOrdersWithoutAirconsByCurrentUser(): int
{
// 使用 whereDoesntHave 筛选出没有关联 'aircons' 的订单
// 结合 where('user_id', Auth::id()) 进一步限定为当前用户的订单
$deletedCount = Order::whereDoesntHave('aircons')
->where('user_id', Auth::id())
->delete(); // 直接执行删除操作
return $deletedCount;
}
// 调用函数执行删除
$count = deleteOrdersWithoutAirconsByCurrentUser();
echo "成功删除了 {$count} 个没有关联空调的订单。/n";
代码解析:
- Order::whereDoesntHave(‘aircons’):这会筛选出所有在 order_aircon 中间表中没有对应记录的 Order。
- ->where(‘user_id’, Auth::id()):进一步将筛选范围缩小到当前登录用户创建的订单。
- ->delete():直接执行 SQL DELETE 语句,删除所有匹配条件的订单记录。此方法会返回被删除的记录数量。
优点:
- 简洁明了: 代码易于理解和维护,符合 Eloquent 的设计哲学。
- 无需手动维护: 关系状态由数据库自动管理,无需额外逻辑。
缺点:
- 性能考量: 对于非常大的数据集,whereDoesntHave 会生成一个 NOT EXISTS 或 LEFT JOIN … WHERE … IS NULL 子查询。虽然 SQL 优化器通常能很好地处理,但在极端情况下,其性能可能不如直接基于计数列的查询。
方法二:通过计数缓存列优化性能
当处理的订单和空调数量庞大时,每次都执行关系查询可能会带来性能瓶颈。为了解决这个问题,我们可以引入一个计数缓存列,例如在 orders 表中添加一个 aircons_count 字段,用于存储每个订单关联的空调数量。
数据库迁移:添加计数列
首先,我们需要为 orders 表添加 aircons_count 列。
// database/migrations/YYYY_MM_DD_HHMMSS_add_aircons_count_to_orders_table.php
use Illuminate/Database/Migrations/Migration;
use Illuminate/Database/Schema/Blueprint;
use Illuminate/Support/Facades/Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
// 添加 aircons_count 列,默认为 0
$table->unsignedInteger('aircons_count')->default(0)->after('user_id');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('aircons_count');
});
}
};
执行迁移:php artisan migrate
维护计数列的逻辑
引入计数列后,关键在于确保其值的准确性。这意味着每当一个 Order 关联或取消关联一个 Aircon 时,都需要相应地更新 orders.aircons_count。这可以通过在中间件、服务层、或者更优雅地通过 Eloquent 事件来实现。
使用 Eloquent 事件(推荐):
我们可以在 Order 模型或 Aircon 模型中监听 attached 和 detached 事件(对于多对多关系)。
// app/Models/Order.php
namespace App/Models;
use Illuminate/Database/Eloquent/Factories/HasFactory;
use Illuminate/Database/Eloquent/Model;
class Order extends Model
{
use HasFactory;
protected $fillable = ['user_id', 'aircons_count'];
public function aircons()
{
return $this->belongsToMany(Aircon::class)->withTimestamps();
}
protected static function booted(): void
{
// 当 Aircon 被关联到 Order 时
static::pivotAttached(function ($model, $relationName, $pivotIds, $pivotAttributes) {
if ($relationName === 'aircons') {
$model->increment('aircons_count');
}
});
// 当 Aircon 从 Order 解除关联时
static::pivotDetached(function ($model, $relationName, $pivotIds) {
if ($relationName === 'aircons') {
$model->decrement('aircons_count');
}
});
}
}
注意:
- pivotAttached 和 pivotDetached 事件在 Laravel 8 中可以通过在模型中定义 booted 方法来监听。
- 确保在 belongsToMany 关系中使用了 ->withTimestamps(),这对于事件的正确触发是必要的。
- 对于已有的数据,你可能需要运行一次性脚本来初始化 aircons_count。
使用计数列进行高效查询
一旦 aircons_count 列被正确维护,查询那些没有关联空调的订单就变得非常简单和高效。
use App/Models/Order;
use Illuminate/Support/Facades/Auth;
/**
* 识别并删除当前用户下没有关联任何空调的订单 (使用计数缓存列)。
*
* @return int 被删除的订单数量
*/
function deleteOrdersWithoutAirconsOptimized(): int
{
// 直接查询 aircons_count 为 0 的订单
$deletedCount = Order::where('aircons_count', 0)
->where('user_id', Auth::id())
->delete();
return $deletedCount;
}
// 调用函数执行删除
$count = deleteOrdersWithoutAirconsOptimized();
echo "成功删除了 {$count} 个没有关联空调的订单 (通过计数缓存)。/n";
代码解析:
- Order::where(‘aircons_count’, 0):直接基于整数列进行查询,这是数据库最擅长的操作之一,速度极快。
- ->where(‘user_id’, Auth::id()):同样结合用户 ID 进行过滤。
- ->delete():执行删除。
优点:
- 极高性能: 查询速度非常快,尤其是在大数据量下,因为避免了复杂的 JOIN 或子查询。
- 可扩展性: 这种模式适用于需要频繁根据关联数量进行筛选的场景。
缺点:
- 维护成本: 需要额外的逻辑来确保 aircons_count 的准确性。如果维护逻辑有缺陷,可能导致数据不一致。
- 额外存储: 增加了数据库表的列数。
总结与选择
两种方法都能有效地识别并删除没有关联子模型的父级记录,但各有侧重:
-
whereDoesntHave 方法:
- 适用场景: 数据量适中,或者对性能要求不是极致苛刻,追求代码简洁和开发效率的场景。
- 优势: 开发简单,无需额外维护。
- 劣势: 大数据量下性能可能不如计数缓存列。
-
计数缓存列方法:
- 适用场景: 数据量非常大,且对查询性能有严格要求的系统;或者需要频繁根据关联数量进行筛选的场景。
- 优势: 查询性能极高。
- 劣势: 需要额外开发和维护逻辑来确保计数列的准确性,增加了系统的复杂性。
在实际项目中,开发者应根据具体的数据规模、性能需求以及开发资源来选择最合适的方法。对于大多数中小型应用,whereDoesntHave 已经足够。但对于高并发、大数据量的场景,引入计数缓存列无疑是提升系统响应速度的有效策略。无论选择哪种方法,都应确保充分测试,以验证其正确性和性能表现。
以上就是Laravel Eloquent:高效识别与删除无关联子模型的父级记录的详细内容,更多请关注php中文网其它相关文章!


