Stars: 322
Forks: 81
Pull Requests: 52
Issues: 68
Watchers: 11
Last Updated: 2023-04-26 12:09:58
This package introduces the join magic for eloquent models and relations.
License: MIT License
Languages: PHP
This package introduces the join magic for eloquent models and relations.
Eloquent is a powerful ORM but its join capabilities are very poor.
With laravel you can't perform sorting of the relationship fields without manually joining related table which is very awkward. Let me give you a few reasons why. If you have a table with posts and related categories your code might look like this:
$posts = Post::select('posts.*')
->join('categories', 'categories.id', '=', 'posts.category_id')
->groupBy('posts.id')
->where('categories.deleted_at', '=', null)
->orderBy('categories.name');
if(request()->get('date')){
$posts->where('posts.date', $date)
}
$posts = $posts->get();
1.The first problem is that you need to worry about select.
->select('posts.*')
Reason : without select id from the category can be selected and hydrated into the Post model.
2.The second problem is that you need to worry about groupBy.
->groupBy('posts.id');
Reason : if the relation is HasOne and there are more than one categories for the post, the query will return more rows for categories.
3.The third problem is that you need to change all other where clauses from :
->where('date', $date)
to
->where('posts.date', $date)
Reason : a post and category can have "date" attribute and in that case without selecting an attribute with table "ambiguous column" error will be thrown.
4.The fourth problem is that you are using table names(not models) and this is also bad and awkward.
->where('posts.date', $date)
5.The fifth problem is that you need to worry about soft deletes for joined tables. If the category is using SoftDeletes trait you must add :
->where('categories.deleted_at', '=', null)
This package will take care of all above problems for you. Unlike sorting, you can perform filtering on the relationship fields without joining related tables, but this package will give you the ability to do this easier.
With laravel you can perform where on the relationship attribute but laravel will generate subqueries which are more slower than joins. With this package you will be available to perform where on the relationship with joins in an elegant way.
Laravel Version | Package Tag | Supported | Development Branch |
---|---|---|---|
>= 5.5.0 | 4.* | yes | master |
< 5.5.0 | - | no | - |
Package is also tested for SQLite, MySql and PostgreSql
1.Install package with composer
composer require fico7489/laravel-eloquent-join
With this statement, a composer will install highest available package version for your current laravel version.
2.Use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoinTrait trait in your base model or only in particular models.
...
use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin;
use Illuminate\Database\Eloquent\Model;
abstract class BaseModel extends Model
{
use EloquentJoin;
...
3.IMPORTANT
For MySql make sure that strict configuration is set to false
config/database.php
'mysql' => [
...
'strict' => false,
...
and that's it, you are ready to go.
Options can be set in the model :
class Seller extends BaseModel
{
protected $useTableAlias = false;
protected $appendRelationsCount = false;
protected $leftJoin = false;
protected $aggregateMethod = 'MAX';
or on query :
Order::setUseTableAlias(true)->get();
Order::setAppendRelationsCount(true)->get();
Order::setLeftJoin(true)->get();
Order::setAggregateMethod(true)->get();
Should we use an alias for joined tables (default = false)
With true query will look like this :
select "sellers".* from "sellers"
left join "locations" as "5b5c093d2e00f"
...
With false query will look like this :
select "sellers".*
from "sellers"
left join "locations"
...
Alias is a randomly generated string.
Should we automatically append relation count field to results (default = false)
With true query will look like this :
select "sellers".*, count(locations.id) AS locations_count
from "sellers"
left join "locations" as "5b5c093d2e00f"
...
Each relation is glued with an underscore and at the end _count prefix is added. For example for
->joinRelations('seller.locations')
field would be seller_locations_count
Should we use inner join or left join (default = true)
select "sellers".*
from "sellers"
inner join "locations"
...
vs
select "sellers".*
from "sellers"
left join "locations"
...
Which aggregate method to use for ordering (default = 'MAX').
When join is performed on the joined table we must apply aggregate functions on the sorted field so we could perform group by clause and prevent duplication of results.
select "sellers".*, MAX("locations" ."number") AS sort
from "sellers"
left join "locations"
group by "locations" ."id"
order by sort
...
Options are : SUM, AVG, MAX, MIN, COUNT
joinRelations($relations, $leftJoin = null)
orderByJoin($column, $direction = 'asc', $aggregateMethod = null)
whereJoin($column, $operator, $value, $boolean = 'and')
orWhereJoin($column, $operator, $value)
whereInJoin($column, $values, $boolean = 'and', $not = false)
whereNotInJoin($column, $values, $boolean = 'and')
orWhereInJoin($column, $values)
orWhereNotInJoin($column, $values)
Allowed relation:
public function locationPrimary()
{
return $this->hasOne(Location::class)
->where('is_primary', '=', 1)
->orWhere('is_primary', '=', 1)
->withTrashed();
}
Not allowed relation:
public function locationPrimary()
{
return $this->hasOne(Location::class)
->where('is_primary', '=', 1)
->orWhere('is_primary', '=', 1)
->withTrashed()
->whereHas('state', function($query){return $query;}
->orderBy('name')
->where(function($query){
return $query->where('is_primary', '=', 1);
});
}
The reason why the second relation is not allowed is that this package should apply all those clauses on the join clause, eloquent use all those clauses isolated with subqueries NOT on join clause and that is more simpler to do.
You might get a picture that there are too many rules and restriction, but it is really not like that. Don't worry, if you do create the query that is not allowed appropriate exception will be thrown and you will know what happened.
Seller::whereJoin('city.title', '=', 'test')
->orWhereJoin('city.title', '=', 'test2');
Seller::where(function ($query) {
$query
->whereJoin('city.title', '=', 'test')
->orWhereJoin('city.title', '=', 'test2');
});
Seller::whereJoin('title', '=', 'test')
->whereJoin('city.title', '=', 'test')
->orderByJoin('city.title')
->get();
Database schema :
Models :
class Seller extends BaseModel
{
public function locations()
{
return $this->hasMany(Location::class);
}
public function locationPrimary()
{
return $this->hasOne(Location::class)
->where('is_primary', '=', 1);
}
public function city()
{
return $this->belongsTo(City::class);
}
class Location extends BaseModel
{
public function locationAddressPrimary()
{
return $this->hasOne(LocationAddress::class)
->where('is_primary', '=', 1);
}
class City extends BaseModel
{
public function state()
{
return $this->belongsTo(State::class);
}
}
Seller::joinRelations('city')
Seller::joinRelations('locationPrimary')
Seller::joinRelations('locations')
Seller::joinRelations('city.state')
Seller::joinRelations('city', true)->joinRelations('city.state', false)
Seller::join(['city.state', 'locations'])
Seller::orderByJoin('city.title')
Seller::orderByJoin('locationPrimary.address')
Seller::orderByJoin('locations.title')
Seller::orderByJoin('city.state.title')
Seller::orderByJoin('locations.id', 'asc', 'COUNT')
Seller::orderByJoin('locations.is_primary', 'asc', 'SUM')
Seller::orderByJoin('locations.is_primary', 'asc', 'AVG')
Seller::orderByJoin('locations.is_primary', 'asc', 'MAX')
Seller::orderByJoin('locations.is_primary', 'asc', 'MIN')
Seller::whereJoin('city.title', '=', 'test')
Seller::whereJoin('locationPrimary.address', '=', 'test')
Seller::whereJoin('locations.title', '=', 'test')
Seller::whereJoin('city.state.title', '=', 'test')
$sellers = Seller::setAppendRelationsCount(true)->join('locations', '=', 'test')
->get();
foreach ($sellers as $seller){
echo 'Number of location = ' . $seller->locations_count;
}
Seller::joinRelations('city', true)
->joinRelations('city.state', false)
->whereJoin('city.id', '=', 1)
->orWhereJoin('city.state.id', '=', 1)
Query :
Order::whereJoin('seller.id', '=', 1)->get();
Sql :
select "orders".*
from "orders"
left join "sellers" on "sellers"."id" = "orders"."seller_id"
where "sellers"."id" = ?
and "orders"."deleted_at" is null
group by "orders"."id"
Query :
Order::orderByJoin('seller.id', '=', 1)->get();
Sql :
select "orders".*, MAX(sellers.id) as sort
from "orders"
left join "sellers" on "sellers"."id" = "orders"."seller_id"
where "orders"."deleted_at" is null
group by "orders"."id"
order by sort asc
Lets look how first example from documentation now looks like. This code :
$posts = Post::select('posts.*')
->join('categories', 'categories.id', '=', 'posts.category_id')
->groupBy('posts.id')
->where('categories.deleted_at', '=', null)
->orderBy('categories.name');
if(request()->get('date')){
$posts->where('date', $date)
}
$posts = $posts->get();
is now :
$posts = Post::orderByJoin('category.name');
if(request()->get('date')){
$posts->where('posts.date', $date)
}
$posts = $posts->get();
Both snippets do the same thing.
This package is well covered with tests. If you want run tests just run composer update and then run tests with "vendor/bin/phpunit"
Feel free to create new issue for :
MIT
Free Software, Hell Yeah!