Eloquent eager loading do not joins? - php

I remember the old days "discovering" it in a query using the Eloquent, if I used the with Laravel do aninner join.
Today by chance I checked the queries of a project and ...
[2014-11-20 23:21:16] sql.INFO: select * from `ocurrences` where `ocurrences`.`deleted_at` is null order by RAND() limit 4 {"bindings":[],"time":3.58,"name":"mysql"} []
[2014-11-20 23:21:16] sql.INFO: select * from `users` where `users`.`id` in ('7') {"bindings":["7"],"time":0.49,"name":"mysql"} []
[2014-11-20 23:21:16] sql.INFO: select * from `users` where `users`.`id` = '7' limit 1 {"bindings":["7"],"time":0.51,"name":"mysql"} []
[2014-11-20 23:21:16] sql.INFO: select * from `tags` limit 5 {"bindings":[],"time":0.41,"name":"mysql"} []
In this case, I'm doing a query like this:
/**
* Get random ocurrences for home
* #return mixed
*/
public static function randomForHome()
{
return static::with('user')
->orderByRaw('RAND()')
->limit(4)
->get();
}
What is wrong and/or how do I do joins with Eloquent?

I found the solution in a Laracasts video (thanks Jeffrey!).
I need to use join('users', 'users.id', '=', 'ocurrences.user_id'). It's a bit logical but I thought with do joins too.
Anyway, here's the final solution:
/**
* Get random ocurrences for home
* #return Eloquent
*/
public static function randomForHome()
{
return static::join('users', 'users.id', '=', 'ocurrences.user_id')
->orderByRaw('RAND()')
->limit(4)
->get();
}
Thanks guys.

Related

I'm trying to load only the last 3 comments on every post

i want get all posts with last three comment on each post. my relation is
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id')->take(3);
}
This would return only 3 comments total whenever I called it instead of 3 comments per post.
i use this way :
1 :
Postfeed::with(['comment' => function($query) {
$query->orderBy('created_at', 'desc')->take(3); }]);
2 :
$postings = Postfeed::with('comments')->get();
but getting same result. please help me out for this problem.
Can you try like that ?;
Postfeed::with('comment')->orderBy('id','desc')->take(3);
Using plain mysql (If using Mysql) query you can get 3 recent comments per post using following query which rejoins comment table by matching created_at
SELECT p.*,c.*
FROM posts p
JOIN comments c ON p.`id` = c.`post_id`
LEFT JOIN comments c1 ON c.`post_id` = c1.`post_id` AND c.`created_at` <= c1.`created_at`
GROUP BY p.`id`,c.`id`
HAVING COUNT(*) <=3
ORDER BY p.`id`,c.`created_at` DESC
Sample Demo
Using laravel's query builder you can write similar to
$posts = DB::table('posts as p')
->select('p.*,c.*')
->join('comments c', 'p.id', '=', 'c.post_id')
->leftJoin('comments as c1', function ($join) {
$join->on('c.post_id', '=', 'c1.post_id')->where('c.created_at', '<=', 'c1.created_at');
})
->groupBy('p.id')
->groupBy('c.id')
->having('COUNT(*)', '<=', 3)
->orderBy('p.id', 'asc')
->orderBy('c.created_at', 'desc')
->get();
You can create a scope in the BaseModel like this :
<?php
class BaseModel extends \Eloquent {
/**
* query scope nPerGroup
*
* #return void
*/
public function scopeNPerGroup($query, $group, $n = 10)
{
// queried table
$table = ($this->getTable());
// initialize MySQL variables inline
$query->from( DB::raw("(SELECT #rank:=0, #group:=0) as vars, {$table}") );
// if no columns already selected, let's select *
if ( ! $query->getQuery()->columns)
{
$query->select("{$table}.*");
}
// make sure column aliases are unique
$groupAlias = 'group_'.md5(time());
$rankAlias = 'rank_'.md5(time());
// apply mysql variables
$query->addSelect(DB::raw(
"#rank := IF(#group = {$group}, #rank+1, 1) as {$rankAlias}, #group := {$group} as {$groupAlias}"
));
// make sure first order clause is the group order
$query->getQuery()->orders = (array) $query->getQuery()->orders;
array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']);
// prepare subquery
$subQuery = $query->toSql();
// prepare new main base Query\Builder
$newBase = $this->newQuery()
->from(DB::raw("({$subQuery}) as {$table}"))
->mergeBindings($query->getQuery())
->where($rankAlias, '<=', $n)
->getQuery();
// replace underlying builder to get rid of previous clauses
$query->setQuery($newBase);
}
}
And in the Postfeed Model :
<?php
class Postfeed extends BaseModel {
/**
* Get latest 3 comments from hasMany relation.
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function latestComments()
{
return $this->comments()->latest()->nPerGroup('post_id', 3);
}
/**
* Postfeed has many Commentpostfeeds
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id');
}
}
And to get the posts with the latest comments :
$posts = Postfeed::with('latestComments')->get();
Ps :
Source
For many to many relationships
You can do it like this,
Postfeed::with('comments',function($query){
$query->orderBy('created_at', 'desc')->take(3);
})
->get();

Poor whereHas performance in Laravel

I want to apply a where condition to relation. Here's what I do:
Replay::whereHas('players', function ($query) {
$query->where('battletag_name', 'test');
})->limit(100);
It generates the following query:
select * from `replays`
where exists (
select * from `players`
where `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test')
order by `id` asc
limit 100;
Which executes in 70 seconds. If I manually rewrite query like this:
select * from `replays`
where id in (
select replay_id from `players`
where `battletag_name` = 'test')
order by `id` asc
limit 100;
It executes in 0.4 seconds. Why where exists is the default behavior if it's so slow? Is there a way to generate the correct where in query with query builder or do I need to inject raw SQL? Maybe I'm doing something wrong altogether?
replays table has 4M rows, players has 40M rows, all relevant columns are indexed, dataset doesn't fit into MySQL server memory.
Update: found that the correct query can be generated as:
Replay::whereIn('id', function ($query) {
$query->select('replay_id')->from('players')->where('battletag_name', 'test');
})->limit(100);
Still have a question why exists performs so poorly and why it is the default behavior
Try this:
mpyw/eloquent-has-by-non-dependent-subquery: Convert has() and whereHas() constraints to non-dependent subqueries.
mpyw/eloquent-has-by-join: Convert has() and whereHas() constraints to join() ones for single-result relations.
Replay::hasByNonDependentSubquery('players', function ($query) {
$query->where('battletag_name', 'test');
})->limit(100);
That's all. Happy Eloquent Life!
The reason for laravel has(whereHas) sometimes slowly is that implemented with where exists syntax.
For example:
// User hasMany Post
User::has('posts')->get();
// Sql: select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
The 'exists' syntax is a loop to the external table, and then queries the internal table (subQuery) every time.
However, there will be performance problems when the users table has a large amount of data, because above sql select * from 'users' where exists... unable to use index.
It can use where in instead of where exists here without damaging the structure.
// select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
// =>
// select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
This will greatly improve performance!
I recommend you try this package hasin, in the above example, you can use the hasin instead of the has.
// User hasMany Post
User::hasin('posts')->get();
// Sql: select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
The hasin just only use where in syntax instead of where exists compared with the framework has, but everywhere else is the same, such as parameters and call mode even the code implementation, and can be used safely.
whereHas performance is poor on tables without index, put index on it and be happy!
Schema::table('category_product', function (Blueprint $table) {
$table->index(['category_id', 'product_id']);
});
This is related to the mysql not to the laravel. You can perform the same thing you wanted from the above with the both options, joins and the subqueries. Subqueries are generally much slower than joins.
Subqueries are:
less complicated
elegant
easier to understand
easier to write
logic separation
and the above facts are why ORMs like eloquent are using suquries. but there are slower! Especially when you have many rows in the database.
Join version of your query is something like this :
select * from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
limit 100;
but now you must change select and add group by and be careful on many other things, but why is this so it is beyond that answer. New query would be :
select replays.* from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
group by replays.id
limit 100;
So that are the reasons why join in more complicated.
You can write raw query in laravel, but eloquent support for join queries are not well supported, also there are no much packages that can help you with that, this one is for example : https://github.com/fico7489/laravel-eloquent-join
WhereHas() query is really as slow as lazy turtle, so I created and still using a trait that I glue to any laravel model which required a simple join requests. This trait make a scope function whereJoin(). You can just pass there a joined model class name, where clause params and enjoy. This trait take care of table names and related details in query. Well, it's for my personal use and ofc feel free to modify this monstruosity.
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
/** #mixin Model */
trait ModelJoinTrait
{
/**
* #param string|\Countable|array $on
* #param $column
* #param $whereOperator
* #param $value
* #param Model $exemplar
* #return array
*/
function _modelJoinTraitJoinPreset($on, $column, $whereOperator, $value, $exemplar){
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
//set up default join and condition parameters
$joinOn =[
'local' => $localTable.'.'.$localId,
'foreign'=> $foreignTable.'.'.$foreignId,
'operator' => '=',
'type'=>'inner',
'alias'=>'_joint_id',
'column'=>$column,
'where_operator'=>$whereOperator,
'value'=>$value
];
//config join parameters based on input
if(is_string($on)){
//if $on is string it treated as foreign key column name for join clause
$joinOn['foreign'] = $foreignTable.'.'.$on;
} elseif (is_countable($on)){
//if $is array or collection there can be join parameters
if(isset($on['local']) && $on['local'])
$joinOn['local'] = $localTable.'.'.$on['local'];
if(isset($on['foreign']) && $on['foreign'])
$joinOn['foreign'] = $localTable.'.'.$on['foreign'];
if(isset($on['operator']) && $on['operator'])
$joinOn['operator'] = $on['operator'];
if(isset($on['alias']) && $on['alias'])
$joinOn['alias'] = $on['alias'];
}
//define join type
$joinTypeArray = ['inner', 'left', 'right', 'cross'];
if(is_countable($on) && isset($on['type']) && in_array($on['type'], $joinTypeArray))
$joinOn = $on['type'];
return $joinOn;
}
/**
* #param Model $exemplar
* #param string|array|\Countable $joinedColumns
* #param string|array|\Countable $ownColumns
* #param string $jointIdAlias
* #return array
*/
function _modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $jointIdAlias = '_joint_id')
{
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
if(is_string($joinedColumns))
$foreignColumn = ["$foreignTable.$joinedColumns"];
else if(is_countable($joinedColumns)) {
$foreignColumn = array_map(function ($el) use ($foreignTable) {
return "$foreignTable.$el";
}, $joinedColumns);
} else {
$foreignColumn = ["$foreignTable.*"];
}
if(is_string($ownColumns))
$ownColumns = ["$localTable.$ownColumns"];
elseif(is_countable($ownColumns)) {
$ownColumns = array_map(function ($el) use ($localTable) {
return "$localTable.$el";
}, $ownColumns);
} else {
$ownColumns = ["$localTable.*"];
}
$columns = array_merge($foreignColumn, $ownColumns);
if($foreignId == $localId){
$columns = array_merge(["$foreignTable.$foreignId as $jointIdAlias"], $columns);
}
return $columns;
}
/**
* #param Builder $query
* #param string|array|\Countable $on
* #param Model $exemplar
*/
function _modelJoinTraitJoinPerform($query, $on, $exemplar){
$funcTable = ['left'=>'leftJoin', 'right'=>'rightJoin', 'cross'=>'crossJoin', 'inner'=>'join'];
$query->{$funcTable[$on['type']]}($exemplar->getTable(),
function(JoinClause $join) use ($exemplar, $on){
$this->_modelJoinTraitJoinCallback($join, $on);
}
);
}
function _modelJoinTraitJoinCallback(JoinClause $join, $on){
$query = $this->_modelJoinTraitJoinOn($join, $on);
$column = $on['column'];
$operator = $on['where_operator'];
$value = $on['value'];
if(is_string($column))
$query->where($column, $operator, $value);
else if(is_callable($column))
$query->where($column);
}
/**
* #param JoinClause $join
* #param array|\Countable $on
* #return JoinClause
*/
function _modelJoinTraitJoinOn(JoinClause $join, $on){
//execute join query on given parameters
return $join->on($on['local'], $on['operator'], $on['foreign']);
}
/**
* A scope function used on Eloquent models for inner join of another model. After connecting trait in target class
* just use it as ModelClass::query()->whereJoin(...). This query function forces a select() function with
* parameters $joinedColumns and $ownColumns for preventing overwrite primary key on resulting model.
* Columns of base and joined models with same name will be overwritten by base model
*
* #param Builder $query Query given by Eloquent mechanism. It's not exists in
* ModelClass::query()->whereJoin(...) function.
* #param string $class Fully-qualified class name of joined model. Should be descendant of
* Illuminate\Database\Eloquent\Model class.
* #param string|array|\Countable $on Parameter that have join parameters. If it is string, it should be foreign
* key in $class model. If it's an array or Eloquent collection, it can have five elements: 'local' - local key
* in base model, 'foreign' - foreign key in joined $class model (default values - names of respective primary keys),
* 'operator' = comparison operator ('=' by default), 'type' - 'inner', 'left', 'right' and 'cross'
* ('inner' by default) and 'alias' - alias for primary key from joined model if key name is same with key name in
* base model (by default '_joint_id')
* #param Closure|string $column Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param null $operator Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param null $value Default Eloquent model::where(...) parameter that will be applied to joined model.
* #param string[] $joinedColumns Columns from joined model that will be joined to resulting model
* #param string[] $ownColumns Columns from base model that will be included in resulting model
* #return Builder
* #throws \Exception
*/
public function scopeWhereJoin($query, $class, $on, $column, $operator = null, $value=null,
$joinedColumns=['*'], $ownColumns=['*']){
//try to get a fake model of class to get table name and primary key name
/** #var Model $exemplar */
try {
$exemplar = new $class;
} catch (\Exception $ex){
throw new \Exception("Cannot take out data of '$class'");
}
//preset join parameters and conditions
$joinOnArray = $this->_modelJoinTraitJoinPreset($on, $column, $operator, $value, $exemplar);
//set joined and base model columns
$selectedColumns = $this->_modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $joinOnArray['alias']);
$query->select($selectedColumns);
//perform join with set parameters;
$this->_modelJoinTraitJoinPerform($query, $joinOnArray, $exemplar);
return $query;
}
}
You can use it like this (Model Goods in example have a dedicated extended data model GoodsData with hasOne relationship between them):
$q = Goods::query();
$q->whereJoin(GoodsData::class, 'goods_id',
function ($q){ //where clause callback
$q->where('recommend', 1);
}
);
//same as previous exmple
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1); //where clause params
// there we have sorted columns from GoodsData model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1, null, //where clause params
['recommend', 'discount']); //selected columns
//and there - sorted columns from Goods model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns from
//joined and base model
//a bit more complex example but still same. Table names is resolved
//by trait from relevant models
$joinData = [
'type'=>'inner' // inner join `goods_data` on
'local'=>'id', // `goods`.`id`
'operator'=>'=' // =
'foreign'=>'goods_id', // `goods_data`.`goods_id`
];
$q->whereJoin(GoodsData::class, $joinData,
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns
return $q->get();
Resulting SQL query will be like this
select
`goods_data`.`id` as `_joint_id`, `goods_data`.`id`, `goods_data`.`recommend`,
`goods`.`id`, `goods`.`name`, `goods`.`price` from `goods`
inner join
`goods_data`
on
`goods`.`id` = `goods_data`.`goods_id`
and
-- If callback used then this block will be a nested where clause
-- enclosed in parenthesis
(`recommend` = ? )
-- If used scalar parameters result will be like this
`recommend` = ?
-- so if you have complex queries use a callback for convenience
In your case there should be like this
$q = Replay::query();
$q->whereJoin(Player::class, 'replay_id', 'battletag_name', 'test');
//or
$q->whereJoin(Player::class, 'replay_id',
function ($q){
$q->where('battletag_name', 'test');
}
);
$q->limit(100);
To use it more efficiently, you can go like this:
// Goods.php
class Goods extends Model {
use ModelJoinTrait;
//
public function scopeWhereData($query, $column, $operator = null,
$value = null, $joinedColumns = ['*'], $ownColumns = ['*'])
{
return $query->whereJoin(
GoodsData::class, 'goods_id',
$column, $operator, $value,
$joinedColumns, $ownColumns);
}
}
// -------
// any.php
$query = Goods::whereData('goods_data_column', 1)->get();
PS I dont run any automated tests for this so be careful in use. It works just fine in my case, but there may be unexpected behaviour in yours.
I think performance does not depend on whereHas only it depends on how many records you have selected
Plus try to optimize your mysql server
https://dev.mysql.com/doc/refman/5.7/en/optimize-overview.html
and also Optimize your php server
and if you have faster query why don't you use raw query object from larval
$replay = DB::select('select * from replays where id in (
select replay_id from players where battletag_name = ?)
order by id asc limit 100', ['test']
);
You can use left join
$replies = Replay::orderBy('replays.id')
->leftJoin('players', function ($join) {
$join->on('replays.id', '=', 'players.replay_id');
})
->take(100)
->get();

Model Primary Key not being Inserted into Query using WhereHas

A Little Background...
My User model has the following two relations:
/**
* Get the connections added by the user.
*
* #param boolean $accepted
* #return BelongsToMany
*/
public function addedConnections($accepted = true)
{
return $this->belongsToMany('App\Models\User\User', 'user_connections', 'user_id', 'connection_id')
->wherePivot('connection_type', '=', 'user')
->wherePivot('accepted', '=', $accepted)
->withTimestamps();
}
/**
* Get the connections the user was invited to.
*
* #param boolean $accepted
* #return BelongsToMany
*/
public function invitedConnections($accepted = true)
{
return $this->belongsToMany('App\Models\User\User', 'user_connections', 'connection_id', 'user_id')
->wherePivot('connection_type', '=', 'user')
->wherePivot('accepted', '=', $accepted)
->withTimestamps();
}
I have attempted to write a method that merges the above two methods to return connections. However, instead of returning this as a Laravel Collection, I want to return it as a query Builder so I can add to the query...
This is what I have implemented:
/**
* Get the connections the user was invited to.
*
* #return Builder
*/
public function connections()
{
return $this
->where(function ($query) {
$query
->whereHas('addedConnections')
->orWhereHas('invitedConnections');
})
->whereDoesntHave('blocked');
}
The Issue...
The problem with this function is that it is not building the query correctly (from what I can see). Instead of returning the connections for the current user, it is returning all connections. This is because of the following two lines in the query:
WHERE user_connections.user_id = users.id
WHERE user_connections.connection_id = users.id
users.id should be the id of the current user, not a reference to the users table.
My Question(s)
Why is this happening? Why is the model id (1, 2, 3 etc) not being inserted into the query instead of the reference to the table?
Regardless of what is causing the issue, I am keen to know if there are any better methods of achieving the above?
Update 1
I think I may have fixed this with the following:
/**
* Get the connections the user was invited to.
*
* #return Builder
*/
public function connections()
{
return $this
->where(function ($query) {
$query
->whereHas('addedConnections', function ($query) {
$query->where('id', '=', $this->id);
})
->orWhereHas('invitedConnections', function ($query) {
$query->where('id', '=', $this->id);
});
})
->whereDoesntHave('blocked');
}
Is this the right solution?
Additional Information
If needed, this is the full SQL query:
SELECT
*
FROM
`users`
WHERE
(
EXISTS
(
SELECT
*
FROM
`users` AS `laravel_reserved_3`
INNER JOIN
`user_connections`
ON `laravel_reserved_3`.`id` = `user_connections`.`connection_id`
WHERE
`user_connections`.`user_id` = `users`.`id` -- THIS IS THE ISSUE
AND `user_connections`.`connection_type` = 'user'
AND `user_connections`.`accepted` = TRUE
AND `laravel_reserved_3`.`deleted_at` IS NULL
)
OR EXISTS
(
SELECT
*
FROM
`users` AS `laravel_reserved_4`
INNER JOIN
`user_connections`
ON `laravel_reserved_4`.`id` = `user_connections`.`user_id`
WHERE
`user_connections`.`connection_id` = `users`.`id` -- THIS IS THE ISSUE
AND `user_connections`.`connection_type` = 'user'
AND `user_connections`.`accepted` = TRUE
AND `laravel_reserved_4`.`deleted_at` IS NULL
)
)
AND NOT EXISTS
(
SELECT
*
FROM
`users` AS `laravel_reserved_5`
INNER JOIN
`user_blocks`
ON `laravel_reserved_5`.`id` = `user_blocks`.`blocked_id`
WHERE
`user_blocks`.`user_id` = `users`.`id`
AND `laravel_reserved_5`.`deleted_at` IS NULL
)
AND `users`.`deleted_at` IS NULL

Laravel complex scope querying relation (for review)

I wrote this and I'm just wondering if it's the best way to do it.
Orders have many transactions... Transactions belongTo order. I am querying order with ids greater than the last ID sent to fulfilment. Then I want only orders that have approved payment status or are offline payment type.
This code works, just wondering if it's the best way to do this :
/**
* Get all orders pending fulfilment
*
* #param $query
* #param $last_fulfiled
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeToFulfil($query, $last_fulfiled)
{
return $query->where('id', '>', $last_fulfiled)
->where(function($query) {
$query->whereHas('transactions', function ($query) {
$query->where('status', '>=', 5);
})->orWhere('payment_method', '=', 2);
});
}
EDIT:
Here is the SQL syntax :
select * from `orders` where `orders`.`deleted_at` is null and `id` > ? and ((select count(*) from `transactions` where `transactions`.`order_id` = `orders`.`id` and `status` >= ?) >= 1 or `payment_method` = ?)
Thanks !
This is definitely the correct Laravel approach for building this query. This almost always generates a pretty decently optimized query, usually a LEFT JOIN or a subquery or a combination of both.
If you want to see how optimal the resulting query would be, add ->toSql() to the end of that query statement and post the raw query syntax here.

Relation not loading, when using select

Introduction
Hello. I'm currently building a FacebookFeedParser, and at the moment I'm trying to build a method in a Controller that lists the Facebook pages with the best post/like ratio from today to the user. To do that, I have built the following query in pure SQL
SELECT pa.facebook_name, COUNT(po.id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(po.id) AS likesPerPost
FROM facebook_posts po
INNER JOIN facebook_pages pa ON pa.id = po.facebook_page_id
WHERE CONVERT_TZ(`facebook_created_time`, 'UTC', 'Europe/Berlin') > '2015-07-16 00:00:00'
GROUP by facebook_page_id
ORDER BY SUM(likes)/COUNT(po.id) DESC;
What I now wanted to do, is to transform this query into Laravel/Eloquent.
Current state
I have the following classes in my project
Controllers: PageController, PostController
Models: FacebookPost, FacebookPage, BaseModel
The FacebookPost and FacebookPage model are both defining their relations like this
FacebookPage
/**
* Defines the relation between FacebookPage and FacebookPost
* One page can have multiple posts
*
* #see FacebookPage
* #see FacebookPost
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(FacebookPost::class);
}
FacebookPost
/**
* Defines the association of this object with FacebookPage
* One facebook_post belongs to one facebook_page
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
{
return $this->belongsTo(FacebookPage::class, 'facebook_page_id');
}
In the BaseModel, I have defined a scope, which will be used multiple times in the project
/**
* Query scope to get the database values for today
*
* #param $query
* #return mixed
*/
public function scopeToday($query)
{
return $query->whereRaw('CONVERT_TZ(`'. $this->createDateColumn .'`, "UTC", "'. env('APP_TIMEZONE') .'") > "' . Carbon::today()->toDateTimeString() . '"');
}
And this is the query I've built in order to get those posts, with the filters
$posts = App\Models\FacebookPost::with('page')
->selectRaw('COUNT(id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(id) AS likesPerPost')
->today()
->groupBy('facebook_page_id')
->orderByRaw('SUM(likes)/COUNT(id) DESC')
->get();
Problem
The problem I'm currently having, is, that, when I try to rebuild the above query, I'm not getting all fields I want. As soon as I add an select to the Builder, the relations array, with the index page is null. If I ommit the select method, I'm getting the FacebookPage, but I want to have those specific fields
Now I'm getting an object. I guess this is because I'm using the Eloquent Builder right? Isn't it somehow possible to only get the fields I want to have? The result I'm expecting should look like this (per row)
facebook_name | postCount | likes | likesPerPost
McDonalds 1000 500 0.5
I also tried it like this
$posts = App\Models\FacebookPost::with(['page' => function($query) {
$query->select('facebook_name');
}])
->selectRaw('COUNT(id) AS postCount, SUM(likes) AS likes, SUM(likes)/COUNT(id) AS likesPerPost')
->today()
->groupBy('facebook_page_id')
->orderByRaw('SUM(likes)/COUNT(id) DESC')
->get();
Would I need to use the DB class instead of Eloquent? Or what would be the best solution for this problem?
Alternative solution
$pages = DB::table('facebook_posts')
->select(DB::raw('facebook_pages.facebook_name, COUNT(facebook_posts.id) AS postCount, SUM(likes) AS likes, ROUND(SUM(likes)/COUNT(facebook_posts.id)) AS likesPerPost'))
->join('facebook_pages', 'facebook_posts.facebook_page_id', '=', 'facebook_pages.id')
->whereRaw('CONVERT_TZ(`'. $this->createDateColumn .'`, "UTC", "'. env('APP_TIMEZONE') .'") > "' . Carbon::today()->toDateTimeString() . '"')
->groupBy('facebook_page_id')
->orderByRaw('ROUND(SUM(likes)/COUNT(facebook_posts.id)) DESC')
->get();
This, however, works. Would this be the correct solution for my use case? I'm just asking if it even make sense to use Eloquent here, since I'm not really trying to get an object, but data from multiple sources.
If you want with('page') to work when you specify the list of fields to fetch using select() you need to make sure that the foreign key is in that list, otherwise Laravel is not able to fetch related models. So in your case make sure that page_id is fetched from the database.

Categories