entense / laravel-relation-joins

增加了通过关系名称进行连接的能力。

dev-master 2022-10-20 13:55 UTC

This package is not auto-updated.

Last update: 2024-09-20 21:17:10 UTC


README

Laravel Version Build Status Style Status Coverage Status Latest Stable Version Total Downloads

此包增加了通过关系名称进行连接的能力。

目录

介绍

此包通过利用您已定义的关系简化了连接过程。

Eloquent 不提供任何连接工具,因此我们一直依赖于基本的查询构建器连接。虽然 Eloquent 有 "has" 概念用于存在性检查,但在某些情况下,您仍然想要返回相关实体的信息或汇总信息。

除了关系本身之外,Eloquent 缺少关系连接意味着您无法利用 Eloquent 的许多强大功能,如模型作用域和软删除。此包旨在纠正所有这些问题。

我看到了其他一些包试图实现与这个类似的目标。我尝试使用其中至少一个,但它们都因各种原因而不足。让我首先解释这个包的功能,你可能会看到为什么这个包更好(至少对我来说)。

安装

使用 Composer 安装此包

composer require reedware/laravel-relation-joins

此包利用自动发现来处理服务提供者。如果您已禁用此包的自动发现,则需要手动注册服务提供者

Reedware\LaravelRelationJoins\LaravelRelationJoinServiceProvider::class

版本控制

此包考虑到最新版本的 Laravel,但支持可追溯到 Laravel 7.x。

对于 Laravel 6.x,使用此包的 2.x 版本。
对于 Laravel 5.5,使用此包的 1.x 版本。

用法

1. 通过关系执行连接

这正是此包的全部要点,以下是一个基本示例

User::query()->joinRelation('posts');

这将通过对 posts 关系从 User 模型应用连接,并自动利用任何查询作用域(如软删除)。

您可以对所有关系类型执行连接,包括多态关系。此外,您还可以使用与基本查询构建器类似的语法执行其他类型的连接

User::query()->leftJoinRelation('posts');
User::query()->rightJoinRelation('posts');
User::query()->crossJoinRelation('posts');

2. 连接到嵌套关系

当您必须通过关系的复杂网络进行导航时,能够通过关系进行连接的能力就会显现出来。当您尝试通过另一个关系连接一个关系时,您可以使用与 "has" 和 "with" 概念类似的自定义语法。

User::query()->joinRelation('posts.comments');

3. 添加连接约束

这是我认为许多现有解决方案存在缺陷的地方。它们要么创建了自定义 "where" 子句,要么限制了查询只支持某些类型的 "where" 子句。与此包不同,没有已知的限制,添加约束的方法非常直观。

User::query()->joinRelation('posts', function ($join) {
    $join->where('posts.created_at', '>=', '2019-01-01');
});

这将把特定的约束添加到已经提供的关联约束中,使其非常易于使用。

查询作用域

此包提供的最强大功能之一是能够在连接中利用查询作用域。在 $join 参数上调用查询作用域与在相关模型上调用它的效果基本相同。

// Using the "active" query scope on the "Post" model
User::query()->joinRelation('posts', function ($join) {
    $join->active();
});

软删除

在所有连接中重新指定软删除可能会让人感到沮丧,因为模型本身已经知道如何处理这个问题。当使用关系连接时,软删除会自动处理!此外,您仍然可以利用随软删除一起提供的查询作用域。

// Disabling soft deletes for only the "Post" model
User::query()->joinRelation('posts', function ($join) {
    $join->withTrashed();
});

4. 添加中转约束

约束不仅限于连接表本身。某些关系需要多个连接,这引入了额外的表。您仍然可以直接在这些连接上应用约束。为了明确起见,这适用于“有一个/多个通过”和“属于/形态到多个”关系。

// Adding pivot ("role_user") constraints for a "Belongs to Many" relation
User::query()->joinRelation('roles', function ($join, $pivot) {
    $pivot->where('domain', '=', 'web');
});
// Adding pivot ("users") constraints for a "Has Many Through" relation
Country::query()->joinRelation('posts', function ($join, $through) {
    $through->where('is_admin', '=', true);
});

这将在中间表上附加特定的约束,以及您提供给远端($join)表的任何约束。

查询作用域

当中间表由一个模型表示时,您可以利用该模型的查询作用域。这是“有一个/多个通过”关系的默认行为。对于“属于/形态到多个”关系,您需要利用->using(Model::class)方法来获得这个好处。

// Using a query scope for the intermediate "RoleUser" pivot in a "Belongs to Many" relation
User::query()->joinRelation('roles', function ($join, $pivot) {
    $pivot->web();
});

软删除

与常规连接约束类似,枢纽上的软删除也会自动考虑。此外,您仍然可以利用随软删除一起提供的查询作用域。

// Disabling soft deletes for the intermediate "User" model
Country::query()->joinRelation('posts', function ($join, $through) {
    $through->withTrashed();
});

当使用“属于/形态到多个”关系时,必须指定枢纽模型才能考虑软删除。

5. 添加多个约束

有时您想为中间连接附加子句。在其他包中,这可能会有些棘手(通过尝试自动推断是否应用连接,或者根本不处理这种情况)。这个包引入了两种解决方案,在不同的场合都有价值。

数组语法

处理多个约束的第一种方法是使用数组语法。这种方法允许您定义所有嵌套连接和约束。

User::query()->joinRelation('posts.comments', [
    function ($join) { $join->where('is_active', '=', 1); },
    function ($join) { $join->where('comments.title', 'like', '%looking for something%'); }
});

数组语法支持顺序和关联两种变体。

// Sequential
User::query()->joinRelation('posts.comments', [
    null,
    function ($join) { $join->where('comments.title', 'like', '%looking for something%'); }
});

// Associative
User::query()->joinRelation('posts.comments', [
    'comments' => function ($join) { $join->where('comments.title', 'like', '%looking for something%'); }
});

如果您正在使用别名,关联数组语法指的是完全合格的关联。

User::query()->joinRelation('posts as articles.comments as threads', [
    'posts as articles' => function ($join) { $join->where('is_active', '=', 1); },
    'comments as threads' => function ($join) { $join->where('threads.title', 'like', '%looking for something%'); }
});

连接类型也可以混合。

User::query()->joinRelation('posts.comments', [
    'comments' => function ($join) { $join->type = 'left'; }
});

通过语法

处理多个约束的第二种方法是使用通过语法。这种方法允许我们分别定义您的连接和约束。

User::query()->joinRelation('posts', function ($join) {
    $join->where('is_active', '=', 1);
})->joinThroughRelation('posts.comments', function ($join) {
    $join->where('comments.title', 'like', '%looking for something%');
});

这里的“通过”概念允许您使用“点”语法定义嵌套连接,其中只有最终关系受到约束,先前关系假定已经处理。所以在这种情况下,joinThroughRelation方法将只应用comments关系连接,但会像它来自Post模型一样执行。

6. 在循环关系中连接

此包还支持连接循环关系,并以与“有”概念相同的方式处理。

public function employees()
{
    return $this->hasMany(static::class, 'manager_id', 'id');
}

User::query()->joinRelation('employees');

// SQL: select * from "users" inner join "users" as "laravel_reserved_0" on "laravel_reserved_0"."manager_id" = "users"."id"

显然,如果您想对employees关系应用约束,这种命名约定并不是所希望的。这让我想到了下一个功能。

7. 别名连接

您可以像这样为上面的示例设置别名。

User::query()->joinRelation('employees as employees');

// SQL: select * from "users" inner join "users" as "employees" on "employees"."manager_id" = "users"."id"

连接不必是循环的才能支持别名。这里有一个例子。

User::query()->joinRelation('posts as articles');

// SQL: select * from "users" inner join "posts" as "articles" on "articles"."user_id" = "users"."id"

这同样适用于嵌套关系。

User::query()->joinRelation('posts as articles.comments as feedback');

// SQL: select * from "users" inner join "posts" as "articles" on "articles"."user_id" = "users"."id" inner join "comments" as "feedback" on "feedback"."post_id" = "articles"."id"

别名中转表

对于需要多个表的关系(即BelongsToMany,HasManyThrough等),别名将应用于远端/非枢纽表。如果您需要为枢纽/通过表设置别名,您可以使用双重别名。

public function roles()
{
    return $this->belongsToMany(EloquentRoleModelStub::class, 'role_user', 'user_id', 'role_id');
}

User::query()->joinRelation('roles as users_roles,roles');
// SQL: select * from "users" inner join "role_user" as "users_roles" on "users_roles"."user_id" = "users"."id" inner join "roles" on "roles"."id" = "users_roles"."role_id"

User::query()->joinRelation('roles as users_roles,positions');
// SQL: select * from "users" inner join "role_user" as "position_user" on "position_user"."user_id" = "users"."id" inner join "roles" as "positions" on "positions"."id" = "position_user"."role_id"

8. 转换到关系

MorphTo关系有一个怪癖,就是不知道需要将其连接到哪个表,因为可能有多个。由于只支持一个表,因此您必须提供要使用的形态类型。

Image::query()->joinMorphRelation('imageable', Post::class);
// SQL: select * from "images" inner join "posts" on "posts"."id" = "images"."imageable_id" and "images"."imageable_type" = ?

与其他连接类型一样,也支持。

Image::query()->leftJoinMorphRelation('imageable', Post::class);
Image::query()->rightJoinMorphRelation('imageable', Post::class);
Image::query()->crossJoinMorphRelation('imageable', Post::class);

指定形态类型后,将跟随传统参数。

// Constraints
Image::query()->joinMorphRelation('imageable', Post::class, function ($join) {
    $join->where('posts.created_at', '>=', '2019-01-01');
});

// Query Scopes
Image::query()->joinMorphRelation('imageable', Post::class, function ($join) {
    $join->active();
});

// Disabling soft deletes
Image::query()->joinMorphRelation('imageable', Post::class, function ($join) {
    $join->withTrashed();
});

嵌套关系

在之前介绍MorphTo关系时,关系本身被单独指出。然而,在嵌套场景中,MorphTo关系可能在任何地方。幸运的是,这并没有改变语法。

User::query()->joinMorphRelation('uploadedImages.imageable', Post::class);
// SQL: select * from "users" inner join "images" on "images.uploaded_by_id" = "users.id" inner join "posts" on "posts"."id" = "images"."imageable_id" and "images"."imageable_type" = ?

由于可以指定多个关系,可能会有多个 MorphTo 关系同时存在。在这种情况下,您需要为每个关系提供形态类型。

User::query()->joinMorphRelation('uploadedFiles.link.imageable', [Image::class, Post::class]);
// SQL: select * from "users" inner join "files" on "files"."uploaded_by_id" = "users"."id" inner join "images" on "images"."id" = "files"."link_id" and "files"."link_type" = ? inner join "users" on "users"."id" = "images"."imageable_id" and "images"."imageable_type" = ?

在上面的场景中,形态类型按照出现的顺序用于 MorphTo 关系。

9. 匿名连接

在罕见情况下,您可能会遇到不想在模型上定义关系,但仍然希望像它存在一样进行连接的情况。您可以通过传入关系本身来实现这一点。

$relation = Relation::noConstraints(function () {
    return (new User)
        ->belongsTo(Country::class, 'country_name', 'name');
});

User::query()->joinRelation($relation);
// SQL: select * from "users" inner join "countries" on "countries"."name" = "users"."country_name"

匿名连接的别名

由于关系不再是字符串,您需要提供一个数组作为别名。

$relation = Relation::noConstraints(function () {
    return (new User)
        ->belongsTo(Country::class, 'kingdom_name', 'name');
});

User::query()->joinRelation([$relation, 'kingdoms');
// SQL: select * from "users" inner join "countries" as "kingdoms" on "kingdoms"."name" = "users"."kingdom_name"

10. 其他一切

所有其他用于连接的元素:聚合、分组、排序、选择等,都通过已建立的查询构建器进行,其中没有任何变化。这意味着您可以轻松地做这样的事情

User::query()->joinRelation('licenses')->groupBy('users.id')->orderBy('users.id')->select('users.id')->selectRaw('sum(licenses.price) as revenue');

我个人认为,我会在 Laravel Nova(特别是透镜)中大量使用这个功能,但我已经需要这种查询多年,在无数场景中。

连接是几乎每个开发者最终都会使用的东西,因此如果 Eloquent 本地支持通过关系进行连接将非常棒。然而,由于这并非开箱即用,您将不得不安装此包。我的目标是通过此包来模仿 Laravel 的“感觉”,其中复杂的实现(如通过命名关系进行连接)简单易用且易于理解。