
本文旨在解决在使用 laravel ORM 时,如何基于多个列的值来移除重复记录的问题。通过使用 sql 的窗口函数 `ROW_NUMBER()`,结合 Laravel 的 DB facade,提供了一种高效且易于理解的解决方案,避免了循环遍历和复杂的逻辑判断,并附带详细代码示例,帮助开发者快速实现该功能。
在使用 Laravel ORM 进行数据查询时,有时需要根据多个字段的值来判断记录是否重复,并只保留其中的一条。例如,在保存用户提交的比赛结果时,如果只更新分数,而不希望每次提交都创建一条新记录,就需要去除基于用户和各个科目的分数相同的重复记录,只保留最新的那一条。Laravel 提供的 distinct() 方法通常只能用于单个字段的去重,无法满足这种多列去重的需求。以下介绍一种使用 SQL 窗口函数结合 Laravel DB facade 实现多列去重的方案。
使用 SQL 窗口函数 ROW_NUMBER() 去重
核心思路是利用 SQL 的窗口函数 ROW_NUMBER() 为每个分组(基于需要去重的列)内的记录分配一个序号,然后只保留序号为 1 的记录。
ROW_NUMBER() OVER (PARTITION BY column1, column2, … ORDER BY column_to_order DESC)
- PARTITION BY column1, column2, …: 指定分组的列,具有相同值的记录将被分到同一个组。
- ORDER BY column_to_order DESC: 指定组内记录的排序方式,通常按照主键或时间戳倒序排列,以便保留最新的记录。
以下是一个 SQL 示例,假设我们有一个名为 results 的表,需要基于 username、discipline_one 到 discipline_six 这几个字段去重,并保留 id 最大的那条记录:
WITH cte AS ( select *, ROW_NUMBER() OVER (PARTITION BY username, discipline_one, discipline_two, discipline_three, discipline_four, discipline_five, discipline_six ORDER BY id DESC) rn FROM results ) SELECT id, username, discipline_one, discipline_two, discipline_three, discipline_four, discipline_five, discipline_six, created_at, updated_at FROM cte WHERE rn = 1
这个sql语句首先使用Common table Expression (CTE) cte,计算每个分组内的行号,并将结果存储在名为 rn 的列中。然后,从 cte 中选择 rn = 1 的记录,即每个分组内的第一条记录,也就是最新的记录。
在 Laravel 中实现多列去重
为了在 Laravel 中使用上述 SQL 语句,可以借助 Laravel 的 DB facade,它允许直接执行原始 SQL 查询。
use IlluminateSupportFacadesDB;  public static function waybackWhen($result) {     $username = $result->username;      // 临时关闭 mysql 严格模式 (可选,如果需要)     config()->set('database.connections.mysql.strict', false);     DB::reconnect(); // 重新连接数据库,使配置生效      $resultSet = DB::select("         WITH cte AS (             SELECT *, ROW_NUMBER() OVER             (PARTITION BY username, discipline_one, discipline_two, discipline_three,              discipline_four, discipline_five, discipline_six ORDER BY id DESC) rn             FROM results         )          SELECT id, username, discipline_one, discipline_two, discipline_three,              discipline_four, discipline_five, discipline_six, created_at, updated_at         FROM cte         WHERE rn = 1 AND username = ?    ", [ $username ]);      // 恢复 MySQL 严格模式 (可选)     config()->set('database.connections.mysql.strict', true);     DB::reconnect();      return $resultSet; }
代码解释:
- DB::select(): 使用 DB facade 的 select() 方法执行 SQL 查询。
- SQL 查询: 使用上面提到的 SQL 语句,注意使用 ? 占位符来防止 SQL 注入。
- [ $username ]: 将 $username 变量作为参数传递给 SQL 查询,替换占位符。
- 严格模式: 如果MySQL开启了严格模式,执行此查询可能会报错。通过config()->set(‘database.connections.mysql.strict’, false);可以临时关闭严格模式,并在查询完成后恢复。需要注意的是,修改配置后需要重新连接数据库才能生效。
使用示例:
$result = appModelsResult::find(1); // 假设有一个 $result 对象 $waybackWhen = AppModelsResult::waybackWhen($result); // $waybackWhen 现在包含了去重后的结果集 dd($waybackWhen);
注意事项
- SQL 注入: 在使用 DB facade 执行原始 SQL 查询时,务必使用参数绑定(? 占位符)来防止 SQL 注入攻击。
- 性能: 对于大数据量的表,使用窗口函数可能会影响性能。可以考虑添加合适的索引来优化查询。
- 数据库兼容性: 不同的数据库系统对窗口函数的支持可能略有不同。请根据实际使用的数据库系统调整 SQL 语句。
- 严格模式: 在MySQL严格模式下,如果SQL语句不符合严格模式的要求,可能会报错。需要根据实际情况选择是否临时关闭严格模式。
总结
本文介绍了一种基于 SQL 窗口函数和 Laravel DB facade 实现多列去重的方案。该方案简洁高效,避免了复杂的循环遍历和逻辑判断,能够有效地解决 Laravel ORM 在多列去重方面的不足。通过合理地使用 SQL 窗口函数,可以极大地简化数据处理逻辑,提高开发效率。


