Laravel where table with relationships - laravel

I’m build a app with laravel 8. This app has table order and table tracking. The order and tracking has a one to many relation, the models are like below:
class Order extends Model
{
protected $with = ['tracking'];
public function tracking()
{
return $this->hasMany(Tracking::class);
}
}
class Tracking extends Model
{
public function order()
{
return $this->belongsTo(Order::class);
}
}
Now I want query the orders filtering them by status in the last tracking inserted. For exemple, with the data below:
Order
ID VALUE
1 100.00
2 200.00
3 300.00
Tracking
ID ORDER_ID STATUS CREATED_AT
1 1 Accept 2022-03-01 00:00:00
2 1 Paid 2022-03-02 00:00:00
3 2 Accept 2022-03-01 00:00:00
4 2 Paid 2022-03-02 00:00:00
5 2 Cancel 2022-03-03 00:00:00
6 3 Accept 2022-03-01 00:00:00
7 3 Paid 2022-03-02 00:00:00
If the param in where clause is status = Paid, I want return order with id 1 and 3, with all tracking related.
I try to use whereHas like this:
$query = Order::query();
$query->whereHas('tracking', function ($query) use ($request) {
return $query->where('status', '=', 'Paid');
});
$orders = $query->paginate(10);
but this make a query in all tracking, and I need query only if the last status is like Paid.
And I need data like this:
Order ID VALUE TRACKING
1 100.00 [[ID: 1, STATUS: Accept], [ID: 2, STATUS: Paid]]
3 300.00 [[ID: 6, STATUS: Accept], [ID: 7, STATUS: Paid]]
Notice, that order with id 2 has Paid in 2022-03-02 but in 2022-03-03 00:00:00 its canceled, so this wont appear.
Any help is welcome.
Thanks.

I cannot reproduce the situation right now. So, suggesting the following solution without any performance testing. I hope it will solve your issue.
DB::table("tracking")
->join("order", function($join){$join;})
->select("tracking.order_id", "order.val", "max (tracking.created_at) as status_updated")
->where("order.id", "=", tracking.order_id)
->groupBy("tracking")
->get();

The wanted result is a bit complex to achieve, so we need to pay attention to how we'll achieve it.
To do so, let's decompose the fetching process into parts :
we want to select the following columns :
order_id and value from orders table.
tracking_id, status and created_at from the related tracking records. Those related records will be fetched using an INNER JOIN clause on tracking.order_id = orders.id.
Say we need to filter by status = 'paid' so in the query we'll likely to have something like where tracking.status = 'paid'.
The wanted result can be described as follows: GET ME orders WHERE LATEST TRACKING STATUS IS 'paid' which means if the latest tracking.status for a specific order is not 'paid' then we don't want that order, nor its trackings, at all.
To achieve the above statement, we need to know the latest tracking record related to order and if that related record has a status equals to paid then fetch all the related records for that order.
Here's an SQL query that fetches the wanted results when we need tracking.status = 'paid' :
-- selects the needed columns
SELECT `orders`.`id` AS `order_id`, `value`, `t`.`id` AS `tracking_id`, `t`.`status`
FROM `orders`
-- fetch the related "tracking" records for each order in the set. The table "tracking" is aliased as "t" for simplicity
INNER JOIN `tracking` `t` ON `t`.`order_id` = `orders`.`id`
-- we need to get only the related records ONLY when the latest "tracking" saved for the current "order" is the status we want (in our case "paid")
WHERE (
-- this subquery gets the status of the latest added "tracking" of each "order"
-- check below sub queries
SELECT `status`
FROM `tracking`
WHERE `order_id` = `orders`.`id` AND `id` = (
-- this subquery gets the "id" of the latest added "tracking"
-- check below sub query
SELECT `id`
FROM `tracking`
WHERE `order_id` = `orders`.`id` AND `created_at` = (
-- this subquery filters the records and returns the "created_at" values of the latest added "tracking"
SELECT `created_at`
FROM `tracking`
WHERE `order_id` = `orders`.`id`
-- instead of using "max" function we can simply get the first record in the reverse order (DESC)
ORDER BY `created_at` DESC LIMIT 1
)
)
) = 'paid'
Let me try to translate the above query into Laravel's query builder:
use Illuminate\Support\Facades\DB
/** inform the query builder that the "orders" table is main table in the query */
DB::table('orders')
/** instruct to select those columns */
->select('orders.id as order_id', 'orders.value', 't.status', 't.created_at')
/** have an INNER JOIN clause to get the related "tracking" records */
->join('tracking as t', 't.order_id', '=', 'orders.id')
->where('t.status', function ($q) {
/** creates a subquery to get the status of the last inserted related "tracking" */
$q->select('status')
->from('tracking')
->whereColumn('order_id', 'orders.id')
->where('created_at', function ($q) {
/** a subquery to get the latest inserted tracking record (only the last will be returned) */
$q->select('created_at')
->from('tracking')
->whereColumn('order_id', 'orders.id')
->orderByDesc('created_at')
->limit(1);
});
})->get(); /** execute the query */
The above query was not tested so it's better that you take some time to test it and tweak it the way you want.
Based on the data sample provided in the question, the above SQL query should return :
order_id
value
tracking_id
status
1
100.00
1
Accept
1
100.00
2
Paid
3
300.00
6
Accept
3
300.00
7
Paid
Hope i have pushed you further.

Edit: My previous answer was totally wrong, so here's another attempt:
An easier way to do this probably would be to define an extended relationship in the Order model:
// App\Models\Order
public function active_tracking() {
return $this->tracking()->where('status', 'Paid');
}
Then you can just fetch your orders with with active Tracking records:
Order::with('active_tracking')->whereHas('active_tracking')->get();

Update 1.2
According to my comment, I have updated my answer. I've a nice idea also is that if the last track of the order has not has a Cancel value I'll return this track else, I'll not return it.
use Illuminate\Pagination\LengthAwarePaginator;
$orders = Order::with('tracks')->withCount('tracks')->get();
$filteredOrders = [];
$orders->each(function ($order) use (&$filteredOrders) {
$order->tracks->each(function ($track) use ($order, &$filteredOrders) {
if ($order->tracks[$order->tracks_count - 1]->STATUS != "Cancel") {
array_push($filteredOrders, $order);
}
});
});
// Paginate the filtered array
return new LengthAwarePaginator($filteredOrders, count($filteredOrders), 10);

Related

How to get the default price of the product with different prices from different date periods from the period closest to today?

my sqlfiddle eample
Hello there,
according to the above sqlfiddle example;
I have a table A where the products are listed and a table B with different prices for different periods associated with these products.
Here I show these prices according to the date the user has chosen. There is no problem.
However, if the user has not selected a date, I cannot show the price of the period closest to today by default.
In the example I gave, the sql query does this successfully, but I cannot write it successfully in the form of laravel query. Or as an Eloquent orm query
How can I do that?
$query->select(['tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price'])
->join('tableB', function($join) {
$join->on('tableA.id', '=', 'tableB.pro_id');
})->where(function($sq) use ($postFrom) {
$sq->when($postFrom[0]=='0', function ($syq) {
$syq->whereRaw('DAYOFYEAR(curdate()) <= DAYOFYEAR(tableB.end_date)');
}, function ($stq) use ($postFrom) {
$stq->whereDate('tableB.start_date', '<=', $postFrom[0])
->whereDate('tableB.end_date', '>=', $postFrom[0]);
});
})->orWhere(function($ssq) use ($postTo) {
$ssq->whereDate('tableB.start_date', '<=', $postTo[0])
->whereDate('tableB.end_date', '>=', $postTo[0]);
})->groupBy('tableA.id')->orderBy('tableB.price', $sortDirection);
note1: $postFrom and $postTo are the start and end dates from the user. If the user did not submit a date, $postFrom is displayed as 0.
note2: I show the default price when the $postFrom[0] == '0' condition is met.
note3: The '2021-03-07' value in the sqlfiddle example is used for example instead of the dynamic present value.
note4: According to this query, it takes the price value of the first period as default. But that's not what I want.
note5: I can't use 'joinSub' because Laravel version is 5.5.
note6:In the example I want to convert to Laravel Query form, the sql query that works without any problems:
select `tableA`.*, `tableB`.`start_date`, `tableB`.`end_date`, `tableB`.`price`
from `tableA`
right join(
SELECT id, start_date, end_date, pro_id, price, DATEDIFF(`tableB`.`end_date`, '2021-03-07') diff
FROM `tableB` GROUP BY id order by diff asc
) `tableB` on `tableA`.`id` = `tableB`.`pro_id` where (date(`end_date`) >= '2021-03-07')
group by `tableA`.`id` order by `price` desc
This is an equivalent query of your query. I haven't executed.
If Laravel Version is greater then 5.5
$query1 = DB::table('tableB')
->selectRaw("id, start_date, end_date, pro_id, price, DATEDIFF(end_date, '2021-03-07') AS diff")
->groupBy('id')->orderBy('diff','ASC');
TableA::select('tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price')
->joinSub($query1, 'tableB', function ($join)
{
$join->on('tableA.id', '=', 'tableB.pro_id');
})
->whereDate('tableB.end_date','>=','2021-03-07')
->groupBy('tableA.id')->orderBy('price','DESC')->get();
For Laravel 5.5
TableA::select('tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price')
->join(DB::raw("(SELECT id, start_date, end_date, pro_id, price,
DATEDIFF(`tableB`.`end_date`, '2021-03-07') diff
FROM `tableB` GROUP BY id order by diff asc) table2 "), function ($join)
{
$join->on('tableA.id', '=', 'table2.pro_id');
})
->whereDate('table2.end_date','>=','2021-03-07')
->groupBy('tableA.id')->orderBy('price','DESC')->get();

Need guidance on how to build a Laravel database query

In Laravel 6.18 I'm trying to figure out how to recreate the following Postgres query.
with data as (
select date_trunc('month', purchase_date) as x_month, date_trunc('year', purchase_date) AS x_year,
sum (retail_value) AS "retail_value_sum"
from coins
where user_email = 'user#email.com' and sold = 0
group by x_month, x_year
order by x_month asc, x_year asc
)
select x_month, x_year, sum (retail_value_sum) over (order by x_month asc, x_year asc rows between unbounded preceding and current row)
from data
I know how to build the main part of the query
$value_of_all_purchases_not_sold = DB::table('coins')
->select(DB::raw('date_trunc(\'month\', purchase_date) AS x_month, date_trunc(\'year\', purchase_date) AS x_year, sum(retail_value) as purchase_price_sum'))
->where('user_email', '=', auth()->user()->email)
->where('sold', '=', 0)
->groupBy('x_month', 'x_year')
->orderBy('x_month', 'asc')
->orderBy('x_year', 'asc')
->get();
but how do you build out the with data as ( and the second select?
I need the data to be cumulative and I'd rather do the calculation in the DB than in PHP.
Laravel doesn't have built-in method(s) for common table expression. You may use a third party package such as this - it has a very good documentation. If you don't want to use an external library, then you need use query builder's select method with bindings such as
$results = DB::select('your-query', ['your', 'bindings']);
return Coin::hydrate($results); // if you want them as collection of Coin instance.

Laravel - Get the last entry of each UID type

I have a table that has 100's of entries for over 1000 different products, each identified by a unique UID.
ID UID MANY COLUMNS CREATED AT
1 dqwdwnboofrzrqww1 ... 2018-02-11 23:00:43
2 dqwdwnboofrzrqww1 ... 2018-02-12 01:15:30
3 dqwdwnbsha5drutj5 ... 2018-02-11 23:00:44
4 dqwdwnbsha5drutj5 ... 2018-02-12 01:15:31
5 dqwdwnbvhfg601jk1 ... 2018-02-11 23:00:45
6 dqwdwnbvhfg601jk1 ... 2018-02-12 01:15:33
...
I want to be able to get the last entry for each UID.
ID UID MANY COLUMNS CREATED AT
2 dqwdwnboofrzrqww1 ... 2018-02-12 01:15:30
4 dqwdwnbsha5drutj5 ... 2018-02-12 01:15:317
6 dqwdwnbvhfg601jk1 ... 2018-02-12 01:15:33
Is this possible in one DB call?
I have tried using DB as well as Eloquent but so far I either get zero results or the entire contents of the Table.
Andy
This is easy enough to handle in MySQL:
SELECT t1.*
FROM yourTable t1
INNER JOIN
(
SELECT UID, MAX(created_at) AS max_created_at
FROM yourTable
GROUP BY UID
) t2
ON t1.UID = t2.UID AND
t1.created_at = t2.max_created_at;
Translating this over to Eloquent would be some work, but hopefully this gives you a good starting point.
Edit: You may want to use a LEFT JOIN if you expect that created_at could ever be NULL and that a given UID might only have null created values.
You can use a self join to pick latest row for each UID
select t.*
from yourTable t
left join yourTable t1 on t.uid = t1.uid
and t.created_at < t1.created_at
where t1.uid is null
Using laravel's query builder it would be similar to
DB::table('yourTable as t')
->select('t.*')
->leftJoin('yourTable as t1', function ($join) {
$join->on('t.uid','=','t1.uid')
->where('t.created_at', '<', 't1.created_at');
})
->whereNull('t1.uid')
->get();
Laravel Eloquent select all rows with max created_at
Laravel Eloquent group by most recent record
SELECT p1.* FROM product p1, product p2 where p1.CREATED_AT> p2.CREATED_AT group by p2.UID
You can achieve this with eloquent using orderBy() and groupBy():
$data = TblModel::orderBy('id','DESC')->groupBy('uid')->get();
SOLVED
Thanks to Tim and M Khalid for their replies. It took me down the right road but I hit a snag, hence why I am posting this solution.
This worked:
$allRowsNeeded = DB::table("table as s")
->select('s.*')
->leftJoin("table as s1", function ($join) {
$join->on('s.uid', '=', 's1.uid');
$join->on('s.created_at', '<', 's1.created_at');
})
->whereNull('s1.uid')
->get();
However I got an Access Violation so I had to go in to config/database.php and set
'strict' => false,
inside the 'mysql' config, which removes ONLY_FULL_GROUP_BY from the SQL_MODE.
Thanks again.
You have to use ORDER BY, and LIMITSQL parameters, which will lead you to an easy SQL request :
for exemple, in SQL you should have something like this :
SELECT *
FROM table_name
ORDER BY `created_at` desc
LIMIT 1
This will returns everything in the table. The results will be ordering by the column "created_at" descending. So the first result will be what you're looking for. Then the "LIMIT" tells to return only the first result, so you won't have all your database.
If you wanna make it with eloquent, here is the code doing the same thing :
$model = new Model;
$model->select('*')->orderBy('created_at')->first();

Laravel eloquent get the latest rows of grouped rows

Using Eloquent, trying to find a way to get the latest rows of every row grouped by: exchange, base, quote
Data
exchange base quote price value created_at
bittrex BTC USD 10000 10000 2018-01-05
bittrex BTC USD 9000 9000 2018-01-01
poloniex BTC USD 10001 10001 2018-01-05
poloniex BTC USD 9000 9000 2018-01-01
binance BTC USD 10002 10002 2018-01-05
binance BTC USD 9000 9000 2018-01-01
binance ETH USD 800 800 2018-01-05
binance ETH USD 700 700 2018-01-01
Result:
bittrex BTC USD 10000 10000 2018-01-05
poloniex BTC USD 10001 10001 2018-01-05
binance BTC USD 10002 10002 2018-01-05
binance ETH USD 800 800 2018-01-05
UPDATE
I went with #Cryode solution, raw SQL instead of Eloquent (if anyone can come up with one Eloquent query to replicate the results of the query below, feel free to post).
I've also changed the structure of the table to add id (increments) as the primary key. I also added the following index $table->index(['exchange', 'base', 'quote', 'created_at']);
Here is the solution:
$currencies = DB::select('SELECT *
FROM (
SELECT DISTINCT exchange, base, quote
FROM tickers
) AS t1
JOIN tickers
ON tickers.id =
(
SELECT id
FROM tickers AS t2
WHERE t2.exchange = t1.exchange
AND t2.base = t1.base
AND t2.quote = t1.quote
ORDER BY created_at DESC
LIMIT 1
)
');
Thanks
Let's first determine what this SQL query would actually look like.
This DBA answer provides some great insight into the "greatest-n-per-group" problem, as well as PostgreSQL and MySQL examples. Inspired by this answer, here's what I've come up with for your single table (assuming MySQL as your DB):
SELECT ticker.*
FROM (
SELECT DISTINCT exchange, base, quote
FROM ticker
) AS exchanges
JOIN ticker
ON ticker.id =
(
SELECT id
FROM ticker
WHERE ticker.exchange = exchanges.exchange
AND ticker.base = exchanges.base
AND ticker.quote = exchanges.quote
ORDER BY created_at DESC
LIMIT 1
);
Oh dear. Getting that into Laravel-speak doesn't look easy.
Personally, I wouldn't even try. Complicated SQL queries are just that because they take advantage of your database to do reporting, data gathering, etc. Trying to shove this into a query builder is tedious and likely comes with little to no benefit.
That said, if you'd like to achieve the same result in a simple way using Laravel's query builder and Eloquent, here's an option:
// Get the unique sets of tickers we need to fetch.
$exchanges = DB::table('ticker')
->select('exchange, base, quote')
->distinct()
->get();
// Create an empty collection to hold our latest ticker rows,
// because we're going to fetch them one at a time. This could be
// an array or however you want to hold the results.
$latest = new Collection();
foreach ($exchanges as $exchange) {
$latest->add(
// Find each group's latest row using Eloquent + standard modifiers.
Ticker::where([
'exchange' => $exchange->exchange,
'base' => $exchange->base,
'quote' => $exchange->quote,
])
->latest()
->first()
);
}
Pros: You can use the query builder and Eloquent abstractions; allows you to maintain your Ticker model which may have additional logic needed during the request.
Cons: Requires multiple queries.
Another option could be to use a MySQL View that encapsulates the complicated query, and create a separate Eloquent model which would fetch from that view. That way, your app code could be as simple as TickerLatest::all().
You may pass multiple arguments to the groupBy method to group by multiple columns
Please refer to documentation https://laravel.com/docs/5.6/queries#ordering-grouping-limit-and-offset
$users = DB::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();
Since Laravel 5.6.17 you can use joinSub() so a possible Eloqunish solution could maybe be something like this:
Group and find the ticket with the last date
$latest = Ticker::select('exchange', 'base', 'quote', DB::raw('MAX(created_at) as created_at'))
->groupBy('exchange', 'base', 'quote');
And join the latest of each group again all records with joinSub()
$posts = DB::table('tickets')
->joinSub($latest, 'latest_tickets', function ($join) {
$join->on('tickets.exchange', '=', 'latest_tickets.exchange')
->on('tickets.base', '=', 'latest_tickets.base')
->on('tickets.quote', '=', 'latest_tickets.quote')
->on('tickets.created_at', '=', 'latest_posts. created_at');
})->get();
You can fetch the latest rows first then group the collection later.
$items = Ticker::latest()->get();
// exchange, base, quote
$groupedByExchange = $items->groupBy('exchange');
$groupedByBase = $items->groupBy('base');
$groupedByQoute = $items->groupBy('qoute');
UPDATE:
You can get the single item by each group by simple adding ->first() after the groupBy() function.
$latestByExchange= Ticker::latest()->groupBy('exchange')->first(); // and so on
Here is another way to get latest record per group by using a self left join and this query can be easily transformed to laravel's query builder.
It doesn't require any specific version of laravel to work, it can work on older versions of laravel too
No need for N+1 queries (overhead) as suggested in other answer
In plain SQL it can be written as
select a.*
from tickers a
left join tickers b on a.exchange = b.exchange
and a.base = b.base
and a.quote = b.quote
and a.created_at < b.created_at
where b.created_at is null
And in query builder it would look like
DB::table('tickers as a')
->select('a.*')
->leftJoin('tickers as b', function ($join) {
$join->on('a.exchange', '=', 'b.exchange')
->whereRaw(DB::raw('a.base = b.base'))
->whereRaw(DB::raw('a.quote = b.quote'))
->whereRaw(DB::raw('a.created_at < b.created_at'))
;
})
->whereNull('b.created_at')
->get();
Laravel Eloquent select all rows with max created_at
Or you use a correlated sub query to choose latest row
SQL
select a.*
from tickers a
where exists (
select 1
from tickers b
where a.exchange = b.exchange
and a.base = b.base
and a.quote = b.quote
group by b.exchange,b.base,b.quote
having max(b.created_at) = a.created_at
);
Query Builder
DB::table('tickers as a')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('tickers as b')
->whereRaw(DB::raw('a.exchange = b.base'))
->whereRaw(DB::raw('a.base = b.base'))
->whereRaw(DB::raw('a.quote = b.quote'))
->groupBy(['b.exchange','b.base','b.quote'])
->havingRaw('max(b.created_at) = a.created_at')
;
})
->get();
DEMO

Laravel: Improved pivot query

I am successfully querying following and it create 130 queries, I want to optimise it and reduce the number of queries, I have set upped the model and controllers following way.
Post Modal
class Post extends Eloquent {
public function Categories () {
return $this->belongsToMany('Category', 'category_post');
}
}
Category Modal
class Category extends Eloquent {
public function posts () {
return $this->belongsToMany('Post', 'category_post');
}
}
and in the Controller, I am using following query, what following query does is, querying the results based on category id.
$category = Category::with('posts')->where('id','=',$id)->paginate(10)->first();
return Response::json(array('category' => $category));
If anyone can give me a hand to optimise the query, would be really greatful.
You are wrong, it doesn't create 130 queries.
It will create the following 3 queries:
select count(*) as aggregate from `categories` where `id` = '5';
select * from `categories` where `id` = '5' limit 10 offset 0;
select `posts`.*, `posts_categories`.`category_id` as `pivot_category_id`, `posts_categories`.`post_id` as `pivot_post_id` from `posts` inner join `posts_categories` on `posts`.`id` = `posts_categories`.`post_id` where `posts_categories`.`category_id` in ('5');
But the question is what exactly you want to paginate. Now you paginate categories and it doesn't make much sense because there's only one category with selected $id.
What you probably want to get is:
$category = Category::where('id','=',$id)->first();
$posts = $category->posts()->paginate(10);
and this will again create 3 queries:
select * from `categories` where `id` = '5' limit 1;
select count(*) as aggregate from `posts` inner join `posts_categories` on `posts`.`id` = `posts_categories`.`post_id` where `posts_categories`.`category_id` = '5';
select `posts`.*, `posts_categories`.`category_id` as `pivot_category_id`, `posts_categories`.`post_id` as `pivot_post_id` from `posts` inner join `posts_categories` on `posts`.`id` = `posts_categories`.`post_id` where `posts_categories`.`category_id` = '5' limit 10 offset 0;
If you would like to improve it, you will probably need to not use Eloquent in this case and use join - but is it worth it? You would now need to manually paginate results without paginate() so it would probably won't be want you want to achieve.
EDIT
What you probably do is:
you get all posts that belongs to the category (but in fact you want to paginate only 10 of them)
for each post you want do display all categories it belongs to.
To lower number of queries you should use:
$category = Category::where('id','=',$id)->first();
$posts = $category->posts()->with('categories')->paginate(10);
and to display it you should use:
foreach ($posts as $p) {
echo $p->name.' '."<br />";
foreach ($p->categories as $c) {
echo $c->name."<br />";
}
}
It should lower your number queries to 4 from 130

Resources