0

I have 2 tables structured like this

products

  • id
  • title

plans

  • id
  • product_id
  • price
  • type

Basically the idea is to have multiple prices for each product, the last plan for each product would be its current price and if its deleted or expire it would fall back to the previous plan

So if a product has 2 plans with ids (1, 2) then the plan with id = 2 would be its current price

I want to show products which their last plans has type = off

Here's the SQL Query generated by the Laravel ORM Eloquent

select * from `products` where exists
        (select * from `plans` where `products`.`id` = `plans`.`product_id`
                and `type` = 'off' 
                and `plans`.`deleted_at` is null) 
        and `products`.`deleted_at` is null

The problem is it doesn't check the last/current plan it would search in all the plans... so even if plan with id = 2 type is not off and if plan.id = 1 type is off I'd still ll have this product in the query
here is the php code:

$wonder_product = Product::whereHas('CurrentPlan', function ($q) {
    $q->where('type', 'off');
})->get();
Salim
  • 9,886
  • 5
  • 23
  • 55
hretic
  • 676
  • 7
  • 30
  • 60
  • [sql-select-only-rows-with-max-value-on-a-column](https://stackoverflow.com/questions/7745609/sql-select-only-rows-with-max-value-on-a-column) – Paul Spiegel Sep 03 '19 at 16:05

2 Answers2

0

You should use whereDoesntHave instead

return Product::whereDoesntHave('plans', function ($q) {
    $q->where('type', 'off');
})->with('plans')->get();

Working Example:

products migration

Schema::create('products', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->timestamps();
});

plans migration

Schema::create('plans', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('product_id');
    $table->foreign('product_id')->references('id')->on('products')
                                 ->onDelete('cascade');
    $table->decimal('price');
    $table->string('type');
    $table->timestamps();
});

Product Model Relationship

public function plans()
{
    return $this->hasMany(Plan::class);
}

Plan Model Relationship

public function product()
{
    return $this->belongsTo(Product::class);
}

Sample Data Seeder

$productWithOnePlanOff = Product::create([
    'title' => 'A product with one of its plans off'
]);
$productWithOnePlanOff->plans()->createMany([
    ['price' => rand(1, 50), 'type' => 'off'],
    ['price' => rand(50, 100), 'type' => 'on']
]);
$productWithNoPlanOff = Product::create([
    'title' => 'A product with none of its plans off'
]);
$productWithNoPlanOff->plans()->createMany([
    ['price' => rand(1, 50), 'type' => 'on'],
    ['price' => rand(50, 100), 'type' => 'on']
]);

Query Part and Results

WhereHas looks for a model that has ANY of its related model match the query's condition

return Product::whereHas('plans', function ($q) {
    $q->where('type', 'off');
})->with('plans')->get();

Results

[
    {
        "id": 1,
        "title": "A product with one of its plans off",
        "created_at": "2019-09-03 16:30:30",
        "updated_at": "2019-09-03 16:30:30",
        "plans": [
            {
                "id": 1,
                "product_id": 1,
                "price": "46.00",
                "type": "off",
                "created_at": "2019-09-03 16:30:30",
                "updated_at": "2019-09-03 16:30:30"
            },
            {
                "id": 2,
                "product_id": 1,
                "price": "50.00",
                "type": "on",
                "created_at": "2019-09-03 16:30:30",
                "updated_at": "2019-09-03 16:30:30"
            }
        ]
    }
]

While the query with whereDoesntHave makes sure NONE of its related model match the query's condition

return Product::whereDoesntHave('plans', function ($q) {
    $q->where('type', 'off');
})->with('plans')->get();

Results

[
    {
        "id": 2,
        "title": "A product with none of its plans off",
        "created_at": "2019-09-03 16:30:30",
        "updated_at": "2019-09-03 16:30:30",
        "plans": [
            {
                "id": 3,
                "product_id": 2,
                "price": "49.00",
                "type": "on",
                "created_at": "2019-09-03 16:30:30",
                "updated_at": "2019-09-03 16:30:30"
            },
            {
                "id": 4,
                "product_id": 2,
                "price": "93.00",
                "type": "on",
                "created_at": "2019-09-03 16:30:30",
                "updated_at": "2019-09-03 16:30:30"
            }
        ]
    }
]

Hope this helps

Zeshan
  • 2,318
  • 2
  • 19
  • 24
Salim
  • 9,886
  • 5
  • 23
  • 55
  • are you sure ? i want plan with highest `id`for each product to be checked for `type=off`... this seems to be asking for products that doesn't have any plans – hretic Sep 03 '19 at 16:27
  • Am not sure what you're asking for! you want to get the products whose latest added plan's type is 'off'? – Salim Sep 03 '19 at 16:42
0

Try with a GROUP BY subquery:

$wonder_product = Product::whereHas('CurrentPlan', function ($q) {
    $q->where('type', 'off')
    ->whereIn('id', function ($subquery) {
        $subquery
        ->from(with(new CurrentPlan)->getTable())
        ->select(DB:raw('MAX(id)'))
        ->groupBy('product_id');
    });
})->get();

Or if you can live with a raw subquery:

$wonder_product = Product::whereHas('CurrentPlan', function ($q) {
    $q->where('type', 'off')
      ->whereRaw('id in (select max(id) from plans group by product_id)')
})->get();

If I'm not wrong, both methods should generate a query like this:

select * from `products`
where exists (
        select * from `plans`
        where `products`.`id` = `plans`.`product_id`
          and `type` = 'off' 
          and `plans`.`deleted_at` is null
          and id in (select max(id) from plans group by product_id)
  ) 
  and `products`.`deleted_at` is null

But if it was me, I would probably write a raw query like this:

$wonder_product = Product::hydrateRaw('
    select products.*
    from products
    where 'off' = (
      select plans.type
      from plans
      where plans.product_id = products.id
        and plans.deleted_at is null
      order by plans.id desc
      limit 1
    )
    and products.deleted_at is null
');
Paul Spiegel
  • 29,577
  • 5
  • 40
  • 50