返回博客
技术 2025年2月17日 4 分钟阅读 · 892 字

Laravel Eloquent ORM 高级技巧:关联关系、预加载与性能调优

从 N+1 查询问题到嵌套预加载,从延迟加载到模型作用域,深入 Eloquent 的高级用法。

#Laravel #Eloquent #PHP #ORM #数据库
本文由 AI 辅助生成,经人工审核发布

N+1 查询问题

Eloquent 最常见的性能陷阱是 N+1 查询:

// ❌ N+1 问题:1 次查用户 + N 次查文章
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count();  // 每次循环触发一条 SQL
}
// SQL: SELECT * FROM users;
// SQL: SELECT * FROM posts WHERE user_id = 1;
// SQL: SELECT * FROM posts WHERE user_id = 2;
// ... 100 个用户 = 101 条 SQL

解决方案:预加载

// ✅ with() 预加载:2 条 SQL 搞定
$users = User::with('posts')->get();
// SQL: SELECT * FROM users;
// SQL: SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...);

嵌套预加载

// 预加载用户的文章 + 文章的标签
$users = User::with('posts.tags')->get();

// 只预加载特定字段
$users = User::with('posts:id,user_id,title')->get();

// 条件预加载
$users = User::with(['posts' => function ($query) {
    $query->where('status', 'published')->orderBy('created_at', 'desc');
}])->get();

关联关系详解

一对多

class User extends Model {
    public function posts() {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model {
    public function user() {
        return $this->belongsTo(User::class);
    }
}

// 使用
$user->posts;           // Collection
$user->posts()->first(); // Builder(可继续链式调用)
$user->posts()->count(); // SQL COUNT,不加载模型

多对多

class User extends Model {
    public function roles() {
        return $this->belongsToMany(Role::class)->withPivot('expires_at');
    }
}

// 中间表额外字段
$user->roles()->attach($roleId, ['expires_at' => now()->addMonth()]);
$user->roles()->sync([1, 2, 3]);  // 同步:添加缺失的,删除多余的
$user->roles()->toggle([1, 2]);   // 切换:有则删,无则加

多态关联

一条评论可以属于文章,也可以属于视频:

class Comment extends Model {
    public function commentable() {
        return $this->morphTo();
    }
}

class Post extends Model {
    public function comments() {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Video extends Model {
    public function comments() {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// 多态预加载
$posts = Post::with(['comments' => function ($q) {
    $q->with('commentable');
}])->get();

一对一(HasOne Through)

用户通过文章表关联到文章的统计数据:

class User extends Model {
    // User → Post → Stat
    public function stat() {
        return $this->hasOneThrough(Stat::class, Post::class);
    }
}

作用域(Scopes)

本地作用域

class Post extends Model {
    public function scopePublished($query) {
        return $query->where('status', 'published')
                     ->where('published_at', '<=', now());
    }
    
    public function scopePopular($query) {
        return $query->withCount('likes')
                     ->having('likes_count', '>', 100)
                     ->orderBy('likes_count', 'desc');
    }
}

// 链式调用
$posts = Post::published()->popular()->limit(10)->get();

动态作用域

public function scopeOfCategory($query, string $category) {
    return $query->where('category', $category);
}

Post::published()->ofCategory('tech')->get();

访问器与修改器

class User extends Model {
    // 访问器:读取时自动处理
    public function getFullNameAttribute() {
        return "{$this->first_name} {$this->last_name}";
    }
    
    // PHP 8.4+ 属性钩子风格
    public function getAvatarUrlAttribute() {
        return $this->avatar 
            ? asset("storage/avatars/{$this->avatar}")
            : asset('img/default-avatar.png');
    }
    
    // 修改器:写入时自动处理
    public function setPasswordAttribute($value) {
        $this->attributes['password'] = bcrypt($value);
    }
}

// 使用
echo $user->full_name;     // 访问器自动调用
$user->password = 'secret'; // 修改器自动加密

集合方法

get() 返回 Collection,支持链式操作:

$users = User::with('posts')->get();

// 过滤
$activeUsers = $users->filter(fn($u) => $u->is_active);

// 映射
$names = $users->map(fn($u) => $u->name);

// 分组
$byRole = $users->groupBy('role');

// 排序
$sorted = $users->sortByDesc('created_at');

// 聚合
$totalPosts = $users->sum(fn($u) => $u->posts->count());

// 链式
$result = $users
    ->filter(fn($u) => $u->posts->isNotEmpty())
    ->map(fn($u) => ['name' => $u->name, 'post_count' => $u->posts->count()])
    ->sortByDesc('post_count')
    ->take(10)
    ->values();

事务与批量操作

事务

DB::transaction(function () {
    $user = User::create(['name' => 'Li']);
    $user->posts()->create(['title' => 'Hello']);
    // 任何异常都会自动回滚
});

批量插入

// ❌ 100 条数据 = 100 条 INSERT
foreach ($data as $item) {
    Post::create($item);
}

// ✅ 1 条 INSERT
Post::insert($data);  // 不触发模型事件
Post::upsert($data, ['slug'], ['title']);  // MySQL UPSERT

批量更新

// Laravel 8+ chunkById 避免 OOM
User::where('status', 'inactive')->chunkById(200, function ($users) {
    foreach ($users as $user) {
        $user->update(['notified_at' => now()]);
    }
});

查询优化

select 避免拉取不需要的列

// ❌ SELECT * FROM users
$users = User::all();

// ✅ 只取需要的列
$users = User::select('id', 'name', 'email')->get();

exists 代替 count

// ❌ 加载整个集合再 count
if (User::where('active', 1)->get()->isNotEmpty()) { ... }

// ✅ SQL EXISTS,不加载模型
if (User::where('active', 1)->exists()) { ... }

总结

Eloquent 的高级技巧核心是:

  1. 预加载消灭 N+1
  2. 作用域封装常用查询条件
  3. 访问器/修改器自动处理字段格式
  4. 集合方法替代 foreach 循环
  5. 批量操作减少 SQL 执行次数
  6. select + exists 减少数据传输量