Laravel eloquent from DB - php

I have a very complicated db query, but I would like to know or is a possibility to short it and make it easier by eloquent?
My Model and his db is :
class Order extends Model
{
public $timestamps = false;
public $incrementing = false;
public function products()
{
return $this->hasMany(OrderProducts::class);
}
public function statuses()
{
return $this->belongsToMany(OrderStatusNames::class, 'order_statuses', 'order_id', 'status_id');
}
public function actualKioskOrders()
{
return
$rows = DB::select("SELECT o.id, o.number, o.name client_name, o.phone,
o.email, o.created_at order_date, osn.name actual_status
FROM orders o
JOIN order_statuses os ON os.order_id = o.id
JOIN (SELECT o.id id, MAX(os.created_at) last_status_date FROM orders o
JOIN order_statuses os ON os.order_id = o.id GROUP BY o.id) t
ON t.id = os.order_id AND t.last_status_date = os.created_at
JOIN order_status_names osn ON osn.id = os.status_id
WHERE os.status_id != 3");
}
}

Of course you can. Laravel query builder implements everything you need.
See Laravel Docs: Query Builder, it have join methods, where clause methods and select methods.
You can do for example the following:
Order::select(['id','number', 'name', 'client_name'])
->where('status_id', '!=', 3)
->join('order_statuses', 'order_statuses.order_id, '=', 'orders.id')
->get()
That's just an example on how you can create queries. Chain many methods that you need to create your query, the docs show many ways to do it, including with more complex joins if you need.

Related

Add additional fields into doctrine result (DTO)

Im trying to get the best rated movies by average from my database and hydrate them nicely into a DTO with doctrine so i can work well later with it and integrate them e.g. into my api with api platform.
I already managed to get it work with a raw SQL query but i cannot manage to get it work with hydration into a DTO with doctrine.
namespace App\Repository;
use App\Entity\Movie;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class MovieRepository extends ServiceEntityRepository
{
public function findBestRated()
{
$sql = "
SELECT m.*, AVG(Cast(r.rating as Decimal)) AS avg_rating, COUNT(r.id) AS count_rating
FROM `movie` m
JOIN rating r ON r.movie_id = m.id
GROUP BY m.id
ORDER BY avg_rating DESC
LIMIT 10
";
$connection = $this->getEntityManager()->getConnection();
$statement = $connection->prepare($sql);
$result = $statement->executeQuery();
return $result->fetchAllAssociative();
}
}
I would like to use a DTO like below:
namespace App\Dto;
use App\Entity\Movie;
class RatedMovie
{
public Movie $movie;
public float $averageRating;
public int $ratingCount;
public function __construct(Movie $movie, float $averageRating, int $ratingCount)
{
$this->movie = $movie;
$this->averageRating = $averageRating;
$this->ratingCount = $ratingCount;
}
}
I found some information on https://geek-week.imtqy.com/articles/en496166/index.html, but still i cannot get the hydration running. I already tried with ResultSetMapping and ResultSetMappingBuilder and a native doctrine query. With ResultSetMappingBuilder i can kind of simulate my raw sql query but the result is still an associative array and not mapped into the RatedMovie DTO.
public function findBestRated()
{
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addScalarResult('id', 'movieId');
$rsm->addScalarResult('name', 'movieName');
$rsm->addScalarResult('avg_rating', 'averageRating', Types::FLOAT);
$rsm->addScalarResult('count_rating', 'countRating', Types::INTEGER);
$sql = "
SELECT m.*, AVG(r.rating) AS avg_rating, COUNT(r.id) AS count_rating
FROM `movie` m
JOIN rating r ON r.name_id = m.id
GROUP BY m.id
ORDER BY avg_rating DESC
LIMIT 10
";
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
return $query->getResult();
}
I cannot get it running with newObjectMappings or the kind of strange syntax SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM ... from the linked article. Any ideas?

whereHas vs join -> applying global scopes

I have a query with a few joins and global scopes for each model, for example:
SELECT *
FROM products p
WHERE EXISTS(
SELECT *
FROM orders o
WHERE o.user_id = 4
AND o.status_id = 1
AND o.user_id = 3
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
)
);
This means that I can simply write a few whereHas statements and my query will have some nested EXIST clauses, but all global scopes (like the user_id on the orders table) will be applied automatically:
$this->builder->whereHas('orders', function ($q) {
$q->where('status_id', '=', 1)
->whereHas('suborder', function ($q) {
$q->where('status_id', '=', 2);
});
});
The problem is that it's slow, it would be much better to have something with plain JOINs instead of ugly nested EXIST clauses:
SELECT *
FROM products p
INNER JOIN orders o ON p.order_id = o.id
INNER JOIN suborders s ON o.id = s.order_id
WHERE o.status_id = 1
AND u.user_id = 3
AND s.status_id = 2;
The problem with this is that I need to use query builder to join these:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2);
And that will not include any of my global scopes on Order and Suborder model. I need to do it manually:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2)
->where('orders.user_id', 3);
It's bad, because I need to replicate my global scopes logic every time I write a query like this, while whereHas applies them automatically.
Is there a way to join a table, and have all global scopes from the joined model applied automatically?
I have worked with a similar issue before and come up with some approach.
First, lets define a macro for Illuminate\Database\Eloquent\Builder in a service provider:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class AppServiceProvider
{
public function boot()
{
Builder::macro('hasJoinedWith', function ($table) {
return collect(
$this->getQuery()->joins
)
->contains(function (JoinClause $joinClause) use ($table) {
return $joinClause->table === $table;
})
});
}
}
Then, lets define the scopes:
class Product extends Model
{
public function scopeOrderStatus($query, $orderStatusId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.status_id', $orderStatusId)
}
public function scopeOrderUser($query, $userId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.user_id', $userId)
}
public function scopeSubOrder($query, $subOrderId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
if (! $query->hasJoinedWith('suborders')) {
$query->join('suborders', 'orders.id', '=', 'suborders.order_id');
}
return $query->where('suborders.id', $subOrderId)
}
}
Finally, you use the scopes together:
Product::orderStatus(1)
->subOrder(2)
->orderUser(3)
// This is optional. There are possibly duplicate products.
->distinct()
->get();
This is the best approach that I can come up with, there may be better ones.
Actually, that what you describe can't be true (will provide proof below). EXISTS don't make query slower and in 99% of case it make faster!
So laravel don't provide ability to make joins for relations from box.
I saw different solutions for this and package on github, but I will not provide link to it, because when I reviewed logic, found a lot of problems with field selecting and rare cases.
Laravel don't generate code with EXISTS as you describe, it add relation search by ID to each EXISTS subquery like
SELECT *
FROM products
WHERE EXISTS(
SELECT *
FROM orders
WHERE o.user_id = 4
AND o.status_id = 1
AND o.id = p.order_id
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
AND s.id = o.suborder_id
)
);
attention to AND o.id = p.order_id and AND s.id = o.suborder_id
When you do select with joins, you should exactly set SELECT from main table, to have right filled Model fields.
Global scopes are global. Purpose to be really global. If you have more then 1-2 places without them, then you should find another solution instead of global scopes. Otherwise you application will be very hard to support and write new code. Developer should not remember every time, that there can be global scopes that he should turn off

laravel: convert many to many (but only one related item) mysql query to a laravel query

Hi I'm having trouble converting a mysql query that I've been working on into a laravel eloquent query and need some help.
I have a reservations table which links to a product table with a many to many relationship. I want to pull all the reservations and just the first product it finds regardless of how many products are related to the reservation.
Here's my sql:
SELECT reservations.id,
reservations.play_date,
reservations.group_size,
reservations.status,
reservations.event_title,
t4.product_id,
t4.id AS link_id,
p1.name,
CONCAT_WS(" ", customers.first_name, customers.last_name, customers.group_name) AS customerName,
reservations.event_type
FROM reservations
LEFT JOIN customers ON reservations.customer_id = customers.id
LEFT JOIN
(SELECT *
FROM product_reservation AS t3
GROUP BY t3.reservation_id ) AS t4 ON t4.reservation_id = reservations.id
LEFT JOIN products AS p1 ON t4.product_id = p1.id
I can place this as a raw query but that produces an array with the result - I need to be able to create a query object so I can work with another module on the results
Is there an eloquent way of doing this - or how can I get this query to work in laravel?
Thank you
Yeah, you can use Eloquent relationships. They would look something like this...
class Reservation extends Eloquent
{
public function products()
{
return $this->belongsToMany('Product');
}
}
class Product
{
public function reservations()
{
return $this->belongsToMany('Reservation');
}
}
$reservations = Reservation::with(array('products', function($q) {
$q->take(1);
}))->get();
foreach($reservations as $reservation) {
echo $reservation->name;
foreach($reservation->products as $product) {
echo $product->description;
}
}

Laravel many to many loading related models with count

I am trying to link 4 tables and also add a custom field calculated by counting the ids of some related tables using laravel.
I have this in SQL which does what I want, but I think it can be made more efficient:
DB::select('SELECT
posts.*,
users.id AS users_id, users.email,users.username,
GROUP_CONCAT(tags.tag ORDER BY posts_tags.id) AS tags,
COUNT(DISTINCT comments.id) AS NumComments,
COUNT(DISTINCT vote.id) AS NumVotes
FROM
posts
LEFT JOIN comments ON comments.posts_id = posts.id
LEFT JOIN users ON users.id = posts.author_id
LEFT JOIN vote ON vote.posts_id = posts.id
LEFT JOIN posts_tags ON posts_tags.posts_id = posts.id
LEFT JOIN tags ON tags.id = posts_tags.tags_id
GROUP BY
posts.id,
posts.post_title');
I tried to implement it using eloquent by doing this:
$trending=Posts::with(array('comments' => function($query)
{
$query->select(DB::raw('COUNT(DISTINCT comments.id) AS NumComments'));
},'user','vote','tags'))->get();
However the NumComments value is not showing up in the query results.
Any clue how else to go about it?
You can't do that using with, because it executes separate query.
What you need is simple join. Just translate the query you have to something like:
Posts::join('comments as c', 'posts.id', '=', 'c.id')
->selectRaw('posts.*, count(distinct c.id) as numComments')
->groupBy('posts.id', 'posts.post_title')
->with('user', 'vote', 'tags')
->get();
then each post in the collection will have count attribute:
$post->numComments;
However you can make it easier with relations like below:
Though first solution is better in terms of performance (might not be noticeable unless you have big data)
// helper relation
public function commentsCount()
{
return $this->hasOne('Comment')->selectRaw('posts_id, count(*) as aggregate')->groupBy('posts_id');
}
// accessor for convenience
public function getCommentsCountAttribute()
{
// if relation not loaded already, let's load it now
if ( ! array_key_exists('commentsCount', $this->relations)) $this->load('commentsCount');
return $this->getRelation('commentsCount')->aggregate;
}
This will allow you to do this:
$posts = Posts::with('commentsCount', 'tags', ....)->get();
// then each post:
$post->commentsCount;
And for many to many relations:
public function tagsCount()
{
return $this->belongsToMany('Tag')->selectRaw('count(tags.id) as aggregate')->groupBy('pivot_posts_id');
}
public function getTagsCountAttribute()
{
if ( ! array_key_exists('tagsCount', $this->relations)) $this->load('tagsCount');
$related = $this->getRelation('tagsCount')->first();
return ($related) ? $related->aggregate : 0;
}
More examples like this can be found here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-hasmany-relation-count-efficiently/
as of laravel 5.3 you can do this
withCount('comments','tags');
and call it like this
$post->comments_count;
laravel 5.3 added withCount

Laravel Eloquent join several tables

I have 3 tables:
products
|id|name|about|
=categories=
|id|name|parent|
=products-categories=
|id|product_id|cat_id|
I need to take a product categories names. I have a sql query:
SELECT s.name FROM products AS p
LEFT JOIN `products-categories` AS cats ON p.id = cats.product_id
LEFT JOIN `categories` AS s ON cats.cat_id = s.id
WHERE product_id = 1;
And It works! But how I can do this with the help of Laravel Eloquent (Not Fluent!)
You can use Eloquent relationship and in this case, create two models, for both tables, i.e. product and Category:
class Category extends Eloquent {
protected $table = 'categories'; // optional
public function products()
{
return $this->belongsToMany('Product', 'products_categories', 'category_id', 'product_id');
}
}
class Product extends Eloquent {
protected $table = 'products'; // optional
public function categories()
{
return $this->belongsToMany('Category', 'products_categories', 'product_id', 'category_id');
}
}
Now you may use these relationship methods to get related data, for example:
$product = Product::with('categories')->find(1);
This will return the product with id 1 and all the related categories in a collection so you may use $product->categories->first()->name or you may do a loop on the categories like:
foreach($product->categories as $cat) {
echo $cat->name;
}
Also you may use join which doesn't require the relationship methods and you may use same approach to join the models that is used in Fluent (Check other answer). But either way, you need to store the category and product mappings in the products_categories table. Read more about many-to-many relationship on Laravel website.
Just use the leftJoin method. It works the same in Eloquent as in Query Builder.
$product = Product::leftJoin('product-categories', 'product-categories.product_id', '=', 'products.id')
->leftJoin('categories', 'categories.id', '=', 'product-categories.cat_id')
->where('product_id', 1)
->first(['categories.name']);

Categories