I have a problem with ordering Eloquent collection result. This is my initial code:
class Major extends Eloquent {
protected $table = 'majors';
/**
* Gets all active majors
*
* #param $orderField Field by which the collection should be ordered
* #param $orderDirection Direction of ordering
*/
public static function getAllActive($orderField = 'order', $orderDirection = 'asc') {
$columns = array('id', 'name_de', 'name_en', 'name_'.Config::get('app.locale').' as name', 'active');
return self::all($columns)->where('active', 1);
}
}
The method to get the majors works fine. Now, I want to order the results by a specific field, so I changed the return to this:
return self::all($columns)->where('active', 1)->orderBy($orderField, $orderDirection);
The code throws the following error:
Call to undefined method Illuminate\Database\Eloquent\Collection::orderBy()
I need to keep the $columns variable as I need the name column alias. How do I order an Eloquent result properly?
all executes the query and returns a collection. You could use the select method:
self::whereActive(1)
->orderBy($orderField, $orderDirection)
->select($columns)
->get();
Ok, figured it out. The idea is to order the collection first and then select the columns:
return self::orderBy($orderField, $orderDirection)
->select($columns)
->where('active', 1)
->get();
Also, Vohuman's answer is totally valid as well.
Related
I have following model:
1- User model
/**
* Define user and functional area relationship
*/
public function functionalAreas()
{
return $this->belongsToMany('App\FunctionalArea', 'user_functional_areas', 'user_id', 'functional_area_id')->withPivot('id', 'is_primary')->withTimestamps();
}
and Business model:
/**
* Define business and user functional area relationship
*/
public function functionalAreas()
{
return $this->belongsToMany('App\FunctionalArea', 'business_functional_areas', 'business_id', 'functional_area_id')->withTimestamps();
}
Now I should take all businesses and users and show them in a single list, for this I'm using from union, following is my query:
public function usersAndOrganizations()
{
$users = $this->users();
$organizations = $this->organizations();
$invitees = $users->union($organizations)->paginate(10);
return response()->json($invitees);
}
private function users()
{
$users = User::byState($approved = true, 'is_approved')
->search()->select([
'id',
DB::raw("CONCAT(first_name, ' ', last_name) AS name"),
'about',
'address',
'slug',
'average_reviews',
DB::raw("'freelancer' AS type")
]);
$users = $users->with([
"functionalAreas" => function ($q) {
$q->select([
'functional_areas.id',
DB::raw("functional_areas.name_en AS name"),
]);
}
]);
return $users;
}
private function organizations()
{
$businesses = Business::where('owner_id', '!=', auth()->user()->id)->verified()
->active()->search()
->select([
'id',
'name',
'about',
'address',
'slug',
'average_reviews',
DB::raw("'business' AS type")
]);
$businesses = $businesses
->with([
"functionalAreas" => function ($q) {
$q->select([
'functional_areas.id',
DB::raw("functional_areas.name_en AS name"),
]);
}
]);
return $businesses;
}
But above query not return the business functional area, its output query use from user relationship instead of business, that with section generate twice the following query:
select
`functional_areas`.`id`,
functional_areas.name_en AS name,
`user_functional_areas`.`user_id` as `pivot_user_id`,
`user_functional_areas`.`functional_area_id` as `pivot_functional_area_id`,
`user_functional_areas`.`id` as `pivot_id`,
`user_functional_areas`.`is_primary` as `pivot_is_primary`,
`user_functional_areas`.`created_at` as `pivot_created_at`,
`user_functional_areas`.`updated_at` as `pivot_updated_at`
from `functional_areas`
inner join `user_functional_areas`
on `functional_areas`.`id` = `user_functional_areas`.`functional_area_id`
where `user_functional_areas`.`user_id` in (2, 6, 7)
But in fact 6, and 7 is business id not user only 2 is user id, one of this queries should use business_functional_areas instead of user_functional_areas.
One more thing found is, all items are inside App\User model in result, its like businesses are also as user object.
The only way for now is to use from map.
public function usersAndOrganizations()
{
$users = $this->users();
$organizations = $this->organizations();
$invitees = $users->union($organizations)->paginate(10);
$invitees = $this->getRelatedData($invitees);
return response()->json($invitees);
}
private function getRelatedData($invitees)
{
$invitees->map(function($object) use($functionalAreaName) {
if($object->type == 'business') {
$relationName = 'businesses';
$relationKey = 'business_id';
$attachableType = Business::MORPHABLE_TYPE;
}
if($object->type == 'freelancer') {
$relationName = 'users';
$relationKey = 'user_id';
$attachableType = User::MORPHABLE_TYPE;
}
$functionalAreas = FunctionalArea::whereHas($relationName, function($q) use ($object, $relationKey){
$q->where($relationKey, $object->id);
})->get([$functionalAreaName.' As name', 'id']);
$object->functional_areas = $functionalAreas->toArray();
});
return $invitees;
}
And remove with from your functions, and call this after you get the paginated result.
In simple words, for now you would not be able to achieve it using Eloquent Eager Loading with Unions. This is not supported yet in Laravel. One of such scenario for which they closed as a Non-Fix issue is Union with Eloquent fail....
Reason: While calling UNION function only the first model(user) is considered main model and model type of result set of other model(Business) passed as argument will be converted to the main one(USER) only and the main model relationship is called on all records(not the desired one).
Due to the above issue only relationship of user model is called on each record of result set. So even for business_id = 1, functional_area of user_id =1 are being fetched.
You can debug more about it from below file & function.
File:
<your_laravel_project>\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php
Function: get
Alternate Solution
You can fetch the both result set as it is and then merge them after data fetch using php.
public function usersAndOrganizations()
{
$users = $this->users()->get();
$organizations = $this->organizations()->get();
$invitees = $users->toBase()->merge($organizations->toBase())->toArray();
dd($invitees);
}
You can not concat incompatible queries with union.
See Unions.
Your users() method return eloquent builder for User entity.
And organizations() return builder for Business entity.
Thus, it is incorrect to select users and organizations in one query.
The correct query is like that:
SELECT City FROM Customers
UNION
SELECT City FROM Suppliers
ORDER BY City;
I'm developing an app on Laravel 5.5 and I'm facing an issue with a specific query scope. I have the following table structure (some fields omitted):
orders
---------
id
parent_id
status
The parent_id column references the id from the same table. I have this query scope to filter records that don't have any children:
public function scopeNoChildren(Builder $query): Builder
{
return $query->select('orders.*')
->leftJoin('orders AS children', function ($join) {
$join->on('orders.id', '=', 'children.parent_id')
->where('children.status', self::STATUS_COMPLETED);
})
->where('children.id', null);
}
This scope works fine when used alone. However, if I try to combine it with any another condition, it throws an SQL exception:
Order::where('status', Order::STATUS_COMPLETED)
->noChildren()
->get();
Leads to this:
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'status' in where clause is ambiguous
I found two ways to avoid that error:
Solution #1: Prefix all other conditions with the table name
Doing something like this works:
Order::where('orders.status', Order::STATUS_COMPLETED)
->noChildren()
->get();
But I don't think this is a good approach since it's not clear the table name is required in case other dev or even myself try to use that scope again in the future. They'll probably end up figuring that out, but it doesn't seem a good practice.
Solution #2: Use a subquery
I can keep the ambiguous columns apart in a subquery. Still, in this case and as the table grows, the performance will degrade.
This is the strategy I'm using, though. Because it doesn't require any change to other scopes and conditions. At least not in the way I'm applying it right now.
public function scopeNoChildren(Builder $query): Builder
{
$subQueryChildren = self::select('id', 'parent_id')
->completed();
$sqlChildren = DB::raw(sprintf(
'(%s) AS children',
$subQueryChildren->toSql()
));
return $query->select('orders.*')
->leftJoin($sqlChildren, function ($join) use ($subQueryChildren) {
$join->on('orders.id', '=', 'children.parent_id')
->addBinding($subQueryChildren->getBindings());
})->where('children.id', null);
}
The perfect solution
I think that having the ability to use queries without prefixing with table name without relying on subqueries would be the perfect solution.
That's why I'm asking: Is there a way to have table name automatically added to Eloquent query methods?
I would use a relationship:
public function children()
{
return $this->hasMany(self::class, 'parent_id')
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children')
->get();
This executes the following query:
select *
from `orders`
where `status` = ?
and not exists
(select *
from `orders` as `laravel_reserved_0`
where `orders`.`id` = `laravel_reserved_0`.`parent_id`
and `status` = ?)
It uses a subquery, but it's short, simple and doesn't cause any ambiguity problems.
I don't think that performance will be a relevant issue unless you have millions of rows (I assume you don't). If the subquery performance will be a problem in the future, you can still go back to a JOIN solution. Until then, I would focus on code readability and flexibility.
A way to reuse the relationship (as pointed out by the OP):
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children', function ($query) {
$query->where('status', self::STATUS_COMPLETED);
})->get();
Or a way with two relationships:
public function completedChildren()
{
return $this->children()
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('completedChildren')
->get();
In MySQL there are two good ways to find the leaf nodes (rows) in an adjacency list. One is the LEFT-JOIN-WHERE-NULL method (antijoin), which is what you did. The other is a NOT EXISTS subquery. Both methods should have a comparable performance (in theory they do exactly the same). However the subquery solution will not introduce new columns to the result.
return $query->select('orders.*')
->whereRaw("not exists (
select *
from orders as children
where children.parent_id = orders.id
and children.status = ?
)", [self::STATUS_COMPLETED]);
You must create a SomeDatabaseBuilder extending the original Illuminate\Database\Query\Builder, and a SomeEloquentBuilder extending the Illuminate\Database\Eloquent\Builder and, finally, a BaseModel extending Illuminate\Database\Eloquent\Model and overwrite these methods:
/**
* #return SomeDatabaseBuilder
*/
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new SomeDatabaseBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
/**
* #param \Illuminate\Database\Query\Builder $query
* #return SameEloquentBulder
*/
public function newEloquentBuilder($query)
{
return new SameEloquentBulder($query);
}
Then, on SomeDatabaseBuilder and SameEloquentBulder, change the methods to qualify columns by default (or make it optional).
I would like to reorder the attribute (COMMENTS) of my object (instance of ARTICLE) after I retrieve it from the DBB. Is this possible?
My object is ARTICLE and it is linked to COMMENTS (which is defined as a collection in entity article)
I know I can order through the repository but the order of my comments depend on many conditions, some not available through the DB.
Condition exemple:
I want at the top the comment whose attribute show_first are set to true whatever their score, and then the other comments ordered depending of their score.
You could add a hidden field to you query that sorting things in the order that you wanted so that you don't need to process the complete ArrayCollection to sort.
public function findByArticleInOrderOfState()
{
return $this->createQueryBuilder('c')
->select('c')
->addSelect('
CASE
WHEN c.state = :state_new THEN 1
WHEN c.state = :state_viewed THEN 2
WHEN c.state = :state_deleted THEN 3
ELSE 4
END AS HIDDEN order_by
')
->setParameter('state_new', 'new')
->setParameter('state_viewed', 'viewed')
->setParameter('state_deleted', 'deleted')
->orderBy('order_by', 'ASC')
->addOrderBy('c.createdAt', 'ASC')
->getQuery()
->getResults();
}
This would create a hidden field order_by and set that depending on the current state of that object, then it would order by that hidden field and then createdAt.
It doesn't really make sense to order comments like that but it does show how you could do it. With a little more info on the actual use case I would (hopefully) be able to make work a bit closer to your specific needs.
Update
In your case when you have show_first == 'yes'|'no' you could do the following..
public function findByArticleInOrderOfState()
{
return $this->createQueryBuilder('c')
->select('c')
->addSelect('
CASE
WHEN c.show_first = :show_first THEN 1
ELSE 2
END AS HIDDEN order_by
')
->setParameter('show_first', 'yes')
->orderBy('order_by', 'ASC')
->addOrderBy('c.createdAt', 'ASC')
->getQuery()
->getResults();
}
Set the getter of comments (getComments()) in your Article entity to get the comments in the order you want.
public function getComments(){
$iterator = $comments->getIterator();
$iterator->uasort(function ($a, $b) {
// change getProperty() with the field you want to order on
return ($a->getProperty() < $b->getProperty()) ? -1 : 1;
});
$comments= new ArrayCollection(iterator_to_array($iterator));
return $comments;
}
For more Infos visit this post "usort" a Doctrine\Common\Collections\ArrayCollection?
For simple ordering of associations, you can use Doctrine annotations.
/**
* #ORM\OneToMany(targetEntity="Comment", mappedBy="article")
* #ORM\OrderBy({"show_first" = "ASC", "score" = "DESC"})
*/
private $comments;
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/tutorials/ordered-associations.html
The following possible within an entity object
public function getCommentsActiveLast3()
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('status', Comment::STATUS_PUBLISHED));
$criteria->setMaxResults(3);
if ($this->comments) {
return $this->comments->matching($criteria);
}
}
I have a model called School and it has many Students .
Here is the code in my model:
public function students()
{
return $this->hasMany('Student');
}
I am getting all the students with this code in my controller:
$school = School::find($schoolId);
and in the view:
#foreach ($school->students as $student)
Now I want to order the Students by some field in the students table. How can I do that?
You have a few ways of achieving this:
// when eager loading
$school = School::with(['students' => function ($q) {
$q->orderBy('whateverField', 'asc/desc');
}])->find($schoolId);
// when lazy loading
$school = School::find($schoolId);
$school->load(['students' => function ($q) {
$q->orderBy('whateverField', 'asc/desc');
}]);
// or on the collection
$school = School::find($schoolId);
// asc
$school->students->sortBy('whateverProperty');
// desc
$school->students->sortByDesc('whateverProperty');
// or querying students directly
$students = Student::whereHas('school', function ($q) use ($schoolId) {
$q->where('id', $schoolId);
})->orderBy('whateverField')->get();
you can add orderBy to your relation, so the only thing you need to change is
public function students()
{
return $this->hasMany('Student');
}
to
public function students()
{
return $this->hasMany('Student')->orderBy('id', 'desc');
}
To answer the original question, the students dynamic property can also be accessed as a relationship method.
So you have this to fetch all students:
$students = $school->students;
Now as a relationship method, this is equivalent:
$students = $school->students()->get();
Given this, you can now add in some ordering:
$students = $school->students()->orderBy('students.last_name')->get();
Since eloquent will be performing a join, make sure to include the table name when referencing the column to order by.
You can also add this to your students method if you want to set a default order that $school->students will always return. Check out the documentation for hasMany() to see how this works.
For Many to one relation I found one answer on:
https://laracasts.com/discuss/channels/eloquent/order-by-on-relationship
$order = 'desc';
$users = User::join('roles', 'users.role_id', '=', 'roles.id')
->orderBy('roles.label', $order)
->select('users.*')
->paginate(10);
this can save day... of anyone
You can use this like this:
$students = $school->students()->orderBy('id', 'desc');
You can also use
$students = $school->students()->orderBy('id', 'desc')->paginate(10);
I am trying to do a query in my Laravel app and I want to use a normal structure for my query. This class either does use Eloquent so I need to find something to do a query totally raw.
Might be something like Model::query($query);. Only that doesn't work.
You may try this:
// query can't be select * from table where
Model::select(DB::raw('query'))->get();
An Example:
Model::select(DB::raw('query'))
->whereNull('deleted_at')
->orderBy('id')
->get();
Also, you may use something like this (Using Query Builder):
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
Also, you may try something like this (Using Query Builder):
$users = DB::select('select * from users where id = ?', array(1));
$users = DB::select( DB::raw("select * from users where username = :username"), array('username' => Input::get("username")));
Check more about Raw-Expressions on Laravel website.
You can use hydrate() function to convert your array to the Eloquent models, which Laravel itself internally uses to convert the query results to the models. It's not mentioned in the docs as far as I know.
Below code is equviolent to $userModels = User::where('id', '>', $userId)->get();:
$userData = DB::select('SELECT * FROM users WHERE id > ?', [$userId]);
$userModels = User::hydrate($userData);
hydrate() function is defined in \Illuminate\Database\Eloquent\Builder as:
/**
* Create a collection of models from plain arrays.
*
* #param array $items
* #return \Illuminate\Database\Eloquent\Collection
*/
public function hydrate(array $items) {}
use DB::statement('your raw query here'). Hope this helps.
I don't think you can by default. I've extended Eloquent and added the following method.
/**
* Creates models from the raw results (it does not check the fillable attributes and so on)
* #param array $rawResult
* #return Collection
*/
public static function modelsFromRawResults($rawResult = [])
{
$objects = [];
foreach($rawResult as $result)
{
$object = new static();
$object->setRawAttributes((array)$result, true);
$objects[] = $object;
}
return new Collection($objects);
}
You can then do something like this:
class User extends Elegant { // Elegant is my extension of Eloquent
public static function getWithSuperFancyQuery()
{
$result = DB::raw('super fancy query here, make sure you have the correct columns');
return static::modelsFromRawResults($result);
}
}
Old question, already answered, I know.
However, nobody seems to mention the Expression class.
Granted, this might not fix your problem because your question leaves it ambiguous as to where in the SQL the Raw condition needs to be included (is it in the SELECT statement or in the WHERE statement?). However, this piece of information you might find useful regardless.
Include the following class in your Model file:
use Illuminate\Database\Query\Expression;
Then inside the Model class define a new variable
protected $select_cols = [
'id', 'name', 'foo', 'bar',
Expression ('(select count(1) from sub_table where sub_table.x = top_table.x) as my_raw_col'), 'blah'
]
And add a scope:
public function scopeMyFind ($builder, $id) {
return parent::find ($id, $this->select_cols);
}
Then from your controller or logic-file, you simply call:
$rec = MyModel::myFind(1);
dd ($rec->id, $rec->blah, $rec->my_raw_col);
Happy days.
(Works in Laravel framework 5.5)
use Eloquent Model related to the query you're working on.
and do something like this:
$contactus = ContactUS::select('*')
->whereRaw('id IN (SELECT min(id) FROM users GROUP BY email)')
->orderByDesc('created_at')
->get();
You could shorten your result handling by writing
$objects = new Collection(array_map(function($entry) {
return (new static())->setRawAttributes((array) $entry, true);
}, $result));
if you want to select info it is DB::select(Statement goes here) just remember that some queries wont work unless you go to Config/Database.php and set connections = mysql make sure 'strict' = false
Just know that it can cause some security concerns
if ever you might also need this.
orderByRaw() function for your order by.
Like
WodSection::orderBy('score_type')
->orderByRaw('FIELD(score_type,"score_type") DESC')
->get();