The Wayback Machine - https://web.archive.org/web/20231128091738/https://github.com/laravel/framework/pull/48613
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Added many-to-many relation trough set collumn #48613

Draft
wants to merge 9 commits into
base: 10.x
Choose a base branch
from

Conversation

Daniel-H123
Copy link

@Daniel-H123 Daniel-H123 commented Oct 2, 2023

Note: This is my first contribution to the laravel framework

I recently worked on a project, where I came across a many-to-many relation with the keys in a set collumn. I couldn't find any 'good' solution to add the relation so I thought, why not add it, since they can be usefull in my opinion. Please note that i'm not experienced in modifying the laravel framework code, since this is my first pr, so any feedback is dearly appreciated.

Summary:

The hasManyInSet belongsToManySet relation allows you to define a many-to-many relation between two tables, using a set collumn, so without having to use a pivot table. While a pivot table is recommended, it is still usefull to use the many-to-many relation in this form.

Situation:

Users:

id username groups
1 User1 1,2,5
2 User2 3
3 User3

Groups:

id name
1 Group1
2 Group2
3 Group3
4 Group4
5 Group5

Example:

You can setup the relation for the user in this example in the model with belongsToManySet().

<?php // Model

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function groups() 
    {
        return $this->belongsToManySet(Group::class, 'groups', 'id');
    }
}

You can call the relation just like any other relation in Laravel.

<?php // Controller

namespace App\Http\Controllers;

use Illuminate\Database\Eloquent\Model;

class UserController extends Controller
{
    public function getUsersWithGroups() 
    {
        $users = User::select('*')->with('groups')->get();
        return response()->json($users);
    }

    public function getUserGroups($user) 
    {
        $users = User::find($user->id)->groups;
        return response()->json($users);
    }

}
@Daniel-H123
Copy link
Author

After further thought it should be something like belongsToManySet, but that just doesn't sound right.

@Rizky92
Copy link

Rizky92 commented Oct 4, 2023

I'm not sure how it'll go. I personally don't see this type of database structure in the wild, but who knows?

Anyway, this looks like a new definition of relation rather than extension of existing BelongsToMany, since BelongsToMany requires explicit intermediate table. You'll also need to figure out how to do this if you think this is a BelongsToMany:

Group::first()
    ->users()
    ->createMany([
        // ...
    ])
    
@Daniel-H123
Copy link
Author

Daniel-H123 commented Oct 4, 2023

Anyway, this looks like a new definition of relation rather than extension of existing BelongsToMany, since BelongsToMany requires explicit intermediate table. You'll also need to figure out how to do this if you think this is a BelongsToMany:

I agree. I'm currently rewriting the relation extending the relation class directly. It currently does not work both ways (when the set collumn is the foreign collumn), so that is also a problem that needs to be solved.

Added compatibility for reverse relation (with set in foreign collumn)
@akr4m
Copy link
Contributor

akr4m commented Oct 10, 2023

The belongsToManySet relation offers a unique and streamlined approach to many-to-many relationships in Laravel. With further refinement, it has potential to be a valuable addition for specific use-cases I think.

@AndrewMast
Copy link

Correct me if I am wrong, but wouldn't this relation only work when on a MYSQL connection because FIND_IN_SET is exclusively a MYSQL function? Several PRs and issues over the years have gone nowhere and have been closed because of this. I am all for making the SQL grammar work on other connections by using other sql functions, but I think it would be best for this to be a standalone package. Honestly, if this was a package, I would have used it in several of my projects already.

@Daniel-H123
Copy link
Author

Daniel-H123 commented Oct 18, 2023

Thanks for your message @AndrewMast

Correct me if I am wrong, but wouldn't this relation only work when on a MYSQL connection because FIND_IN_SET is exclusively a MYSQL function?

This is true, and something I am working on at the moment. I'm looking to extend laravel with a custom whereInSet function, but I think it would be difficult to make it efficiënt (with using indices) and usable with all db engines.

I'll try to make it work, but if it doesn't, it would be inpractical adding it in laravel itself, so i'll just publish it as a package as you suggested.

@Daniel-H123
Copy link
Author

Daniel-H123 commented Oct 22, 2023

I added a new whereInSet() function to the Laravel builder, so it uses the like operator instead of the MySQL-only FIND_IN_SET function. It should work now with all other supported Laravel database engines. I'm still figuring out if there are things that I could have made more efficient, and there is also some logic that needs to be added, for example, adding and removing items from a set. However, I would appreciate any feedback on the quality or usefulness of the current revision of my code.

Newly added whereInSet() logic:

/**
  * Add a "where in set" clause to the query.
  *
  * @param  string  $column
  * @param  mixed|array|null  $values
  * @param  string  $boolean
  * @param  bool  $not
  * @return $this
  */
public function whereInSet($column, $values = null, $boolean = 'and', $not = false)
{
  $type = $not ? 'not like' : 'like';
  $groupBoolean = $not ? 'and' : 'or'; // Boolean between the wheres added next
  
  $wheres = [];
  foreach (Arr::wrap($values) as $value) {
      $wheres[] = [$column, $not ? '!=' : '=', $value, $groupBoolean]; // Value is exact match
      $wheres[] = [$column, $type, $value.',%', $groupBoolean]; // Value is first in set
      $wheres[] = [$column, $type, '%,'.$value.',%', $groupBoolean]; // Value is in middle of set
      $wheres[] = [$column, $type, $value.',%', $groupBoolean]; // Value is last in set
  }
  
  return $this->addArrayOfWheres($wheres, $boolean); // Add all wheres to query
}

Example use of the whereInSet() function:

User::whereInSet('groups', 1)->get();

Raw SQL query in this example:

SELECT *
FROM `users`
WHERE (
  `groups` = 1
  OR `groups` LIKE '1,%'
  OR `groups` LIKE '%,1,%'
  OR `groups` LIKE '%,1'
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants