reedware/laravel-relation-joins

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

v6.0.1 2024-03-20 17:04 UTC

README

Laravel Version Tests Lint Code Coverage Static Analysis Total Downloads

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

目录

介绍

此包通过利用您已经定义的关系来简化连接操作。

Eloquent没有提供连接的工具,所以我们一直依赖于基础的查询构建器连接。虽然Eloquent有“存在”的概念,但在某些情况下,您仍然希望返回相关实体的信息或聚合信息。

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

我看到了其他一些包试图实现与本包类似的目标。我试图至少参与其中一个,但它们都有很多不足之处。让我首先解释一下这个包的功能,您可能会看到为什么这个包更好(至少对我打算用它的情况来说)。

安装

使用Composer安装此包

composer require reedware/laravel-relation-joins

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

Reedware\LaravelRelationJoins\LaravelRelationJoinServiceProvider::class

版本控制

此包针对最新的Laravel版本进行维护,但支持遵循Laravel的支持策略

用法

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的编码“感觉”相匹配,其中复杂实现(如通过命名关系进行连接)的使用简单且易于理解。