如何使用 Laravel 5 Eloquent 查询具有多个外键的数据透视表

5

我有以下表结构:

| products | product_options | product_option_values | product_option_to_product |
+----------+-----------------+-----------------------+---------------------------+
| id       | id              | id                    | id                        |
|          |                 |                       | product_id                |
|          |                 |                       | product_option_id         |
|          |                 |                       | product_option_value_id   |

我的关系看起来像这样:

// Product.php
public function options()
{
    return $this->belongsToMany('App\ProductOption', 'product_option_to_product', 'product_id', 'product_option_id')->withPivot('product_option_value_id', 'product_id');
}

// ProductOption.php
public function values()
{
    return $this->belongsToMany('App\ProductOptionValue', 'product_option_to_product', 'product_option_id', 'product_option_value_id')->withPivot('product_id');
}

现在当我尝试这样做:
$products = Product::with('options')->with('options.values')->first();

或者

$products = Product::with('options.values')->first();

我获取到了所有可选值,因为它没有使用产品ID来加载选项值。

有没有人知道如何确保它只加载属于该产品的值?

我可以在产品上直接建立与选项值的关系,但我真的需要它们附加到它们所属的选项上。

编辑:

我尝试过像这样的连接:

$products = Product::with(['options.values' => function($q) {
    $q->join('products', 'products.id', '=', 'product_option_to_product.product_id');
}])->first();

但它会得到相同的结果。

目前我实施了一种非常糟糕的解决方法,在我的视图中进行了一个小检查:

@if($value->pivot->product_id == $product->id)
    <div class="checkbox">
        <label>
            <input type="checkbox" name="{{ $option->id }}" value="{{ $value->id }}"> {{ $value->language->name }}
        </label>
    </div>
@endif

然而,我觉得这并不正确。
1个回答

3
您的问题在于,来自product_options表的记录不知道您正在检索的产品。
根据您在问题中提到的数据库结构,我编写了以下代码:
$product = App\Product::with('options')->with('options.values')->first();
foreach ($product->options as $option) {
    foreach ($option->values as $value) {
        printf(
            "Product: %s, Option: %d, Value: %d\n",
            $product->id,
            $option->id,
            $value->id
        );
    }
}

上面的代码仅仅是打印了某个产品特定选项和数值的可能组合。我使用了您在应用程序中创建的相同关系。

当我加载该代码时,Eloquent(Laravel的ORM)会在数据库中运行以下查询:

-- Retrieves the first product
select * from `products` limit 1;

-- Retrieves all options using the product_id as condition
select `product_options`.*, `product_option_to_product`.`product_id` as `pivot_product_id`, `product_option_to_product`.`product_option_id` as `pivot_product_option_id`, `product_option_to_product`.`product_option_value_id` as `pivot_product_option_value_id` 
from `product_options` inner join `product_option_to_product` on `product_options`.`id` = `product_option_to_product`.`product_option_id` 
where `product_option_to_product`.`product_id` in ('1')

-- Retrieves all values using the option_id as condition
select `product_option_values`.*, `product_option_to_product`.`product_option_id` as `pivot_product_option_id`, `product_option_to_product`.`product_option_value_id` as `pivot_product_option_value_id`, `product_option_to_product`.`product_id` as `pivot_product_id` 
from `product_option_values` inner join `product_option_to_product` on `product_option_values`.`id` = `product_option_to_product`.`product_option_value_id` 
where `product_option_to_product`.`product_option_id` in ('1', '2')

可以看到,最后一个查询语句并没有使用product_id作为条件来检索值,因为product_options表中没有引用products表。

最终,我已经实现了与你相同的结果。所有与我正在搜索的选项相关的值,不管产品如何。

修复这个问题最简单的方法是在调用关系时添加查询限制。像这样:

$product = App\Product::with('options')->with('options.values')->first();
foreach ($product->options as $option) {
    foreach ($option->values()->where('product_id', $product->id)->get() as $value) {
        printf("PRO: %s, OPT: %d, VAL: %d<br>\n", $product->id, $option->id, $value->id);
    }
}

请注意,我使用 $option->values() 方法来调用关系方法,而非将其作为属性调用。当调用关系方法时,您可以像使用查询构建器那样使用它。
如果您不想使用此解决方法,则可能需要更改数据库结构和模型类。
--
编辑
另一种解决方案是使用查询构建器获取特定产品的所有值。
您可以在控制器中使用以下代码实现:
$values = ProductOptionValue::select([
    'product_option_values.*',
    'product_option_to_product.option_id',
])
    ->join('product_option_to_product', 'product_option_to_product.product_option_value_id', '=', 'product_option_values.id')
    ->where('product_id', $product->id)
    ->get();

通过此方式,您可以获得特定产品(由其$product->id确定)及其相应option_id的所有值。

要在模板中使用上述查询结果,只需在复选框名称中做出微小更改:

<div class="checkbox">
    <label>
        <input type="checkbox" name="{{ $value->option_id }}" value="{{ $value->id }}"> {{ $value->language->name }}
    </label>
</div>

谢谢你的回答,看起来是个不错的解决方案。我还没有尝试过,但是最后的foreach循环会产生额外的查询吗?还是它可以使用已经检索到的结果进行操作?如果是这样的话,我更喜欢我的解决方案。 - David Ericsson
由于产品限制,它不幸地执行了一些额外的查询。顺便说一句,看起来你正在尝试尽可能多地使用ORM方法,但有时直接依赖查询构建器并不是一个坏主意,特别是为了实现一些性能上的提升。 - Gustavo Straube
在这种情况下,我的解决方案不是更快吗?;) - David Ericsson
总的来说,这取决于您的表中有多少个值。如果您的表中有很多值,您将不得不循环遍历所有值并检查其乘积。这可能比为每个选项执行一个查询更慢且使用更多内存。我刚刚更新了我的答案,并提供了一个使用查询构建器的可选解决方案。 - Gustavo Straube
没想过这种方式。我会采用你的解决方案。谢谢! - David Ericsson

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