告别手动排序!Eloquent Sortable让Laravel模型排序效率提升10倍的实战指南
你是否还在为Laravel项目中的模型排序功能编写重复代码?是否遇到过拖拽排序后数据错乱的尴尬?是否在处理软删除模型排序时踩过坑?本文将系统讲解如何使用Eloquent Sortable(一个专为Laravel Eloquent设计的排序行为扩展)解决这些痛点,通过15个实战场景+20段可直接复用的代码,让你彻底掌握模型排序的优雅实现方式。
读完本文你将获得:
- 3分钟快速集成模型排序功能的完整流程
- 8种排序操作的最优实现代码(含批量排序/上下移动/置顶置底)
- 5个进阶场景解决方案(软删除/全局作用域/时间戳忽略)
- 性能优化指南与常见问题排查清单
为什么选择Eloquent Sortable?
在Laravel开发中,实现模型排序(如文章顺序、产品展示优先级、菜单层级)是高频需求。传统方案通常需要开发者手动维护排序字段、编写更新逻辑,不仅重复劳动多,还容易出现并发安全问题。
Eloquent Sortable(以下简称ES)是由Spatie团队开发的轻量级扩展包,通过Trait注入方式为模型提供完整排序能力。其核心优势在于:
核心优势解析
| 特性 | Eloquent Sortable | 手动实现 | 其他排序包 |
|---|---|---|---|
| 代码量 | 3行Trait引入 | 平均50+行 | 5-10行配置 |
| 排序算法 | 内置优化查询 | 需手动优化 | 基础实现 |
| 软删除支持 | 原生兼容 | 需额外开发 | 部分支持 |
| 事件系统 | 排序事件触发 | 需自行实现 | 无 |
| 批量操作 | 支持ID数组排序 | 需手动循环 | 有限支持 |
| 全局作用域 | 自定义查询支持 | 复杂条件拼接 | 不支持 |
快速开始:3分钟集成指南
环境要求
- Laravel 5.8+(推荐8.x/9.x版本)
- PHP 7.2+
- Composer依赖管理工具
安装步骤
# 1. 安装扩展包
composer require spatie/eloquent-sortable
# 2. 发布配置文件(可选)
php artisan vendor:publish --provider="Spatie\EloquentSortable\EloquentSortableServiceProvider"
配置文件位于config/eloquent-sortable.php,默认配置如下:
return [
// 默认排序字段名
'order_column_name' => 'order_column',
// 创建时自动排序
'sort_when_creating' => true,
// 排序时忽略时间戳更新
'ignore_timestamps' => false,
];
基础实现
以文章模型(Post)为例,实现排序功能仅需3步:
// 1. 数据库迁移添加排序字段
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->unsignedInteger('order_column')->default(0); // 排序字段
$table->timestamps();
});
// 2. 模型集成Sortable Trait
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
class Post extends Model implements Sortable
{
use SortableTrait;
// 排序配置(可选,覆盖默认值)
public $sortable = [
'order_column_name' => 'order_column',
'sort_when_creating' => true,
];
}
// 3. 使用排序功能
$posts = Post::ordered()->get(); // 按排序字段正序查询
核心功能详解
自动排序机制
ES的核心特性是创建模型时自动分配排序号,实现原理如下:
通过shouldSortWhenCreating()方法控制是否自动排序,可在模型中重写该方法实现复杂逻辑:
public function shouldSortWhenCreating(): bool
{
// 仅管理员创建的记录自动排序
return auth()->user()->isAdmin();
}
排序查询作用域
ordered()作用域提供便捷的排序查询,支持正序/倒序切换:
// 基本用法:正序排列
$posts = Post::ordered()->get();
// 倒序排列
$posts = Post::ordered('desc')->get();
// 结合其他条件
$posts = Post::where('category_id', 5)->ordered()->paginate(10);
底层实现代码:
public function scopeOrdered(Builder $query, string $direction = 'asc')
{
return $query->orderBy($this->determineOrderColumnName(), $direction);
}
批量排序操作
setNewOrder()方法支持通过ID数组快速重排,特别适合前端拖拽排序场景:
// 前端传递的ID顺序数组
$newOrder = [3, 1, 4, 2]; // 新排序后的ID序列
// 执行批量排序
Post::setNewOrder($newOrder);
// 从指定序号开始排序(默认从1开始)
Post::setNewOrder($newOrder, 10); // 序号从10开始递增
// 自定义主键列(适用于非id主键)
Post::setNewOrderByCustomColumn('uuid', $uuidOrderArray);
注意:批量排序会触发
EloquentModelSortedEvent事件,可通过监听该事件记录排序日志或执行后续操作。
进阶场景解决方案
1. 带软删除模型的排序
当模型使用软删除时,默认排序会忽略已删除记录。如需包含软删除模型,需重写buildSortQuery()方法:
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model implements Sortable
{
use SortableTrait, SoftDeletes;
// 重写排序查询构建方法
public function buildSortQuery(): Builder
{
return static::query()->withTrashed();
}
}
// 测试代码(来自SortableTest.php)
public function it_can_get_the_highest_order_number_with_trashed_models()
{
$this->setUpSoftDeletes();
DummyWithSoftDeletes::first()->delete();
$this->assertEquals(
DummyWithSoftDeletes::withTrashed()->count(),
(new DummyWithSoftDeletes())->getHighestOrderNumber()
);
}
2. 忽略时间戳更新
批量排序时频繁更新updated_at字段可能影响性能,可通过配置禁用:
// 全局配置
config(['eloquent-sortable.ignore_timestamps' => true]);
// 模型级别配置(优先于全局)
public $sortable = [
'ignore_timestamps' => true,
];
实现原理是利用Laravel的$timestamps属性临时禁用时间戳更新:
// 核心代码片段
if (config('eloquent-sortable.ignore_timestamps', false)) {
static::$ignoreTimestampsOn = array_values(array_merge(
static::$ignoreTimestampsOn, [static::class]
));
}
// 排序操作...
static::$ignoreTimestampsOn = array_values(array_diff(
static::$ignoreTimestampsOn, [static::class]
));
3. 处理全局作用域
当模型应用了全局作用域(如只查询激活状态记录),排序可能遗漏部分数据,需在排序时临时移除作用域:
// 方法1:通过modifyQuery回调
Post::setNewOrder($ids, 1, null, function($query) {
$query->withoutGlobalScope(ActiveScope::class);
});
// 方法2:重写buildSortQuery方法
public function buildSortQuery(): Builder
{
return static::query()->withoutGlobalScope('active');
}
// 测试用例(来自SortableTest.php)
public function it_can_set_new_order_without_global_scopes_models()
{
$this->setUpIsActiveFieldForGlobalScope();
$newOrder = Collection::make(Dummy::all()->pluck('id'))->shuffle()->toArray();
DummyWithGlobalScope::setNewOrder($newOrder, 1, null, function ($query) {
$query->withoutGlobalScope('ActiveScope');
});
foreach (Dummy::orderBy('order_column')->get() as $i => $dummy) {
$this->assertEquals($newOrder[$i], $dummy->id);
}
}
常用排序操作代码集
单个模型排序
// 获取排序相关信息
$model = Post::find(1);
$highestOrder = $model->getHighestOrderNumber(); // 获取最大序号
$lowestOrder = $model->getLowestOrderNumber(); // 获取最小序号
$isFirst = $model->isFirstInOrder(); // 是否为第一个
$isLast = $model->isLastInOrder(); // 是否为最后一个
// 移动操作
$model->moveOrderUp(); // 向上移动(与前一个交换)
$model->moveOrderDown(); // 向下移动(与后一个交换)
$model->moveToStart(); // 移到最前
$model->moveToEnd(); // 移到最后
$model->moveBefore($targetModel); // 移到目标模型之前
$model->moveAfter($targetModel); // 移到目标模型之后
// 交换排序
$model->swapOrderWithModel($anotherModel); // 与另一个模型交换位置
Post::swapOrder($model1, $model2); // 静态方法交换两个模型
批量排序操作
// 基础批量排序
$ids = [3, 1, 4, 2]; // 新的ID顺序
Post::setNewOrder($ids);
// 指定起始序号
Post::setNewOrder($ids, 10); // 序号从10开始,依次为10,11,12,13...
// 使用自定义主键
Post::setNewOrderByCustomColumn('uuid', $uuidArray);
// 结合集合使用
$posts = Post::all();
Post::setNewOrder($posts->shuffle()->pluck('id'));
// 带查询修改器的批量排序
Post::setNewOrder($ids, 1, null, function($query) {
$query->where('category_id', request('category_id'));
});
性能优化指南
数据库索引
为排序字段添加索引是提升查询性能的关键:
// 迁移文件中添加索引
$table->unsignedInteger('order_column')->index()->default(0);
未添加索引时,ordered()作用域会执行全表扫描;添加索引后,查询性能提升显著:
缓存策略
对排序结果进行缓存,减少数据库查询:
// 使用Laravel缓存
$posts = Cache::remember('sorted_posts', 60, function () {
return Post::ordered()->get();
});
// 排序更新时清除缓存
Post::setNewOrder($ids);
Cache::forget('sorted_posts');
分批次排序
处理大量数据排序时,采用分批次更新减少锁表时间:
// 批量排序优化(处理1000+条记录)
$chunkSize = 50;
$ids = Post::pluck('id')->shuffle()->toArray();
$chunks = array_chunk($ids, $chunkSize);
foreach ($chunks as $chunk) {
$start = $chunk === reset($chunks) ? 1 : null;
Post::setNewOrder($chunk, $start);
usleep(10000); // 短暂延迟,避免数据库压力
}
常见问题与解决方案
Q1: 排序号出现重复怎么办?
A: 执行序号修复命令:
// 修复指定模型的排序号
public function fixOrderNumbers()
{
$models = Post::orderBy('order_column')->get();
$order = 1;
foreach ($models as $model) {
$model->order_column = $order++;
$model->saveQuietly(); // 静默保存不触发事件
}
return "修复完成,共处理 {$order} 条记录";
}
Q2: 如何实现分组排序(如同一分类下独立排序)?
A: 重写buildSortQuery方法:
public function buildSortQuery(): Builder
{
return static::query()->where('category_id', $this->category_id);
}
// 使用示例
$categoryId = 5;
$posts = Post::where('category_id', $categoryId)->ordered()->get();
$newPost = new Post();
$newPost->category_id = $categoryId;
$newPost->save(); // 自动分配该分类下的最大序号+1
Q3: 排序操作触发了不必要的模型事件?
A: 使用saveQuietly()替代save():
// 模型中重写相关方法
public function moveOrderUp(): static
{
// ...原有逻辑...
$this->saveQuietly(); // 静默保存
return $this;
}
完整案例:实现拖拽排序功能
结合前端Vue.js实现完整的拖拽排序功能:
后端API
// routes/api.php
Route::patch('/posts/sort', [PostController::class, 'sort']);
// PostController.php
public function sort(Request $request)
{
$ids = $request->input('ids');
Post::setNewOrder($ids);
return response()->json([
'status' => 'success',
'message' => '排序更新成功'
]);
}
前端实现(Vue+SortableJS)
<template>
<div class="post-list" ref="postList">
<div v-for="post in posts" :key="post.id" class="post-item">
{{ post.title }}
</div>
</div>
</template>
<script>
import Sortable from 'sortablejs';
import axios from 'axios';
export default {
data() {
return {
posts: []
};
},
mounted() {
this.fetchPosts();
this.initSortable();
},
methods: {
async fetchPosts() {
const { data } = await axios.get('/api/posts');
this.posts = data;
},
initSortable() {
const el = this.$refs.postList;
const sortable = new Sortable(el, {
animation: 150,
onEnd: this.handleSortEnd
});
},
async handleSortEnd(evt) {
const ids = this.posts.map(post => post.id);
await axios.patch('/api/posts/sort', { ids });
this.$notify({ type: 'success', message: '排序已更新' });
}
}
};
</script>
总结与展望
Eloquent Sortable通过Trait方式为Laravel模型提供了强大的排序能力,核心价值在于:
- 代码简化:平均减少80%的排序相关代码
- 功能完善:覆盖从基础排序到复杂场景的全需求
- 稳定可靠:经过Spatie团队严格测试,广泛应用于生产环境
未来版本可能会增加的功能:
- 支持多字段组合排序
- 内置排序历史记录
- 更细粒度的事件系统
建议所有需要模型排序功能的Laravel项目都集成此扩展包,不仅能提升开发效率,还能避免重复造轮子带来的潜在bug。
最后,附上项目地址与资源链接:
- 项目仓库:https://gitcode.com/gh_mirrors/el/eloquent-sortable
- 官方文档:可通过查看项目README.md获取完整使用说明
- 贡献指南:欢迎提交PR和Issue参与项目改进
希望本文能帮助你彻底掌握Eloquent Sortable的使用技巧,让模型排序功能的开发变得轻松愉快!如果觉得本文对你有帮助,请点赞收藏,并关注获取更多Laravel开发实战指南。
下期预告:《Laravel模型事件系统深度剖析》,带你深入理解Eloquent的生命周期事件机制。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



