告别手动排序!Eloquent Sortable让Laravel模型排序效率提升10倍的实战指南

告别手动排序!Eloquent Sortable让Laravel模型排序效率提升10倍的实战指南

【免费下载链接】eloquent-sortable Sortable behaviour for Eloquent models 【免费下载链接】eloquent-sortable 项目地址: https://gitcode.com/gh_mirrors/el/eloquent-sortable

你是否还在为Laravel项目中的模型排序功能编写重复代码?是否遇到过拖拽排序后数据错乱的尴尬?是否在处理软删除模型排序时踩过坑?本文将系统讲解如何使用Eloquent Sortable(一个专为Laravel Eloquent设计的排序行为扩展)解决这些痛点,通过15个实战场景+20段可直接复用的代码,让你彻底掌握模型排序的优雅实现方式。

读完本文你将获得:

  • 3分钟快速集成模型排序功能的完整流程
  • 8种排序操作的最优实现代码(含批量排序/上下移动/置顶置底)
  • 5个进阶场景解决方案(软删除/全局作用域/时间戳忽略)
  • 性能优化指南与常见问题排查清单

为什么选择Eloquent Sortable?

在Laravel开发中,实现模型排序(如文章顺序、产品展示优先级、菜单层级)是高频需求。传统方案通常需要开发者手动维护排序字段、编写更新逻辑,不仅重复劳动多,还容易出现并发安全问题。

Eloquent Sortable(以下简称ES)是由Spatie团队开发的轻量级扩展包,通过Trait注入方式为模型提供完整排序能力。其核心优势在于:

mermaid

核心优势解析

特性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的核心特性是创建模型时自动分配排序号,实现原理如下:

mermaid

通过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()作用域会执行全表扫描;添加索引后,查询性能提升显著:

mermaid

缓存策略

对排序结果进行缓存,减少数据库查询:

// 使用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模型提供了强大的排序能力,核心价值在于:

  1. 代码简化:平均减少80%的排序相关代码
  2. 功能完善:覆盖从基础排序到复杂场景的全需求
  3. 稳定可靠:经过Spatie团队严格测试,广泛应用于生产环境

未来版本可能会增加的功能:

  • 支持多字段组合排序
  • 内置排序历史记录
  • 更细粒度的事件系统

建议所有需要模型排序功能的Laravel项目都集成此扩展包,不仅能提升开发效率,还能避免重复造轮子带来的潜在bug。

最后,附上项目地址与资源链接:

  • 项目仓库:https://gitcode.com/gh_mirrors/el/eloquent-sortable
  • 官方文档:可通过查看项目README.md获取完整使用说明
  • 贡献指南:欢迎提交PR和Issue参与项目改进

希望本文能帮助你彻底掌握Eloquent Sortable的使用技巧,让模型排序功能的开发变得轻松愉快!如果觉得本文对你有帮助,请点赞收藏,并关注获取更多Laravel开发实战指南。

下期预告:《Laravel模型事件系统深度剖析》,带你深入理解Eloquent的生命周期事件机制。

【免费下载链接】eloquent-sortable Sortable behaviour for Eloquent models 【免费下载链接】eloquent-sortable 项目地址: https://gitcode.com/gh_mirrors/el/eloquent-sortable

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值