Optimization of Laravel pivot table relationship - mysql

I have a pivot table called invite_riskarea which is designed as follows:
This table handles the permissions that have a specific user (through an invite id) to access to specific riskfields. Each riskfield is associated with a riskarea which acts as the main container of specific riskfields.
Within the model Invite I have this relationship:
public function riskareas()
{
return $this->belongsToMany(Riskarea::class)->withPivot('riskfield_id', 'insert', 'edit', 'view');
}
In this way I can return all the riskareas associated with a specific invite, and I should be able to return all the riskfields associated with a specific riskarea in the same invite model.
As you can see from the table invite_riskarea, I have three columns called insert, edit, and delete. These columns manage the types of permissions assigned to a specific user (via an invite id) for a specific riskfield belonging to a riskarea.
I'm trying to retrieve the riskarea permission in the following way:
$invite = Invite::where('id', 58)->first();
$riskarea = $invite->riskareas[0];
$riskfield = $riskareas->riskfields[0];
echo 'view permission => ' . $riskfield->insert;
The problem's that I'm not able to setup a correct relationship in the Invite model that returns me the pivot data of the permissions columns only for the riskfield associated with the riskarea.
So I have manage to handle this situation in this way:
$riskareas = Riskarea::all();
foreach ($riskareas as &$riskarea) {
foreach ($riskarea->riskfields as &$riskfield) {
$result = DB::table('invite_riskarea')
->select('insert', 'edit', 'view')
->where([
'riskarea_id' => $riskarea->id,
'riskfield_id' => $riskfield->id
])
->first();
if ($result) {
$riskfield->insert = $result->insert;
$riskfield->edit = $result->edit;
$riskfield->view = $result->view;
}
}
}
Essentially, I get all the riskareas, and then I iterate over the riskfields associated. For each riskfield, I get the permissions in the invite_riskarea table and then I have the correct structure that I want.
So to summarize:
Is it actually possible create a model relationship that returns the permissions for riskfield and not for riskarea?
Is my table implementation good enough to handle that situation?

I suggest you define back the many-to-many relation for the Riskfield model with the Invite model.
You can also define a direct many-to-many relationship with riskfield in the Invite model. This is how convenient it is for you personally.
And so the inverse many-to-many relationship
public function invites()
{
return $this->belongsToMany(Invite::class)->withPivot('insert', 'edit', 'view');
}
Then get all objects' Riskfields that are associated with the specified invite:
$riskfields = Riskfields::wherehas('invites' . function (Builder $query) use ($invite_id) {
$query->where('invites.id', $invite_id);
})->with('invites')->get();
Then you can access the desired fields of the pivot table in the specified way:
foreach ($riskfields as $riskfield) {
foreach ($riskfield->invites as $invite) {
$insertRiskField = $invite->pivot->insert;
$editRiskField = $invite->pivot->edit;
$viewRiskField = $invite->pivot->view;
}
}
Eager loading executes one query to the database
Yes
Documentation Laravel

Related

Laravel Many To Many Merges Pivot?

I must be going insane or be really tired. So I have this situation where I get a collection of all the Roles assigned to the User. That part goes ok.... however I noticed something super strange.
I am using Laravel 8 and PHP8 (not the strange part).
For some reason, I do not get only the result from the other table but also pivot data is merged in. I can't tell why this is happening. Here is the example:
Relationship on user model:
/**
* Relationship with roles model.
*
* #return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(
Role::class,
'role_user',
'user_id',
'role_id'
)->withTimestamps();
}
Relationship on the Role model:
/**
* Relationship with users table.
*
* #return BelongsToMany
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(
User::class,
'role_user',
'role_id',
'user_id'
)->withTimestamps();
}
In the user model, I have this.
$this->roles->each(function($role) {
dd($role);
});
I was expecting to get a dump of related model however for some weird reason what I get is pivot table merged with the model:
"id" => 7 // this is the relation ID from the pivot table
"display_name" => "Administrator" // this is from Role model
"code" => "admin" // role model
"description" => "Super User - can do everything in the system. This role should only be assigned to IT staff member." // role model
"created_at" => "2021-10-01 11:00:00" // pivot table
"updated_at" => null // pivot table
"deleted_at" => null // pivot table
"role_id" => 1 // pivot table
"user_id" => 2 // pivot table
Either I am doing something very wrong or I am missing something very obvious. Does anyone know what in the world is happening here?
Just to add: the data is from both places but the result is just a Role model as expected.
Should I not just get the role model without the pivot stuff in it? It is overriding my role model fields.
EDIT:
Parenthesis seems to make a difference. The data is still merged. However, when I do it like this looks like data from end model is merged (so it overrides) to data from the pivot. So I get correct ID.
$this->roles()->each(function($role) {
echo $role;
});
But this gives me this weird pivot merged version with wrong ID.
$this->roles->each(function($role) {
echo $role;
});
I know what that was exactly. Without thinking I've added the ID column into the pivot table.
This ID from pivot was overriding my ID from my end model. After I've removed it the problem is gone.
I don't know why Laravel would by default add these fields and merge with pivot columns... I guess it just does that for no reason. Although I don't understand what's the point if there is a separate mechanism to access the pivot table (pivot relationship on the model).
This makes me think I did something wrong. But yeah, hope it helps. If anyone knows why Laravel automatically adds pivot stuff, let me know.

Selecting a value from an n:n relationship

I currently have three tables: users, roles, and a user_to_role “pivot” table defining a many-to-many relationship between users and roles:
users
protected $fillable = [
'name', 'email', 'password',
];
user_to_role
protected $fillable = [
'id', 'user_id', 'role_id'
];
roles
protected $fillable = [
'id', 'role_name',
];
The role_name values are admin and client.
When a user logs in, I want to show a view for the specific role that the user is assigned. I don't really know how to do that in the controller, however. I have something like the following, but I know it won’t work:
public function index()
{
if (Auth::user()->role_id==1) {
// and something here which I don't know
return view('homeadmin');
}
}
I know I have to take the id from the roles table, make the connection with the user_to_role pivot, and then join that with the users table, but I don't really know how.
You need to define a relationship between User model and Role model.
# User.php
public function roles()
{
return $this->belongsToMany(Role::class, 'user_to_role');
}
Optionally, define the relationship on Role model as well.
# Role.php
public function users()
{
return $this->belongsToMany(User::class, 'user_to_role');
}
Then, you can access the relationship and use collection methods on it.
public function index()
{
// or Auth::user()->roles->contains('role_name', 'admin') if you want to be specific
if (Auth::user()->roles->contains('id', 1)) {
return view('homeadmin');
}
return view('homeuser');
}
Optionally, you could make a method in the User model to check if an user is admin or client.
# User.php
public function isAdmin()
{
return $this->roles->contains('id', 1); // or contains('role_name', 'admin')
}
public function isClient()
{
return $this->roles->contains('id', 2); // or contains('role_name', 'client')
}
public function index()
{
if (Auth::user()->isAdmin()) {
return view('homeadmin');
}
return view('homeclient');
}
Eloquent Relationships - Many to Many
Collections - contains() method
First of all, if you have User and Role Model mapping to your users and roles table, the convention is to name your pivot table role_user. But you can get along with your current table naming as well.
I would agree the answer of IGP and add a few more suggestions.
If you just need to implement role and user and don't have to build it yourself, there are plenty of existing packages that can help you handle role-permission. You don't really needs to build from scratch. For example, depends on the Laravel version you use, you may choose;
spatie/laravel-permission
Bouncer
Zizaco/Entrust
If you would like to implement role management yourself, when you define your relationship, you need to think about if a user would have multiple roles in the future. Based on what you show us right now, there are only client and admin role. Looks like a user would only be either client or admin but not both. And if you are sure those are the only two roles and a user would be either one, you don't need to have roles table at all. You can just add a boolean column such as is_admin in users table to flag the role.
But let's say you will have more roles, and a user can have multiple roles at the same time. Then you DO need to define a many to many relationship. Other answers already provide example on that pretty well. I would also suggest to define a universal role-handling model function to check all roles. In your User.php model,
public function hasRole($role)
{
// check if user have any of the specified roles
if (is_array($role)) {
foreach($role as $r) {
if ($this->roles->contains('role_name', $r)) {
return true;
}
}
return false;
} else {
return $this->roles->contains('role_name', $role);
}
}
That way, in anywhere in your App, you can check your user role by calling
Auth::user()->hasRole('admin');
or check if user contains any role in a list by calling
Auth::user()->hasRole(['admin', 'client']);

Eloquent query problem using with() function for model relationship eager loading

How do write this eloquent query in Laravel so that it eager loads with() the relationship model in this example between a User model and Profile model? I was trying to avoid 2 separate queries.
I feel I am close, but somethings not quite right.
$author = User::where('id', $id)->with('profile')->get();
The collection is returning the user details correctly. But it's showing the profile relationship as null.
#relations: array:1 [▼
"profile" => null
]
I believe I have things setup correctly with a User model and a Profile needed relationships.
User.php
public function profile()
{
return $this->hasOne('App\AuthorProfile', 'user_id');
}
AuthorProfile.php
public function user()
{
return $this->belongsTo('App\User');
}
Assuming for AuthorProfile model table you have record with id of user it should be fine.
However you wrote:
I was trying to avoid 2 separate queries.
Well, it's not true, if you have single record, eager loading won't help you at all. In this case 2 queries will be execute - no matter if you use eager loading or you won't.
Eager loading would help if you had multiple users and for each of them you wanted to load profile, but if you have single record it won't change anything.
Additionally instead of:
$author = User::where('id', $id)->with('profile')->get();
you should rather use:
$author = User::with('profile')->find($id);
because you expect here single user.
$users = User::with('profile')->find($id);
Your model should be like this.The User_id on the profile table and id on the user table
public function profile()
{
return $this->hasOne('App\AuthorProfile', 'user_id','id');
}

Relationships between tables in laravel using backpack package

I am using backpack CRUD package to create my website project in laravel 5.2
I want to establish a relationship between two tables. First table is called customer and second table is called transaction. Each customer has many transaction(1:N relationship).
Customer table record:
ID Name
123456 xyz
Transaction table record:
ID CustomerID
101010 123456
I know that I have to specify the relation in the customer model. But, how can I display the result of the relationship in CRUD ?
You should have relationships on both the Transaction and the Customer models, so you can do $customer->transactions and $transaction->customer:
class Customer extends Model
{
/**
* Get the comments for the blog post.
*/
public function transactions()
{
return $this->hasMany('App\Transactions', 'CustomerID', 'ID');
}
}
and
class Transaction extends Model
{
/**
* Get the comments for the blog post.
*/
public function customer()
{
return $this->belongsTo('App\Customer', 'CustomerID', 'ID');
}
}
Spend some time in the Eloquent Relationships Documentation. It's really important to understand them if you want to be a Laravel developer.
In order to display the relationship in the CRUD, you can then use Backpack's select column type to display it in the table view and select or select2 field types to display it in the add/edit views. Read the CRUD Example Entity to better understand how that works.
First of all when you are creating migrations for both tables, table which contain Foreign Key (FK) must have field like this:
public function up(){
$table->increments('id');
$table->integer('customerID')->unsigned();
}
After that you are need to call next command into console
php artisan migrate
Next is going next commands:
php arisan backpack:crud customers
php arisan backpack:crud transactions
After that you need to define functions in models which returns values from other tables. Customer models need to have next function
public function transactions(){
return $this->hasMany('Transaction');
}
Transaction model must have next function
public function customer() {
return $this->belongsTo('Customer');
}
Next you must add CRUD field in Customer controller to display
transactions in select box.
$this->crud->addField([
'label' => 'Transactions', // Label for HTML form field
'type' => 'select2', // HTML element which displaying transactions
'name' => 'customerID', // Table column which is FK for Customer table
'entity'=> 'customer', // Function (method) in Customer model which return transactions
'attribute' => 'ID', // Column which user see in select box
'model' => 'Transaction' // Model which contain FK
]);
Hope this helps :)
After you built onetomany relationship with transaction, you can get the results.
$customer=Customer::where(['id'=>'123456'])->with('transaction')
->first();
print_r($customer->Name); // gives the customer name
foreach($customer->transaction as $cid)
{
print_r($cid->CustomerID); // gives the customer id
}
Laravel Relationships Documentation is always helpful. Go through it.

Yii model: Dynamic table relations

Table.linkedIndex is related to LinkedIndex.ID. The value of the field LinkedIndex.TableName is either Linked1 or Linked2 and defines which of these tables is related to a row in Table.
Now i want to make a dynamical link with Yii models so that i can easily get from a Table row to the corresponding Linked1 or Linked2 row:
Table.linkedID = [LinkedIndex.TableName].ID
Example
Table values:
LinkedIndex values:
Now I should get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;
Model
In the model Table, I tried to make the relation to the table with the name of the value of linkedIndex.TableName:
public function relations()
{
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
'linked' => array(
self::HAS_ONE,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
)
}
But then I get the error:
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
Is there any way to make a dynamic relation Table.linkedID -> [LinkedIndex.TableName].ID with Yii Models?
Per the Yii docs here:
http://www.yiiframework.com/doc/api/1.1/CActiveRecord#relations-detail
I'd suggest using self::HAS_ONE instead (unless there can be multiple rows in LinkedIndex with the same ID - although from the looks of above, I doubt that's the case).
You can link tables together that have different keys by following the schema:
foreign_key => primary_key
In case you need to specify custom PK->FK association you can define it as array('fk'=>'pk'). For composite keys it will be array('fk_c1'=>'pk_с1','fk_c2'=>'pk_c2').
so in your case:
public function relations(){
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
);
}
where LinkedIndex is the class name for the LinkedIndex model (relative to your Table model - i.e. same folder. You could change that, of course) and array('ID' => 'linkedIndex') specifies the relationship as LinkedIndex.ID = Table.linkedIndex.
Edit
Looking at your updated example, I think you're misunderstanding how the relations function works. You're getting the error
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
because you're trying to create another relation here:
'linked' => array(
self::BELONGS_TO,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
This part: linkedIndex.TableName refers to a new model class linkedIndex.TableName, so Yii attempts to load that class' file linkedIndex.TableName.php and throws an error since it doesn't exist.
I think what you're looking for is to be able to access the value TableName within the table LinkedIndex, correct? If so, that's accessible from within the Table model via:
$this->linkedIndex->TableName
This is made possible by the relation we set up above. $this refers to the Table model, linkedIndex refers to the LinkedIndex relation we made above, and TableName is an attribute of that LinkedIndex model.
Edit 2
Per your comments, it looks like you're trying to make a more complex relationship. I'll be honest that this isn't really the way you should be using linking tables (ideally you should have a linking table between two tables, not a linking table that says which 3rd table to link to) but I'll try and answer your question as best as possible within Yii.
Ideally, this relationship should be made from within the LinkedIndex model, since that's where the relationship lies.
Since you're using the table name as the linking factor, you'll need to create a way to dynamically pass in the table you want to use after the record is found.
You can use the LinkedIndex model's afterFind function to create the secondary link after the model is created within Yii, and instantiate the new linked model there.
Something like this for your LinkedIndex model:
class LinkedIndex extends CActiveRecord{
public $linked;
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return 'LinkedIndex';
}
public function afterFind(){
$this->linked = new Linked($this->TableName);
parent::afterFind();
}
//...etc.
}
The afterFind instantiates a new Linked model, and passes in the table name to use. That allows us to do something like this from within the Linked model:
class Linked extends CActiveRecord{
private $table_name;
public function __construct($table_name){
$this->table_name = $table_name;
}
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return $this->table_name;
}
//...etc.
}
which is how we dynamically create a class with interchangeable table names. Of course, this fails of the classes need to have separate operations done per-method, but you could check what the table_name is and act accordingly (that's pretty janky, but would work).
All of this would result in being to access a property of the linked table via (from within the Table model):
$this->linkedIndex->linked->foo;
Because the value of LinkedIndex.TableName and Table.linkedID is needed to get the values, I moved the afterFind, suggested by M Sost, directly into the Table-Class and changed its content accordingly. No more need for a virtual model.
class Table extends CActiveRecord {
public $linked; // Needs to be public, to be accessible
// ...etc.
public function afterFind() {
$model = new $this->linkedIndex->TableName;
$this->linked = $model::model()->findByPk( $this->linkedID );
parent::afterFind();
}
// ...
}
Now I get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;