Giter Club home page Giter Club logo

eloquent-power-joins's Introduction

Eloquent Power Joins

Laravel Supported Versions run-tests MIT Licensed Latest Version on Packagist Total Downloads

The Laravel magic you know, now applied to joins.

Joins are very useful in a lot of ways. If you are here, you most likely know about and use them. Eloquent is very powerful, but it lacks a bit of the "Laravel way" when using joins. This package make your joins in a more Laravel way, with more readable with less code while hiding implementation details from places they don't need to be exposed.

A few things we consider is missing when using joins which are very powerful Eloquent features:

  • Ability to use relationship definitions to make joins;
  • Ability to use model scopes inside different contexts;
  • Ability to query relationship existence using joins instead of where exists;
  • Ability to easily sort results based on columns or aggregations from related tables;

You can read a more detailed explanation on the problems this package solves on this blog post.

Installation

You can install the package via composer:

composer require kirschbaum-development/eloquent-power-joins

For Laravel versions < 8, use the 2.* version:

composer require kirschbaum-development/eloquent-power-joins:2.*

Usage

This package provides a few features.

1 - Join Relationship

Let's say you have a User model with a hasMany relationship to the Post model. If you want to join the tables, you would usually write something like:

User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');

This package provides you with a new joinRelationship() method, which does the exact same thing.

User::joinRelationship('posts');

Both options produce the same results. In terms of code, you didn't save THAT much, but you are now using the relationship between the User and the Post models to join the tables. This means that you are now hiding how this relationship works behind the scenes (implementation details). You also don't need to change the code if the relationship type changes. You now have more readable and less overwhelming code.

But, it gets better when you need to join nested relationships. Let's assume you also have a hasMany relationship between the Post and Comment models and you need to join these tables, you can simply write:

User::joinRelationship('posts.comments');

So much better, wouldn't you agree?! You can also left or right join the relationships as needed.

User::leftJoinRelationship('posts.comments');
User::rightJoinRelationship('posts.comments');

Joining polymorphic relationships

Let's imagine, you have a Image model that is a polymorphic relationship (Post -> morphMany -> Image). Besides the regular join, you would also need to apply the where imageable_type = Post::class condition, otherwise you could get messy results.

Turns out, if you join a polymorphic relationship, Eloquent Power Joins automatically applies this condition for you. You simply need to call the same method.

Post::joinRelationship('images');

You can also join MorphTo relationships.

Image::joinRelationship('imageable', morphable: Post::class);

Note: Querying morph to relationships only supports one morphable type at a time.

Applying conditions & callbacks to the joins

Now, let's say you want to apply a condition to the join you are making. You simply need to pass a callback as the second parameter to the joinRelationship method.

User::joinRelationship('posts', fn ($join) => $join->where('posts.approved', true))->toSql();

For nested calls, you simply need to pass an array referencing the relationship names.

User::joinRelationship('posts.comments', [
    'posts' => fn ($join) => $join->where('posts.published', true),
    'comments' => fn ($join) => $join->where('comments.approved', true),
]);

For belongs to many calls, you need to pass an array with the relationship, and then an array with the table names.

User::joinRelationship('groups', [
    'groups' => [
        'groups' => function ($join) {
            // ...
        },
        // group_members is the intermediary table here
        'group_members' => fn ($join) => $join->where('group_members.active', true),
    ]
]);

Using model scopes inside the join callbacks ๐Ÿคฏ

We consider this one of the most useful features of this package. Let's say, you have a published scope on your Post model:

    public function scopePublished($query)
    {
        $query->where('published', true);
    }

When joining relationships, you can use the scopes defined in the model being joined. How cool is this?

User::joinRelationship('posts', function ($join) {
    // the $join instance here can access any of the scopes defined in Post ๐Ÿคฏ
    $join->published();
});

When using model scopes inside a join clause, you can't type hint the $query parameter in your scope. Also, keep in mind you are inside a join, so you are limited to use only conditions supported by joins.

Using aliases

Sometimes, you are going to need to use table aliases on your joins because you are joining the same table more than once. One option to accomplish this is to use the joinRelationshipUsingAlias method.

Post::joinRelationshipUsingAlias('category.parent')->get();

In case you need to specify the name of the alias which is going to be used, you can do in two different ways:

  1. Passing a string as the second parameter (this won't work for nested joins):
Post::joinRelationshipUsingAlias('category', 'category_alias')->get();
  1. Calling the as function inside the join callback.
Post::joinRelationship('category.parent', [
    'category' => fn ($join) => $join->as('category_alias'),
    'parent' => fn ($join) => $join->as('category_parent'),
])->get()

For belongs to many or has many through calls, you need to pass an array with the relationship, and then an array with the table names.

Group::joinRelationship('posts.user', [
    'posts' => [
        'posts' => fn ($join) => $join->as('posts_alias'),
        'post_groups' => fn ($join) => $join->as('post_groups_alias'),
    ],
])->toSql();

Select * from table

When making joins, using select * from ... can be dangerous as fields with the same name between the parent and the joined tables could conflict. Thinking on that, if you call the joinRelationship method without previously selecting any specific columns, Eloquent Power Joins will automatically include that for you. For instance, take a look at the following examples:

User::joinRelationship('posts')->toSql();
// select users.* from users inner join posts on posts.user_id = users.id

And, if you specify the select statement:

User::select('users.id')->joinRelationship('posts')->toSql();
// select users.id from users inner join posts on posts.user_id = users.id

Soft deletes

When joining any models which uses the SoftDeletes trait, the following condition will be also automatically applied to all your joins:

and "users"."deleted_at" is null

In case you want to include trashed models, you can call the ->withTrashed() method in the join callback.

UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());

You can also call the onlyTrashed model as well:

UserProfile::joinRelationship('users', ($join) => $join->onlyTrashed());

Extra conditions defined in relationships

If you have extra conditions in your relationship definitions, they will get automatically applied for you.

class User extends Model
{
    public function publishedPosts()
    {
        return $this->hasMany(Post::class)->published();
    }
}

If you call User::joinRelationship('publishedPosts')->get(), it will also apply the additional published scope to the join clause. It would produce an SQL more or less like this:

select users.* from users inner join posts on posts.user_id = posts.id and posts.published = 1

Global Scopes

If your model have global scopes applied to it, you can enable the global scopes by calling the withGlobalScopes method in your join clause, like this:

UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());

There's, though, a gotcha here. Your global scope cannot type-hint the Eloquent\Builder class in the first parameter of the apply method, otherwise you will get errors.

2 - Querying relationship existence (Using Joins)

Querying relationship existence is a very powerful and convenient feature of Eloquent. However, it uses the where exists syntax which is not always the best and may not be the more performant choice, depending on how many records you have or the structure of your tables.

This packages implements the same functionality, but instead of using the where exists syntax, it uses joins. Below, you can see the methods this package implements and also the Laravel equivalent.

Please note that although the methods are similar, you will not always get the same results when using joins, depending on the context of your query. You should be aware of the differences between querying the data with where exists vs joins.

Laravel Native Methods

User::has('posts');
User::has('posts.comments');
User::has('posts', '>', 3);
User::whereHas('posts', fn ($query) => $query->where('posts.published', true));
User::whereHas('posts.comments', ['posts' => fn ($query) => $query->where('posts.published', true));
User::doesntHave('posts');

Package equivalent, but using joins

User::powerJoinHas('posts');
User::powerJoinHas('posts.comments');
User::powerJoinHas('posts.comments', '>', 3);
User::powerJoinWhereHas('posts', function ($join) {
    $join->where('posts.published', true);
});
User::powerJoinDoesntHave('posts');

When using the powerJoinWhereHas method with relationships that involves more than 1 table (One to Many, Many to Many, etc.), use the array syntax to pass the callback:

User::powerJoinWhereHas('commentsThroughPosts', [
    'comments' => fn ($query) => $query->where('body', 'a')
])->get());

3 - Order by

You can also sort your query results using a column from another table using the orderByPowerJoins method.

User::orderByPowerJoins('profile.city');

If you need to pass some raw values for the order by function, you can do like this:

User::orderByPowerJoins(['profile', DB::raw('concat(city, ", ", state)']);

This query will sort the results based on the city column on the user_profiles table. You can also sort your results by aggregations (COUNT, SUM, AVG, MIN or MAX).

For instance, to sort users with the highest number of posts, you can do this:

$users = User::orderByPowerJoinsCount('posts.id', 'desc')->get();

Or, to get the list of posts where the comments contain the highest average of votes.

$posts = Post::orderByPowerJoinsAvg('comments.votes', 'desc')->get();

You also have methods for SUM, MIN and MAX:

Post::orderByPowerJoinsSum('comments.votes');
Post::orderByPowerJoinsMin('comments.votes');
Post::orderByPowerJoinsMax('comments.votes');

In case you want to use left joins in sorting, you also can:

Post::orderByLeftPowerJoinsCount('comments.votes');
Post::orderByLeftPowerJoinsAvg('comments.votes');
Post::orderByLeftPowerJoinsSum('comments.votes');
Post::orderByLeftPowerJoinsMin('comments.votes');
Post::orderByLeftPowerJoinsMax('comments.votes');

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

Sponsorship

Development of this package is sponsored by Kirschbaum Development Group, a developer driven company focused on problem solving, team building, and community. Learn more about us or join us!

License

The MIT License (MIT). Please see License File for more information.

Laravel Package Boilerplate

This package was generated using the Laravel Package Boilerplate.

eloquent-power-joins's People

Contributors

adammparker avatar amandovledder avatar brandonferens avatar brunolopesr avatar coolgoose avatar datlechin avatar dvanscott avatar fabalv avatar geosot avatar glamax13 avatar jimpeters avatar jonneroelofs avatar luilliarcec avatar luisdalmolin avatar marickvantuil avatar masterix21 avatar navneetrai avatar niektenhoopen avatar richardhowes avatar runig006 avatar s-7777 avatar tortuetorche avatar toyi avatar vedmant avatar wilkerwma avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eloquent-power-joins's Issues

Custom orderByPowerJoins

How to orderByPowerJoins if the field is need like
DB::raw('concat(firstname, ' ', lastname)')

im try to install and get this error:

im try to install and get this error:

In ProviderRepository.php line 208:

Class 'KirschbaumDevelopment\EloquentJoins\PowerJoinsServiceProvider' not found

issue with saved relationship system

From what I understand from your code, the relationships are stored in an array, and it does not admit 2 with the same name even though they have different parents, for example:

Product::joinRelationship('translation')->joinRelationship('features.translation')

The previous code only gets the relationships of 'translation' from the first join, and the second join only gets the relation of 'features', ignoring the 'translation'.

The problem comes in this line of code:

     /**
     * Checks if the relationship was already joined.
     */
    public function relationshipAlreadyJoined($relation)
    {
        return isset(PowerJoins::$joinRelationshipCache[spl_object_id($this)][$relation]);
    }

    /**
     * Marks the relationship as already joined.
     */
    public function markRelationshipAsAlreadyJoined($relation)
    {
        PowerJoins::$joinRelationshipCache[spl_object_id($this)][$relation] = true;
    }

It can be fixed modifying the following:
In scopeJoinNestedRelationship update to:

$latestRelation = null;
$relationNameWithParents = null;
foreach ($relations as $index => $relationName) {
    ...
    $relationCallback = null;
    $relationNameWithParents = isset($relationNameWithParents) ? $relationNameWithParents . '.' . $relationName : $relationName;
    ...

    if ($this->relationshipAlreadyJoined($relationNameWithParents)) {
        $latestRelation = $relation;
        continue;
    }
    ...
    $this->markRelationshipAsAlreadyJoined($relationNameWithParents);
}

Suggestion : Merge with Laravel

This is a great package have you considered to request that this becomes a part of Laravel? Specially the join on relationships is the lost feature of eloquent.

unkdown issue

Hi, I'm here again, and I'm sure you're going to hate me.

I have found a issue, and it was difficult for me to replicate it to give you an example, since I could not see where it came from, here I write the details:

From Code:

for ($i = 0; $i < 2; $i++) {
    Page::joinRelationship('translation')->first();
}
Page::with('translations')->get();
dd(Page::joinRelationship('translation')->toSql());

//Output
select pages.* from pages


From Code:

for ($i = 0; $i < 1; $i++) {
    Page::joinRelationship('translation')->first();
}
Page::with('translations')->get();
dd(Page::joinRelationship('translation')->toSql());

//Output
select pages.* from pagesinner joinpage_translationsonpage_translations.page_id=pages.idandpage_translations.locale` = ?


From Code:

for ($i = 0; $i < 2; $i++) {
    Page::joinRelationship('translation')->first();
}
//Page::with('translations')->get();
dd(Page::joinRelationship('translation')->toSql());

//Output
select pages.* from pagesinner joinpage_translationsonpage_translations.page_id=pages.idandpage_translations.locale` = ?

Code 2 and 3 are correct, but code 1 is wrong, it is very rare and only appears in certain cases as I have shown you in the examples that I have replicated.

I hope you can know why this happens because I have no idea.

How to .

Hello,

How would you return a result with powerjoins when you have an order table that belongs to customer can have employee and you also need to know the creator.

customer_id <-- references customers table to whom this order belongs to
employee_id <-- the order manager who is assigned to this order,
creator_id <-- user who created this order,
creator <-- user type customer/employee

currently i'm using raw query where i join both tables customers and employees for creator_id and then do if condition using value of creator column..

the scenerio is very specific but I was exploring your package so i thought you could help.

P.s I know I should move creator to different table.

Thanks in advance!

Nested joins with different join type

hi, thanks for your work

Is it possible to join nested relationships with different join types?

User::query()
    ->join('posts', 'posts.user_id', '=', 'users.id')
    ->leftJoin('comments', 'comments.post_id', '=', 'posts.id')
    ->toSql()

Implement a way to include trashed models in the joins

Currently, the soft delete clauses are always applied to the joins if the model uses soft deletes. But, you might want to include trashed models in your query.

Just relying on the withTrashed() from the query itself is not really reliable, as you are not telling that you want to include trashed models on the joined table.

This could be a method you call fluently, like:

User::withTrashedJoins('posts')->joinRelationship('posts');

It could be also a method like:

User::joinRelationshipWithTrashed('posts')

Open to suggestions for the API.

orderByUsingJoins missing results

When using Model::orderByUsingJoins() you use an inner join query, however, this causes rows to be missing if they don't have the required relationship filled.

Is there are way to use left joins when using Mode::orderByUsingJoins()?

Join to self realtion (Feature Request: Join alias)

Hello.

First off, Thank you for making this awesome package.

I have a Model called category and it has a many-to-many relation to itself like this.

class Category extends Model
{
    public function subCategories()
    {
        return $this->belongsToMany(Category::class, 'category_children', 'parent_id', 'child_id');
    }

    public function parentCategories()
    {
        return $this->belongsToMany(Category::class, 'category_children', 'child_id', 'parent_id');
    }
}

And also have a Product model.

class Product extends Model
{
    public function categories()
    {
        return $this->belongsToMany(Category::class);
    }
}

Now I can use join relationship in Product.

 Product::joinRelationship('categories')->first();

But can not join nested relationship.

Product::joinRelationship('categories.parentCategories')->first();

Error:

Illuminate/Database/QueryException with message 'SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'categories' (SQL: ...)' 

It would be nice to have join alias feature to fix this.
something like this.

Product::joinRelationship('categories.parentCategories', [
    'categories.parentCategories' => function ($join) {
         $join->as('parent_categories');
    }
])->first();

Or

Product::joinRelationship('categories.parentCategories', ['categories.parentCategories as parent_categories'])->first();

// And/Or

Product::joinRelationship('categories.parentCategories', ['categories.parentCategories as parent_categories' => function ($join) {
    $join->...
}])->first();

I used ['categories.parentCategories' => ] instead of ['parentCategories' => ] because maybe I have a join like this.

Product::joinRelationship('categories.parentCategories.parentCategories')

Then parentCategories is ambighous.

Nested where clauses in join clause cause Exception

Hey, I really like you extension! It helped me optimizing my queries a lot! But I came across an Exception when using nested where clauses inside a join clause:

$query = Events::joinRelationship('dates', function ($join) use ($dateFrom, $dateTo) {
    $join->as('date')
        ->where(function ($query) use ($dateFrom) {
            $query->where("date.start", ">=", $dateFrom)
                ->orWhere("date.end", ">=", $dateFrom);
        })
        ->where("date.start", "<=", $dateTo);
});

The reason is that PowerJoinClause::where doesn't accept a Closure as it's first parameter. An Exception is thrown from the Str::contains function, because $column is not a string:

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($this->alias && Str::contains($column, $this->tableName)) {
$column = str_replace("{$this->tableName}.", "{$this->alias}.", $column);
}
return parent::where($column, $operator, $value, $boolean);
}

If you take a look at the method from the parent class Illuminate\Database\Query\Builder::where, you can see that the $column argument can be a Closure, which results in a nested where clause: https://github.com/laravel/framework/blob/2cd2877fb003d395b5b292a363b917e31cf60200/src/Illuminate/Database/Query/Builder.php#L695-L700

Do you think nested where clauses could be implemented?

If you don't have the time or my request would be too complicated, don't worry about it. I can find another way to build my query.

Join Using Alias

Hi @luisdalmolin

Just following up on #54
I am still unable to get this to work correctly and im unsure where i am going wrong?

$records = Order::joinRelationshipUsingAlias('salesExec', 'salesExec')
            ->select([
                'orders.id',
                'orders.vehicle_reg',
                'orders.created_at',
                'salesExec.first_name'
            ])
            ->toSql();

Using ->get() i get the following error:

Column not found: 1054 Unknown column 'salesExec.first_name' in 'field list'

When using ->toSql() i can see that its using a random string as the alias instead of salesExec?

select orders.id, orders.vehicle_reg, orders.created_at, salesExec.first_name from orders inner join users as be48e2c2f8d67d4ad035546f40ad055e on be48e2c2f8d67d4ad035546f40ad055e.id = orders.sales_exec_user_id

I am using the latest version: 2.5.0

Join disappears using withCount

Laravel dont merging joins, when using withCount method on relation.
I'm have hasMany relation, where i'm need joins, but i'm cant use it with withCount
Because this method, called from Builder::withCount->withAggregate
\Illuminate\Database\Eloquent\Concerns\QueriesRelationships::mergeConstraintsFrom

joinRelationshipCache breaks union query

I use an sql union statement to chain multiple queries together.
It all works great unless you join the same relation twice.

$query1 = User::joinRelationship('posts');
$query2 = User::joinRelationship('posts.comments');
// errors: unknown column posts.id
$query1->union($query2)->get();

The resulting query is something like this:

select * from users
join posts on posts.user_id = user.id
union
select * from users
-- join on posts is missing here
join comments on comments.post_id = posts.id

I modified the method \Kirschbaum\PowerJoins\PowerJoins::clearPowerJoinCaches to also clear the joinRelationshipCache and it now works without an error.

    public function clearPowerJoinCaches()
    {
        $this->joinRelationshipCache = []; // <- this is the new line
        PowerJoins::$powerJoinAliasesCache = [];

        return $this;
    }

table name specified more than once

Hello guys!
I'm facing with this issue:

class User extends Model
{
	public function country()
	{
		return $this->belongsTo(Country::class);
	}

	public function organization()
	{
		return $this->belongsTo(Organization::class);
	}
}

class Organization extends Model
{
	public function country()
	{
		return $this->belongsTo(Country::class);
	}
}

class Country extends Model
{
	//
}

User::leftJoinRelationship('country')->leftJoinRelationShip('organization.country')->count() // error: table name "countries" specified more than once

Probably, need to extend already created join with ยซorยป instead of creating new one.
Seems to be related to #37

How to return/view joined relation?

I have a test project with user/post models

User.php

class User {
    use PowerJoins;

    public function posts() {
        return $this->hasMany(Post::class);
    }
}

Post.php is almost empty

Query

Route::get('/', function () {
    return User::joinRelationship('posts')->find(1);
});

And, i see

{
  "id": 1,
  "name": "Mrs. Elvera Considine MD",
  "email": "[email protected]",
  "email_verified_at": "2021-02-24T10:35:31.000000Z",
  "created_at": "2021-02-24T10:35:31.000000Z",
  "updated_at": "2021-02-24T10:35:31.000000Z"
}

How to return posts? getPostsAttribute? $visible = [] ?

$user = User::joinRelationship('posts')->find(1);
return $user->posts;

Gives me a new db query SELECT * FROM "posts" WHERE "posts"."user_id" = '1' and "posts"."user_id" IS not NULL

How to use that ?

orderByUsingJoins does not get the correct latest relationship model for nested relationships

The following looks for the latest relationship name from the root model of the query.

$latestRelationshipModel = $this->getModel()->$latestRelationshipName()->getModel();

The following query from the tests works because there is a comments relationship defined on the User model.

// just making sure the query doesn't throw any exceptions
User::orderByUsingJoins('posts.comments.approved', 'desc')->get();

If you comment out the comments relationship, you will get the following error:

Call to undefined method KirschbaumDevelopment\EloquentJoins\Tests\Models\User::comments()

Or if you try this query, it will fail:
User::orderByUsingJoins('posts.category.title', 'desc')->get();

self relationship

I have Admin model with a relation called inviter which indicates the admin that invited this one to the system (self-relationship).

so the inviter relation is:

function inviter() { return $this->belongsTo(self::class,'invited_by'); }

calling : joinRelationship('inviter')->find(1) result on

Syntax error or access violation: 1066 Not unique table/alias: 'admins' (SQL: select `admins`.* from `admins` inner join `admins` on `admins`.`invited_by` = `admins`.`id` where `admins`.`id` = 1 limit 1)

Join soft deletes

As said in the docs, when joining any models which use the SoftDeletes trait, the "deleted_at" is null condition added to any joins.
But is there a way to join without this condition? Like withTrashed() model method?

Joining using Aliases

I cannot get this to work at all.

my relationship is called salesExec.
public function salesExec() { return $this->hasOne(User::class, 'id', 'sales_exec_user_id'); }

$records = Order::leftJoinRelationship('salesExec')->select('users.first_name')->first();
Works but no good when i need to join the users table again.

$records = Order::leftJoinRelationshipUsingAlias('salesExec')->select('salesExec.first_name')->first();
Does not work: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'salesExec.first_name'

$records = Order::leftJoinRelationshipUsingAlias('salesExec')->select('sales_exec.first_name')->first();
Does not work: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sales_exec.first_name'

$records = Order::leftJoinRelationshipUsingAlias('salesExec')->select('users.first_name')->first();
Does not work: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'users.first_name'

$records = Order::leftJoinRelationshipUsingAlias('salesExec')->toSql();

Seems to join as a random string, so have no way to select specifc columns

select orders.* from orders left join users as b69c457af5e262af3fe7823187d6beb7 on b69c457af5e262af3fe7823187d6beb7.id = orders.sales_exec_user_id

$records = Order::leftJoinRelationship('salesExec', [
            'salesExec' => function ($join) {
                $join->as('sales_exec');
            },
        ])->select('sales_exec.first_name')->first();

Does not work: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sales_exec.first_name'

$records = Order::leftJoinRelationship('salesExec', [
            'users' => function ($join) {
                $join->as('sales_exec');
            },
        ])->select('sales_exec.first_name')->first();

Does not work: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sales_exec.first_name'

Not quite sure what im doing wrong here?

On a separate note, it should be great to join using as alias like this:

$records = Order::leftJoinRelationshipUsingAlias('salesExec', 'sales_exec')

Instead of

$records = Order::leftJoinRelationship('salesExec', [
            'users' => function ($join) {
                $join->as('sales_exec');
            },
        ])->select('sales_exec.first_name')->first();

orWhere closure with local scopes won't work

Item.php

class Item extends Model
{
   use \Kirschbaum\PowerJoins\PowerJoins;

    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }
}

Order.php

class Order extends Model
{
  use \Kirschbaum\PowerJoins\PowerJoins;

  public function scopePending($query)
  {
      return $query->where('status', 1); //pending status
  }
  
  public function scopeConfirmed($query)
  {
      return $query->where('status', 2); //confirmed status
  }

  public function scopeCancelled($query)
  {
      return $query->where('status', 3); //cancelled status
  }

  public function scopeNotExpired($query)
  {
      return $query->where('expires_at', '>', now());
  }
}

Fetching all items doesn't having order (pending and expired)
This is not working

Item::powerJoinWhereHas('order', function ($query) {
                $query->where(function ($qroup) {
                    $qroup->where('status', '!=', 1)
                        ->orWhere(function ($q) {
                            $q->pending()->notExpired()
                        });
                });
            })

Throws error

InvalidArgumentException with message 'Method pending does not exist in PowerJoinClause class'

Following works

Item::powerJoinWhereHas('order', function ($query) {
                $query->where(function ($qroup) {
                    $qroup->where('status', '!=', 1)
                        ->orWhere(function ($q) {
                            $scoped=Order::make()->scopePending($q);
                            Order::make()->scopeNotExpired($scoped);
                        });
                });
            })

BelongsToMany with relatedKey

Hi,

PowerJoins does not work with belongToMany when we use custom related key (6th parameter), for example :
return $this->belongsToMany("\App\Models\Material", "CUSTOMER_MATERIAL", "CUSTNO", "CARNO", "CUSTNO", "CARNO");

Generated query is :
select * from [CUSTOMER] left join [CUSTOMER_MATERIAL] on [CUSTOMER_MATERIAL].[CUSTNO] = [CUSTOMER].[CUSTNO] left join [MATERIAL] on [MATERIAL].[IDMaterial] = [CUSTOMER_MATERIAL].[CARNO]
Should be :
select * from [CUSTOMER] left join [CUSTOMER_MATERIAL] on [CUSTOMER_MATERIAL].[CUSTNO] = [CUSTOMER].[CUSTNO] left join [MATERIAL] on [MATERIAL].[CARNO] = [CUSTOMER_MATERIAL].[CARNO]

I think line 108 in RelationshipsExtraMethods.php should be:
"{$this->getModel()->getTable()}.{$this->getRelatedKeyName()}",

Error when the relation has a function inside the where

In my code, in the relation I have added a function inside the where, looking like this:

return $this->hasOne($translateModel, $this->getForeignKey(), $this->getKeyName())->where(function ($q){ ... });

And it gives me the following error:
Undefined index: column

I have looked a bit and the problem comes from the file "eloquent-power-joins/src/Mixins/RelationshipsExtraMethods.php" in the method "shouldNotApplyExtraCondition()",

Have a:
return $condition['column'] === $key;

But it has inside of "$condition":
array:3 [โ–ผ "type" => "Nested" "query" => Illuminate\Database\Query\Builder {#1737 โ–ถ} "boolean" => "and" ]

Fatal Error

I'm getting this error: Fatal error: Uncaught BadMethodCallException: Call to undefined method Illuminate\Database\Eloquent\Builder::getSelect()

The issue is that the PowerJoins trait tries to call getSelect in the scopeJoinRelationship method. The code giving me issues is using Laravel 6. Below is what I wrote when calling it.

$recipients = UserData::joinRelationship('users')->where('resub_sent', false)->dd();

Here's what UserData looks like.

use Illuminate\Database\Eloquent\Model;
use Kirschbaum\PowerJoins\PowerJoins;

class UserData extends Model
{
	use PowerJoins;
}

Full error:

Fatal error: Uncaught BadMethodCallException: Call to undefined method Illuminate\Database\Eloquent\Builder::getSelect() in [REDACTED]/vendor/illuminate/support/Traits/ForwardsCalls.php:50
Stack trace:
#0 [REDACTED]/vendor/illuminate/support/Traits/ForwardsCalls.php(36): Illuminate\Database\Eloquent\Builder::throwBadMethodCallException()
#1 [REDACTED]/vendor/illuminate/database/Eloquent/Builder.php(1377): Illuminate\Database\Eloquent\Builder->forwardCallTo()
#2 [REDACTED]/vendor/kirschbaum-development/eloquent-power-joins/src/PowerJoins.php(43): Illuminate\Database\Eloquent\Builder->__call()
#3 [REDACTED]/vendor/illuminate/database/Eloquent/Builder.php(986): [WordPressProj]\Models\UserData->scopeJoinRelationship()
#4 [REDACTED]/vendor/illuminate/database/Eloquent/Builder.php(1370): in [REDACTED]/vendor/illuminate/support/Traits/ForwardsCalls.php on line 50

Please note that this implementation is being used outside of a Laravel project, but it uses the following things inside composer.json.

"illuminate/database" : "^6.0",
"illuminate/filesystem" : "^6.0",
"illuminate/translation" : "^6.0",
"illuminate/validation" : "^6.0",
"illuminate/pagination" : "^6.0",
"kirschbaum-development/eloquent-power-joins" : "^2.1"

If I'm missing something that PowerJoins relies on, please let me know. It just seems strange that in the years I've been working with Laravel on this same project, that this would be the first time I'd have something missing that Eloquent ORM needs to function in a given way.

Get error method getSelect not exists when run in testing environment

DB config:
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306

Step to reproduce:

  1. Call relationship(hasMany) query
  2. Then, Call powerJoinWhereHas
    Code Sample:
$this->members()
          ->powerJoinWhereHas(
              'socialOnlineStatus',
              fn ($query) => $query->online()
          )->excludeSelf()->exists();

Trace:

{\n
    "message": "Call to undefined method Illuminate\\Database\\Eloquent\\Builder::getSelect()",\n
    "exception": "BadMethodCallException",\n
    "file": "/Users/raymond/Desktop/iebuy-api/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php",\n
    "line": 50,\n
    "trace": [\n
        {\n
            "file": "/Users/raymond/Desktop/iebuy-api/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php",\n
            "line": 36,\n
            "function": "throwBadMethodCallException",\n
            "class": "Illuminate\\Database\\Eloquent\\Builder",\n
            "type": "::"\n
        },\n
        {\n
            "file": "/Users/raymond/Desktop/iebuy-api/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php",\n
            "line": 1511,\n
            "function": "forwardCallTo",\n
            "class": "Illuminate\\Database\\Eloquent\\Builder",\n
            "type": "->"\n
        },\n
        {\n
            "file": "/Users/raymond/Desktop/iebuy-api/vendor/kirschbaum-development/eloquent-power-joins/src/PowerJoins.php",\n
            "line": 270,\n
            "function": "__call",\n
            "class": "Illuminate\\Database\\Eloquent\\Builder",\n
            "type": "->"\n
        },

PHP 8.0

It seems that this package does not support PHP 8.0? Does it only support ^7.1 or ^8.1?

Any reason why 8.0 is skipped?

Global scopes

I like this package, but global scopes are not applied to anything I join.

There is that hardcoded case of soft deletes, but no solution ready for other global scopes.

Is there a good way to add global scopes to a joined relation?

Example:

class Bar extends Model {
    public function foos(): HasMany { }
}
class Foo extends Model {
    // hide all foo with hidden = 1
    use WithHiddenScope;
}

// all foo are considered, even the ones with hidden = 1
$bar = Bar::joinRelationship('foos');

orderByUsingJoins fails when called in a 2nd query for the same model

We have the following code:

$user = auth()->user();

$query1 = $user->comments()
    ->orderByUsingJoins('post.created_at', 'desc')
    ->get();

$query2 = $user->comments()
    ->orderByUsingJoins('post.created_at', 'desc')
    ->get();

Query 1 runs successfully. However, query 2 fails:

Unknown column 'posts.created_at' in 'order clause'

This happens because the join statement is missing from the 2nd query, while the order by part is still present.

Calling $user->refresh() between the two queries, works around this issue.

nested powerJoinWhereHas failure

As powerJoinWhereHas expects instance of Illuminate\Database\Eloquent\Builder, we are unable to use it in below query

return Category::with(['getBrands' => function ($brand) {
            $brand->distinct('brands.id')
            ->powerJoinWhereHas('products', function ($join) {
                return $join->where(['active' => true, 'website' => true])->powerJoinWhereHas('channel', function ($channel) {
                    return $channel->where(['active' => true, 'website' => true]);
                });
            })
            ->withCount('products');
        }])->withCount('products')->get();

this throws
Argument 1 passed to App\\Models\\Product\\Product::scopePowerJoinWhereHas() must be an instance of Illuminate\\Database\\Eloquent\\Builder, instance of Kirschbaum\\PowerJoins\\PowerJoinClause given

along with this issue, a not unique table/alias issue also occured when i tried again after removing nested powerJoinWherHas like :

return Category::with(['getBrands' => function ($brand) {
            $brand->distinct('brands.id')
            ->powerJoinWhereHas('products', function ($join) {
                return $join->where(['active' => true, 'website' => true]);
            })
            ->withCount('products');
        }])->withCount('products')->get();

both of the above queries works when used with default whereHas.

Thanks.

issue with powerJoinHas

When I try to use "powerJoinHas" I get the following error:

Country::powerJoinHas('translation')->get()

SQLSTATE[42000]: Syntax error or access violation: 1055 'countries.iso2' isn't in GROUP BY (SQL: select countries.*, count(country_translations.id) as country_translations_count from countries left join country_translations on country_translations.country_id = countries.id and country_translations.locale = en group by countries.id having country_translations_count >= 1 limit 1)

Use relation collections

First of all: thanks for this cool library! ๐Ÿ˜Š

Right now, I'm scratching my head because when I do the join all the data is retrieved correctly but all attributes are stored in the attributes of the model (even foreign attributes). Is it somehow possible to store the retrieved data automatically in the corresponding relations?

So after using $post->joinRelationship('user')->get() I want to be able to use $post->user. Is that even possible?
Thanks!

[2.3.3] call multi joinRelationship function query builder

            $query->where('status', 'ACTIVE');
        });

$queryBuilder->joinRelationship('relationships', function ($query) use ($condition) {
            $query->where('role', 'ADMIN');
        });```

The query just accept first condition, as I see in PowerJoins::scopeJoinRelationship, we check relationshipAlreadyJoined
So, this function will be return.
Please help to support multi where statement like above, I tried with order it work well.

Issue with aliased joins

Hey there,

I am having issues, using nested relationships that have been aliased. Take this example:

Purchase::where('purchases.id', 1)
			->leftJoinRelationShip('serial.product', [
				'serial' => function ($join) {
					$join->as('purchase_serial');
				},
				'product' => function ($join) {
					$join->as('purchase_product');
				},
			])
			->first();

This will produce the following error:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'serials.product_id' in 'on clause' (SQL: select purchases.* from purchases left join serials as purchase_serial on purchase_serial.code = purchases.purchase_code left join products as purchase_product on serials.product_id = purchase_product.id where purchases.id = 1 limit 1)

That's because the on clause does not take the aliased table name into account. So, insead of

on `serials`.`product_id` = `purchase_product`.`id`

it should actually be

on `purchase_serial`.`product_id` = `purchase_product`.`id`

I went ahead and tried to manually specify the on clause like this

Purchase::where('purchases.id', 1)
			->leftJoinRelationShip('serial.product', [
				'serial' => function ($join) {
					$join->as('purchase_serial');
				},
				'product' => function ($join) {
					$join->as('purchase_product')->on('purchase_serial.product_id', '=', 'purchase_product.id');
				},
			])
			->first();

Unfortunately this will append the on clause, instead of replacing it and will therefore result in this error:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'serials.product_id' in 'on clause' (SQL: select purchases.* from purchases left join serials as purchase_serial on purchase_serial.code = purchases.purchase_code left join products as purchase_product on serials.product_id = purchase_product.id and purchase_serial.product_id = purchase_product.id where purchases.id = 1 limit 1)

Thanks for your time!

Scoped relationship with different models of same column names throws error

I have 2 models that have similar column names i.e. name, joins throw this error:

SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'name' in on clause is ambiguous

select `videos`.* from `videos` inner join `tracked_jobs` on `tracked_jobs`.`trackable_id` = `videos`.`id` and `trackable_type` = App\\Models\\Video and `name` = ProcessVideo and `status` = finished

Model Video:

    public function last_successful_job()
    {
        return $this->morphOne(TrackedJob::class, 'trackable')->finished();
    }

Model TrackedJob:

    public function scopeFinished($query)
    {
        return $query->where('name', 'ProcessVideo')
            ->where('status', 'finished')
            ->orderBy(TrackedJob::CREATED_AT, 'desc');
    }

However if I try to use joinRelationshipUsingAlias like so, I get a different error.
I also noticed that the SQL generated is different from above, as it is ignoring my scope.

$video->joinRelationshipUsingAlias('last_successful_job', 'testme')->get();
SQLSTATE[HY093]: Invalid parameter number 

select `videos`.* from `videos` inner join `tracked_jobs` as `testme` on `testme`.`trackable_id` = `videos`.`id`

Any way of getting around this?

php-8

need update composer.json

PHP 8.1 Support

I was testing my app with PHP 8.1 and got these deprecation notices.

Accessing static trait property Kirschbaum\PowerJoins\PowerJoins::$joinMethodsMap is deprecated, it should only be accessed on a class using the trait in /app/vendor/kirschbaum-development/eloquent-power-joins/src/PowerJoins.php on line 42

Accessing static trait property Kirschbaum\PowerJoins\PowerJoins::$powerJoinAliasesCache is deprecated, it should only be accessed on a class using the trait in /app/vendor/kirschbaum-development/eloquent-power-joins/src/PowerJoins.php on line 386

Accessing static trait property Kirschbaum\PowerJoins\PowerJoins::$powerJoinAliasesCache is deprecated, it should only be accessed on a class using the trait in /app/vendor/kirschbaum-development/eloquent-power-joins/src/Mixins/RelationshipsExtraMethods.php on line 305

Feature request: Relationship join on pivot

First of all: Thank you for the awesome package and happy easter ๐Ÿฐ !

In our current project, we are using the relationshipJoins with various many-to-many relations, but do not really care about the data on the relationship table and instead only need to join the pivot table.
So I wanted to suggest adding a function, which does the same as relationshipJoin, which already does an inner join on the pivot, but does not also join the table of the related model.

Thanks

Select `relationship_table`.* is not added to select

I have a code:

Models\CatalogProductEntity::leftJoinRelationship('catalog_product_entity_varchars.eav_attribute')->limit(1)->get();

and select is

select `catalog_product_entity`.* from `catalog_product_entity` left join `catalog_product_entity_varchar` on `catalog_product_entity_varchar`.`row_id` = `catalog_product_entity`.`row_id` left join `eav_attribute` on `catalog_product_entity_varchar`.`attribute_id` = `eav_attribute`.`attribute_id` limit 1"

Select is :

or even simple:

\Models\CatalogProductEntity::leftJoinRelationship('catalog_product_entity_varchars')->limit(1)->get();

Select Is:

"select `catalog_product_entity`.* from `catalog_product_entity` left join `catalog_product_entity_varchar` on `catalog_product_entity_varchar`.`row_id` = `catalog_product_entity`.`row_id` limit 1"

Should be

"select `catalog_product_entity`.*, `catalog_product_entity_varchar`.*   from `catalog_product_entity` left join `catalog_product_entity_varchar` on `catalog_product_entity_varchar`.`row_id` = `catalog_product_entity`.`row_id` limit 1"

You have this described in the doc however you don't have doc how to add fields from the join. It doesn't have any sence join and not select fields:

https://github.com/kirschbaum-development/eloquent-power-joins#select--from-table

I have written some solution could you consider adding this to the core. and select with TableName.*

$query->leftJoinRelationship('eav_attribute')->addSelect(['eav_attribute.*']);

When clause

Thank you for the awesome package and really makes working with joins easier.

In our current project, we are using the relationshipJoins to filter on datatables, however we don't always have the filters active we rely alot on the ->when(bool, closure(Builder $query){//}) functionality however since the closure is an instance of the query builder we lose the functionality of the package

To this we thought about two potential solutions

  1. adding a powerJointWhen function suite
  2. Extending the functionality for the underlying querybuilder somhow (might be too involved)

However perhaps I'm missing something and this is already feasible with the current package.

Joining relationships twice causes SQL Exception.

This code produces an SQL Exception, because the relationship is joined twice in the query:

Model::query()
  ->joinRelationship('relation')
  ->joinRelationship('relation')
  ->dd();

outputs:

select `model`.* from `model` inner join `relation` on `relation`.`id` = `model`.`id` inner join `relation` on `relation`.`id` = `model`.`id`

I've tried to get to the bottom of the bug and for me it seems like the PowerJoins::$joinRelationshipCache cache gets emptied for some reason.

But this only happens in this case. When joining two nested relationships (as in the test case) the sql query is correct, that's why this bug wasn't covered by the tests.

Join missing from query

If you have two different relations to the same table and then have a shared relation on those relations, then if you trying querying them you end up missing 1 join in the resulting query. An example will make this much clearer I think:

 $query =  \App\Models\Event::query()
            ->leftJoinRelationShip('location.planet', [
                'location' => function($join){
                    $join->as('location_alias');
                },
                'planet' => function($join){
                    $join->as('location_planet_alias');
                }
            ])
            ->leftJoinRelationShip('secondaryLocation.planet', [
                'secondaryLocation' => function($join){
                    $join->as('secondary_location_alias');
                },
                'planet' => function($join){
                    $join->as('secondary_location_planet_alias');
                }
           ])
           ;

Results in:

select `events`.* from `events` 
    left join `locations` as `location_alias` on `events`.`location_id` = `location_alias`.`id` 
    left join `planets` as `location_planet_alias` on `location_alias`.`planet_id` = `location_planet_alias`.`id` 
    left join `locations` as `secondary_location_alias` on `events`.`secondary_location_id` = `secondary_location_alias`.`id`

The there should also have been another left join to planets for the secondary location.

I think it caused by the usage of the $relationJoinCache in the method scopeJoinNestedRelationship.

$relationJoinCache = $index === 0
                ? "{$relation->getQuery()->getModel()->getTable()}.{$index}.{$relationName}"
                : "{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getModel()->getTable()}.{$relationName}";

It uses the {$latestRelation->getModel()->getTable()} (tablename) but I think it should probably be useing the latestRelation's relationname right?

This seems to fix the problem:

$relationJoinCache = $index === 0
                ? "{$relation->getQuery()->getModel()->getTable()}.{$index}.{$relationName}"
                : "{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getRelationName()}.{$relationName}";

Is this something you can change / would merge if I create a Pr?

Usage without traits possible?

Hello,

I have a package that accepts model classes to create tables for it, but when it comes to sorting and filtering, it needs joins instead of relationship instances. But since the classes are dynamic and given from outside world, I don't have the chance to extend the model classes with traits. I can do it with using eval, but it's not recommended, and will not work in most servers, it's not an option. So I need to use PowerJoins trait methods without the trait...

Is it somehow possible? For example

$model = PowerJoins::joinRelationship($model, ["related1", "related2"]);

Thanks.

boolean scopes with false doesnt get correctly substituted in the final query

for example

SELECT
	`drivers`.*,
	(
		SELECT
			count(*)
		FROM
			`policies`
		WHERE
			`drivers`.`id` = `policies`.`driver_id`
			AND `to_be_collected` =) AS `policies_count`, (
			SELECT
				count(*)
			FROM
				`policies`
			WHERE
				`drivers`.`id` = `policies`.`driver_id`
				AND `to_be_collected` = 1) AS `receipts_count`, (sum(policies.items_total) + sum(policies.freight - policies.amount)) AS `debt`
		FROM
			`drivers`
		LEFT JOIN `policies` ON `policies`.`driver_id` = `drivers`.`id`
			AND `to_be_collected` =
			AND `is_returned` =
			AND `is_delivered` =
			AND `is_closed` =
		GROUP BY
			`drivers`.`id`
		ORDER BY
			`debt` DESC

note all the to_be_collected / is_returned /etc.. = should have 0 for a value which correspond to the original scope

public function scopeIsOpen($q)
{
    return $q->where('is_returned', false)
              ->where('is_delivered', false)
              ->where('is_closed', false)
              ->where('to_be_collected', false);
}

also here is the query

$query->leftJoinRelationship('opened_policies')
      ->selectRaw('(sum(policies.items_total) + sum(policies.freight - policies.amount)) AS `debt`')
      ->groupBy('drivers.id')
      ->orderBy('debt', $sort_dir);

am not sure if am missing something or if its a bug but when adding 0 to all the fields and run it directly, the data is correctly ordered,
if u need any more info plz do tell.

whereHasUsingJoins fails inside model scopes

When using whereHasUsingJoins inside a model scope, it fails as Laravel tries to use dynamic wheres instead of using the proper method. Need to figure out a good approach for this, but may need to ship a new version where we include a trait in the models instead of adding macros due to how Laravel works.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.