Laravel 编码技巧 - 模型关系

作者: 温新

图书: 【Laravel 编码技巧】

阅读: 181

时间: 2024-03-01 18:47:06

模型关联中使用 Order By

你可以在关联关系中直接指定 orderBy()

public function products()
{
    return $this->hasMany(Product::class);
}

public function productsByName()
{
    return $this->hasMany(Product::class)->orderBy('name');
}

将 where 语句添加到多对多关系

在多对多关系中,可以使用 wherePivot 方法将 where 语句添加到数据中间表中。

class Developer extends Model
{
     // 获取与此开发人员相关的所有客户端
     public function clients()
     {
          return $this->belongsToMany(Clients::class);
     }

     // 仅获取本地客户端
     public function localClients()
     {
          return $this->belongsToMany(Clients::class)->wherePivot('is_local', true);
     }
}

获取另一个关系的最新(或最旧)的数据

从 Laravel 8.42 开始,在 Eloquent 模型中,你可以定义一个关系,该关系将获取另一个关系的最新(或最旧)项。

/**
 * 获取用户的最新订单
 */
public function latestOrder()
{
    return $this->hasOne(Order::class)->latestOfMany();
}

/**
 * 获取用户最早的订单
 */
public function oldestOrder()
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

条件关系

如果您注意到您经常使用带有附加 where 条件的相同关系,那么你可以创建一个单独的关系方法。

模型

public function comments()
{
    return $this->hasMany(Comment::class);
}

public function approved_comments()
{
    return $this->hasMany(Comment::class)->where('approved', 1);
}

DB 原生查询 havingRaw ()

你可以在很多地方使用原始数据库查询,比如在 groupBy() 后面调用 havingRaw()

Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();

Eloquent 使用 has() 实现多层调用查询

你可以在关联关系查询中使用 has() 实现两层关联查询。

// Author -> hasMany(Book::class);
// Book -> hasMany(Rating::class);
$authors = Author::has('books.ratings')->get();

一对多关系中获取符合指定数量的信息

hasMany()中,你可以通过条件过滤,获取符合的数据。

// Course -> hasMany(Lesson::class)
$course = Course::query()->has('lessons', '>', 20)->get();

默认模型

你可以在 belongsTo 关系中设置返回一个默认的模型,从而避免类似于使用 {{ $post->user->name }} 当 $post->user 不存在的时候,引起的致命的错误。

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

一对多关系中创建多条关联数据

在一对多关系中,你可以使用 saveMany 通过一次提交,保存多条关联数据。

$course = Course::query()->find(1);

$course->lessons()->saveMany([
    new Lesson(['name' => 'lesson 1']),
    new Lesson(['name' => 'lesson 2']),
]);

多层级预加载

Laravel 中,你可以在一条语句中预加载多个层级,在这个例子中,我们不仅加载作者关系,而且还加载作者模型上的国家关系。

$users = Book::with('author.country')->get();

预加载指定字段

你可以在 Laravel 中预加载并指定关联中的特定字段。

$users = Book::with('author:id,name')->get();

你同样可以在深层级中这样做,如第二层级关系:

$users = Book::with('author.country:id,name')->get();

轻松更新父级 updated_at

如果你正在更新一条记录,并且想要更新父关系的 updated_at 列(比如,你添加了新的帖子评论,并且想要更新 posts.updated_at),只需在子模型上使用 $touches ='post']; 属性。

class Comment extends Model
{
    protected $touches = ['post'];
}

始终检查是否存在关联

永远不要在执行 $model->relationship->field 时不检查 relationship 对象是否仍然存在。

它可能会因为任何原因被删除,在你的代码之外,被其他人排队的作业等。可以使用 if-else 语句,或者在 Blade 中使用 {{ model->relationship->field ?? '' }},或者使用 {{ optional(model->relationship)->field }}。在 PHP 8 中,甚至可以使用 nullsafe 操作符 {{ $model->relationship?->field }}

使用 withCount() 统计关联记录数

如果你有 hasMany() 的关联,并且你想统计子关联记录的条数,不要写一个特殊的查询。例如,如果你的用户模型上有帖子和评论,使用 withCount()

$courses = Course::query()->withCount('lessons')->get();

结果

[
    {
        "id": 1,
        "name": "Clement Strosin",
        "created_at": "2023-11-09T08:53:06.000000Z",
        "updated_at": "2023-11-09T08:53:06.000000Z",
        "lessons_count": 21
    }
]

同时,在 Blade 文件中,您可以通过使用 {relationship}_count 属性获得这些数量

@foreach ($courses as $course)
<tr>
    <td>{{ $course->name }}</td>
    <td class="text-center">{{ $course->lessons_count }}</td>
</tr>
@endforeach

也可以按照这些统计字段进行排序:

Course::query()->withCount('lessons')->orderBy('lessons_count', 'desc')->get();

关联关系中过滤查询

如果你想加载关系数据,你可以在闭包函数中指定一些限制或排序。例如,如果你只想获取拥有最多三个最大城市的国家,以下是代码:

$countries = Country::with(['cities' => function($query) {
    $query->orderBy('population', 'desc');
}])->get();

动态加载相关模型

你不仅可以实现对关联模型的实时预加载,还可以根据情况动态设置某些关联关系,需要在模型初始化方法中处理:

class ProductTag extends Model
{
    protected $with = ['product'];

    public function __construct() {
        parent::__construct();
        $this->with = ['product'];

        if (auth()->check()) {
            $this->with[] = 'user';
        }
    }
}

使用 hasMany 替代 belongsTo

在关联关系中,如果创建子关系的记录中需要用到父关系的 ID ,那么使用 hasMany 比使用 belongsTo 更简洁。

// if Post -> belongsTo(User), and User -> hasMany(Post)...
// 而不是传递 user_id...
Post::create([
    'user_id' => auth()->id(),
    'title' => request()->input('title'),
    'post_text' => request()->input('post_text'),
]);

// 可以这样做
auth()->user()->posts()->create([
    'title' => request()->input('title'),
    'post_text' => request()->input('post_text'),
]);

重命名 pivot 表名

如果你想要重命名pivot并用其他的什么方式来调用关系,你可以在你的关系声明中使用 ->as('name') 来为关系取名。

模型

public function podcasts() {
    return $this->belongsToMany(Podcast::class)
        ->as('subscription')
        ->withTimestamps();
}

控制器

$podcasts = $user->podcasts();

foreach ($podcasts as $podcast) {
    // instead of $podcast->pivot->created_at ...
    echo $podcast->subscription->created_at;
}

在一行代码中更新父级

如果你有一个 belongsTo() 关系,你可以在同一句话中更新 Eloquent 关系数据:

// 如果 Project -> belongsTo(User::class)
$project->user->update(['email' => 'some@gmail.com']);

Laravel 7+ 的外键

从 Laravel 7 开始,在迁移中你不需要为关系字段编写两行代码 - 一行用于字段,另一行用于外键。使用方法 foreignId()。

// 在 Laravel 7 之前
Schema::table('posts', function (Blueprint $table) {
    $table->unsignedBigInteger('user_id');
    $table->foreign('user_id')->references('id')->on('users');
}

// 从 Laravel 7 开始
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained();
}

// 或者,如果你的字段与表引用不同
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('created_by_id')->constrained('users', 'column');
}

两种 whereHas 组合使用

在Eloquent中,你可以将whereHas()和orDoesntHave()组合在一个句子中。

User::whereHas('roles', function($query) {
    $query->where('id', 1);
})
->orDoesntHave('roles')
->get();

检查关联方法是否存在

要检查Eloquent关系方法是否存在,可以使用PHP的method_exists()函数。如果关系名称是动态的,你可以这样做:

$user = User::first();
if (method_exists($user, 'roles')) {
    // 使用 $user->roles()->... 进行操作
}

获取中间表的关联关系数据

在多对多关系中,您定义的中间表里面可能会包含扩展字段,甚至可能包含其它的关联关系。

然后生成一个单独的数据透视模型:

php artisan make:model RoleUser --pivot

接下来,使用->using()方法将其指定在belongsToMany()中。然后你可以像示例中那样进行操作。

// app/Models/User.php
public function roles()
{
    return $this->belongsToMany(Role::class)
        ->using(RoleUser::class)
        ->withPivot(['team_id']);
}
// app/Models/RoleUser.php: 注意继承自 Pivot,而不是 Model
use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    public function team()
    {
        return $this->belongsTo(Team::class);
    }
}

最后,在控制器中,你可以这样做:

$firstTeam = auth()->user()->roles()->first()->pivot->team->name;

获取一对多关系中子集的数量

除了可以使用 Eloquent 中的 withCount() 方法统计子集数量外,还可以直接用 loadCount() 更加便捷和快速获取:

// 如果你的书有很多评论...
$book = App\Book::first();

$book->loadCount('reviews');
// 然后你可以访问 $book->reviews_count;

// 或者附加条件
$book->loadCount(['reviews' => function ($query) {
    $query->where('rating', 5);
}]);

对关联模型数据进行随机排序

您可以使用 inRandomOrder() 对 Eloquent 的查询结果进行随机排序,同时也可以作用于关联关系中,实现关联数据的随机排序。

// 如果你有一个测验,想要随机化问题...

// 1. 如果你想要以随机顺序获取问题:
$questions = Question::inRandomOrder()->get();

// 2. 如果你想还要以随机顺序获取问题选项:
$questions = Question::with(['answers' => function($q) {
    $q->inRandomOrder();
}])->inRandomOrder()->get();

过滤一对多关系

这只是我的项目中的一个代码示例,展示了过滤 hasMany 关系的可能性

TagTypes -> hasMany Tags -> hasMany Examples

你想查询所有的类型,以及它们的标记,但只查询那些有例子的类型,按大多数例子排序。

tag_types = TagType::with(['tags' => function ($query) {
    $query->has('examples')
        ->withCount('examples')
        ->orderBy('examples_count', 'desc');
    }])->get();

通过中间表字段过滤多对多关联

如果你有一个多对多关联,你可以在中间表中添加一个额外字段,这样你可以在查询列表时用它排序。

class Tournament extends Model
{
    public function countries()
    {
        return $this->belongsToMany(Country::class)->withPivot(['position']);
    }
}
class TournamentsController extends Controller

public function whatever_method() {
    $tournaments = Tournament::with(['countries' => function($query) {
            $query->orderBy('position');
        }])->latest()->get();
}

whereHas 的简短写法

Laravel 8.57 中发布:通过包含一个简单条件的简短方法来写 whereHas()

// Before
User::whereHas('posts', function ($query) {
    $query->where('published_at', '>', now());
})->get();

// After
User::whereRelation('posts', 'published_at', '>', now())->get();

可以为关联模型添加查询条件

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

    // 获取器
    public function getPublishedPostsAttribute()
    {
        return $this->posts->filter(fn ($post) => $post->published);
    }

    // 模型关联方法
    public function publishedPosts()
    {
        return $this->hasMany(Post::class)->where('published', true);
    }
}

Eloquent 查询构建器中的新方法 whereBelongsTo()

Laravel 8.63.0 版本中,新增了一个名为 whereBelongsTo() 的 Eloquent 查询构建器方法。

这个方法可以让你在查询中移除 BelongsTo 外键名称,并使用关系方法作为单一来源!

示例代码:

// 原始查询方式:
$query->where('author_id', $author->id)

// 使用 whereBelongsTo() 方法:
$query->whereBelongsTo($author)

// 添加更多高级过滤条件:
Post::query()
    ->whereBelongsTo($author)
    ->whereBelongsTo($cateogry)
    ->whereBelongsTo($section)
    ->get();

// 指定自定义关系:
$query->whereBelongsTo($author, 'author')

一对一关系中的 is() 方法用于比较模型

现在,我们可以在不进行进一步数据库访问的情况下对相关模型进行比较。

// 之前:外键从 Post 模型中获取
$post->author_id === $user->id;

// 之前:需要额外请求来通过 Author 关系获取 User 模型
$post->author->is($user);

// 之后
$post->author()->is($user);

whereHas 多个连接

// User Model
class User extends Model
{
    protected $connection = 'conn_1';

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

// Post Model
class Post extends Model
{
    protected $connection = 'conn_2';

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

// wherehas()
$posts = Post::whereHas('user', function ($query) use ($request) {
      $query->from('db_name_conn_1.users')->where(...);
  })->get();

更新存在的中间表记录

如果要更新表上存在中间记录,请使用updateExistingPivot而不是syncWithPivotValues

// 迁移
Schema::create('role_user', function ($table) {
    $table->unsignedId('user_id');
    $table->unsignedId('role_id');
    $table->timestamp('assigned_at');
});

// 第一个参数是记录的id
// 第二个参数是关联记录
$user->roles()->updateExistingPivot(
    $id, ['assigned_at' => now()],
);

获取最新或最老数据的关系

在一个模型中,我们可以定义一个关系,该关系将获得另一个关系的最新(或最旧)项。

public function historyItems(): HasMany
{
    return $this
        ->hasMany(ApplicationHealthCheckHistoryItem::class)
        ->orderByDesc('created_at');
}

public function latestHistoryItem(): HasOne
{
    return $this
        ->hasOne(ApplicationHealthCheckHistoryItem::class)
        ->latestOfMany();
}

使用 ofMany 代替自定义查询

class User extends Authenticable {
    // 获取用户最受欢迎的帖子
    public function mostPopularPost() {
        return $this->hasOne(Post::class)->ofMany('like_count', 'max');
    }
}

在关系上使用 orWhere 时避免数据泄漏

$user->posts()
    ->where('active', 1)
    ->orWhere('votes', '>=', 100)
    ->get();

返回:所有投票数大于或等于 100 的帖子都将被返回

select * from posts where user_id = ? and active = 1 or votes >= 100
use Illuminate\Database\Eloquent\Builder;

$users->posts()
    ->where(function (Builder $query) {
        return $query->where('active', 1)
                    ->orWhere('votes', '>=', 100);
    })
    ->get();

返回:投票数大于或等于 100 的用户帖子将被返回

select * from posts where user_id = ? and (active = 1 or votes >= 100)
请登录后再评论