检查是否存在belongsToMany关系 - Laravel

101

我的两个表(客户和产品)使用Laravel的blongToMany和一个中间表建立了多对多关系。

现在我想检查一个特定的客户是否拥有一个特定的产品。

我可以创建一个模型来在中间表中进行检查,但由于Laravel不需要此模型来进行belongsToMany方法,因此我想知道是否有其他方法可以检查某个关系是否存在而无需为中间表创建模型。

9个回答

232

我认为官方的做法是这样的:

$client = Client::find(1);
$exists = $client->products->contains($product_id);

这种方法有些浪费,因为它会执行SELECT查询,将所有结果放入Collection中,最后通过foreach遍历Collection以找到您传入的ID对应的模型。然而,它不需要对枢轴表进行建模。

如果您不喜欢这种浪费,您可以在SQL/Query Builder中自己完成,这也不需要对表进行建模(如果您没有其他目的获取Client模型,则无需获取它):

$exists = DB::table('client_product')
    ->whereClientId($client_id)
    ->whereProductId($product_id)
    ->count() > 0;

2
+1. 这是官方的方式,应该标为最佳答案。 - Arda
2
@DavidMIRV,您是否从上面的代码示例中得到了该错误? 如果是,请确保您将关系称为属性进行调用? 如果您将其作为方法调用(即$exists = $client->products()->contains($product_id);),那么它确实会说“BelongsToMany”上不存在contains。 但是,如果您将其作为属性调用,则“BelongsToMany”将被静默转换为集合,其中确实具有“contains()”。 - alexrussell
3
这种方法能够奏效,但是它会把大量数据加载到内存中来检查关系是否存在。 - Alexey Mezenin
2
@AlexeyMezenin 是的,我同意你的观点。不过在我的情况下,我已经加载了相关的模型。因此不会加载额外的数据。无论如何,感谢你的见解。这可能会帮助其他人找到答案。 - Gustavo Straube
2
警告:这将使查询中检索到的所有模型都被填充,比直接使用DB库进行原始查询需要更多的内存。如果您有很多模型,则可能会出现问题。 - henritroyat
显示剩余3条评论

51

这个问题非常老,但是这可能会对其他正在寻找解决方案的人有所帮助:

$client = Client::find(1);
$exists = $client->products()->where('products.id', $productId)->exists();

没有像 @alexrussell 的解决方案中那样的“浪费”,查询也更有效率。


1
当我在我的模型中尝试这样做时,我会收到错误SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: - user4748790
@George 然后尝试使用 where('products.id', $productId) - Mouagip
不认为你需要表别名,id就足够了。这是在产品关系中确定的。在我看来,这是最干净的解决方案。 - Johan
@Johan 如果例如“client”也有一个ID,则需要别名。这很常见。 - Mouagip
这个解决方案更加稳定。当我使用 $client->products->contains($product_id) 进行过快迭代时,曾经遇到过问题。 - ALeX inSide
完美运行。你甚至可以将其添加到模型访问器中。 - Dan H

20

Alex的解决方案是可行的,但它会将Client模型和所有相关的Product模型从DB加载到内存中,并在这之后才检查关系是否存在。

更好的Eloquent方法是使用whereHas()方法。

1. 您不需要加载客户端模型,只需使用其ID即可。

2. 您也不需要像Alex一样将与该客户相关的所有产品加载到内存中。

3. 一个SQL查询到DB。

$doesClientHaveProduct = Product::where('id', $productId)
    ->whereHas('clients', function($q) use($clientId) {
        $q->where('id', $clientId);
    })
    ->count();

10

更新:我没有考虑到检查多个关系的有用性,如果是这种情况,那么@deczo对这个问题有一个更好的答案。只运行一个查询来检查所有关系是期望的解决方案。

    /**
     * Determine if a Client has a specific Product
     * @param $clientId
     * @param $productId
     * @return bool
     */
    public function clientHasProduct($clientId, $productId)
    {
        return ! is_null(
            DB::table('client_product')
              ->where('client_id', $clientId)
              ->where('product_id', $productId)
              ->first()
        );
    }
您可以将此放入您的User/Client模型中,或者将其放入ClientRepository中,并在需要它的任何地方使用它。
if ($this->clientRepository->clientHasProduct($clientId, $productId)
{
    return 'Awesome';
}

但是如果您已经在客户端Eloquent模型上定义了belongsToMany关系,则可以在Client模型中执行以下操作:

    return ! is_null(
        $this->products()
             ->where('product_id', $productId)
             ->first()
    );

8

@nielsiano的方法可以实现,但在我看来,这会为每个用户/产品对查询数据库,这是一种浪费。

如果您不想加载所有相关模型的数据,那么针对单个用户,我会这样做:

// User model
protected $productIds = null;

public function getProductsIdsAttribute()
{
    if (is_null($this->productsIds) $this->loadProductsIds();

    return $this->productsIds;
}

public function loadProductsIds()
{
    $this->productsIds = DB::table($this->products()->getTable())
          ->where($this->products()->getForeignKey(), $this->getKey())
          ->lists($this->products()->getOtherKey());

    return $this;
}

public function hasProduct($id)
{
    return in_array($id, $this->productsIds);
}

然后,您可以简单地执行以下操作:
$user = User::first();
$user->hasProduct($someId); // true / false

// or
Auth::user()->hasProduct($someId);

只执行1个查询,然后您就可以使用数组。


最简单的方法是像@alexrussell建议的那样使用contains

我认为这是一个偏好问题,所以除非您的应用程序很大并且需要大量优化,否则您可以选择您觉得更容易使用的方式。


很棒的回答@deczo,使用Laravel内置的帮助程序使查询动态化,涉及键/表 - 我自己也倾向于这样做,但这将其提升到了一个新的水平!:D - alexrussell
2
谢谢,但是为了学习哪些方法只返回一个键“col_id”,哪些方法限定/前缀于“table.col_id”,仍需大量使用Eloquent。不幸的是,Eloquent经常缺乏一致性。 - Jarek Tkaczyk
1
是的 - 我必须说,尽可能尊重Taylor,你越深入研究Laravel,就会发现更多令人烦恼的不一致之处和代码不够清晰(而且没有可用的注释/文档)。但嘿,也不能说我能做得更好! - alexrussell

7

大家好) 我对这个问题的解决方案是: 我创建了一个自己的类,继承自 Eloquent,并将所有我的模型都从它继承。在这个类中,我编写了这个简单的函数:

function have($relation_name, $id) {
    return (bool) $this->$relation_name()->where('id','=',$id)->count();
}

为了检查现有的关系,您需要编写类似以下内容的代码:
if ($user->have('subscribes', 15)) {
    // do some things
}

这种方式只生成一个SELECT count(...)查询,而不从表中获取真实数据。

6

要检查两个模型之间关系的存在性,我们只需要针对中间表进行单个查询,而无需进行任何联接操作。

您可以使用内置的newPivotStatementForId方法来实现:

$exists = $client->products()->newPivotStatementForId($product->id)->exists();

请注意,如果模型使用了SoftDeletes,则此方法不会考虑它。即:在这种情况下,如果产品被软删除,这仍然是真的。 - Leonardo Cabré
如何检测是否存在至少一条记录,而不使用$product->id属性?在我的情况下,我想获取所有至少有一个产品的客户,因为我先前不知道$product->id - Pathros

4

使用trait:

trait hasPivotTrait
{
    public function hasPivot($relation, $model)
    {
        return (bool) $this->{$relation}()->wherePivot($model->getForeignKey(), $model->{$model->getKeyName()})->count();
    }
}

.

if ($user->hasPivot('tags', $tag)){
    // do some things...
}

没有解决我的问题,但帮助我走上了正确的道路,谢谢。 - phaberest

2
这个有点时间,但也许我能帮助到某些人。
if($client->products()->find($product->id)){
 exists!!
}

需要注意的是,您必须具备产品和客户模型。希望这能帮到您。

是的,它对我有帮助。如果通过标识符找不到模型,则返回相关模型或null。 - Антон Баранов
如何检测是否存在至少一条记录,而不是使用->find()方法?在我的情况下,我想获取所有至少拥有一个产品的客户,因为我之前不知道$product->id - Pathros

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接