Three-way pivot table or complex relationship in Laravel - php

I have two tables, shipments and customers. In the real world, a customer can be related to a shipment in three ways: as the biller, the destination and/or the origin.
So my question here is, do I have a pivot table with three columns, one for the shipment_id, one for the customer_id, and one for the relationship_type id? Or do I have separate tables? I'm not sure how best to approach this as it's the first of it's kind that I've run up against.

I faced this couple weeks ago and I came up with a solution.
Assuming that one customer can have different relations to different
shipments.
First of all you need a new model for customer roles obviously that model it will be Relation model.
First approach: You could solve this by using more than one pivot table which works but its not a good database design. I solved it first like this but realized its not optimal choice when it comes to db.
Second approach: You could solve this by defining pivot table as a model, but I havent tried that way even though I know it works and its a solution.
Better approach: use one pivot table for three models. In that case you have to define pivot table when you define a relationship example :
Customer model:
public function relations()
{
return $this->belongsToMany(Relation::class, 'customer_relation_shippment');
}
Relation model:
public function customers()
{
return $this->belongsToMany(Relation::class, 'customer_relation_shippment');
}
and the other model as well.
now lets say you want to add a relation to a customer.
Lets grab first customer and first shipment and say we want to add a relation as a biller:
$customer = Customer::first();
$shipment = Shipment::first();
$relation = Relation::where('name','biller')->get();
$customer->relations()->attach($shipment->id, ['relationship_type'=>$relation->id]);
By using only one pivot table of course its a bit more complex to perform operations towards those models like CRUD, but when it comes to database design/optimazation of course it is the right choice! Note that I came to this conclusion after dealing with a similar real world issue and it turned way more faster db interaction then using more than one pivot.

Here is how I would design your project.
I don't think you even need a pivot table or many-to-many relationship.
Note: For clarity and avoiding confusion with the User, I will use Account to refer to what you call a Customer. At the end you used customer account in your comments.
You have a shipment that relates to three different entities. However, those entities are represented by the same data model in your database: the Account model.
A basic one-to-many relationship will suffice.
An account can have many shipments. And the shipment belongs to one account.
Now, how to add the "Type" of the relationship? We don't need a pivot table, we just add another one-to-many relationship.
An Account as biller may have many shipments, and the shipment belongs to one biller.
An Account as origin may have many shipments, and the shipment belongs to one origin.
An Account as destination may have many shipments, and the shipment belongs to one origin.
To explain, here is an example code:
We have three models: User, Account, and Shipment
Let's start with the schema:
Schema::create('accounts', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
Schema::create('shipments', function (Blueprint $table) {
$table->increments('id');
$table->string('from');
$table->string('to');
$table->unsignedInteger('biller_id');
$table->unsignedInteger('origin_id');
$table->unsignedInteger('destination_id');
$table->foreign('biller_id')
->references('id')->on('accounts');
$table->foreign('origin_id')
->references('id')->on('accounts');
$table->foreign('destination_id')
->references('id')->on('accounts');
$table->timestamps();
});
We have three columns referencing the id on the accounts table.
For the models and the relationships:
Account Model:
class Account extends Model
{
public function billerShipments()
{
return $this->hasMany(Shipment::class, 'biller_id');
}
public function originShipments()
{
return $this->hasMany(Shipment::class, 'origin_id');
}
public function destinationShipments()
{
return $this->hasMany(Shipment::class, 'destination_id');
}
public function users()
{
return $this->belongsToMany(User::class);
}
}
Shipment Model:
class Shipment extends Model
{
public function billerAccount()
{
return $this->belongsTo(Account::class, 'biller_id');
}
public function originAccount()
{
return $this->belongsTo(Account::class, 'origin_id');
}
public function destinationAccount()
{
return $this->belongsTo(Account::class, 'destination_id');
}
}
Example to create a shipment
$billerAccount = \App\Account::create(['name' => 'account b']);
$originAccount = \App\Account::create(['name' => 'account a']);
$destinationAccount = \App\Account::create(['name' => 'account c']);
$newShipment = \App\Shipment::create([
'from' => 'city 1',
'to' => 'city 2',
'biller_id' => $billerAccount->id,
'origin_id' => $originAccount->id,
'destination_id' => $destinationAccount->id,
]);
echo $billerAccount->billerShipments()->count(); // 1
echo $originAccount->originShipments()->count(); // 1
echo $destinationAccount->destinationShipments()->count(); // 1
echo $newShipment->billerAccount->name === $billerAccount->name; // 1
echo $newShipment->originAccount->name === $originAccount->name; // 1
echo $newShipment->destinationAccount->name === $destinationAccount->name; // 1
For the account-user relationships, it can be many-to-many or one-to-many depending on your requirements.

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

Many to Many Relationship with 2 FK referencing from same PK Laravel 8

Hello I am working on a Laravel project, that i have to assign for one Mentorship «Mentoria», one Mentor «Mentor» and one student «Mentorando». The data of the student and the mentor, came from the Users table (i assigned them roles, using Spatie) , and the other table is called «Mentoria» Since there exists a many to many relation i created the pivot table that is called «utilizador_mentoria» and has ID_Mentor, ID_Mentorando (both are FKs coming from the users table),and ID_mentoria (coming from Mentoria table). I defined both models as this:
User Model:
protected $casts = [
'email_verified_at' => 'datetime',
];
public function interesses(){
return $this->belongsToMany(AreaInteresse::class, 'utilizador_interesse', 'id_utilizador', 'id_interesse');
}
public function mentorias(){
return $this->belongsToMany(Mentoria::class, 'utilizador_mentoria', 'id_mentoria', 'id_mentorando', 'id_mentor');
}
ps: I have interesses function with other model, that is working properly. my problem is with the «mentorias»
Mentoria Model:
public function users(){
return $this->belongsToMany(User::class, 'utilizador_mentoria','id_mentor','id_mentorando','id_mentoria');
}
With this, i am trying to get the data from all Mentorias, and the data of the Mentor that is assigned to that that Mentoria, however when i am doing this code on the controller, the data coming from the user appears empty, despite i have the DB filled with data. I tried a echo for testing, and it only shows the data of the Mentoria, and where it should appear the data of the Mentor assigned to that MEntoria, it is empty
the code from the controller:
public function mentorias(){
$mentorias = Mentoria::with('users')->get();
echo $mentorias;
return view('admin/mentorias/admin_mentorias', ['mentorias' => $mentorias]);
}
the output of the echo
[{"id":2,"titulo":"teste","titulo_en":"test","descricao":"fe","descricao_en":"ewfwe","created_at":"2021-12-28T01:32:10.000000Z","updated_at":"2021-12-28T01:32:10.000000Z","users":[]}]
Since as i already said, i already used data from 2 tables with Many to Many relation, however with only 1 FK per PK, and it is working properly, i have no idea why it is not working this way . I already checked for similar questions, however with no luck
Edit:
For testing purposes, i removed the column of one of the two FK that reference from the same PK, and i managed to work, however with this aditional FK i am not managing to make it work . I believe that the problem is with the relation, in the models but i have no idea how to make it work
I rearranged the funcitons in the models as they are now
User Model:
public function mentorias(){
return $this->belongsToMany(Mentoria::class, 'utilizador_mentoria', 'id_mentor', 'id_mentorando', 'id_mentoria');
}
Mentoria Model:
public function users(){
return $this->belongsToMany(User::class, 'utilizador_mentoria','id_mentoria','id_mentorando','id_mentor');
i also tried to took out,example «id_entorando» from the main () and put it after with the «withPivot» method, but it still didn't worked
I don't know if I properly understood the problem but your relationship is not a single "many to many", but two "one to many". In the end I'll show why it's convenient to consider them as two separated relationships.
First: If a user can have multiple mentorships but a mentorship can have only 1 mentor (which I suppose is what's happening), then you should use the "hasMany/belongsTo" pair:
User Model:
public function mentorias(){
return $this->hasMany(Mentoria::class);
}
Mentoria Model:
public function user(){
return $this->belongsTo(User::class);
}
Second: Complete the scenario with the other relationship bewteen the mentorship and the students:
User Model:
public function mentorias(){
return $this->hasMany(Mentoria::class);
}
public function mentoria(){
return $this->belongsTo(Mentoria::class);
}
Mentoria Model:
public function user(){
return $this->belongsTo(User::class);
}
public function mentorandos(){
return $this->hasMany(User::class);
}
There should be a user_id column in the mentorias table that represents the Teacher which the mentorships belong to:
Schema::table('mentorias', function (Blueprint $table) {
$table->unsignedInteger('user_id')->nullable();
});
And there should also be a mentor_id column in the users table that represents the mentorship which the students belong to:
Schema::table('users', function (Blueprint $table) {
$table->unsignedInteger('mentoria_id')->nullable();
});
Third: This double representation of the User Model could lead to confusion, since two different objects (teacher and student) are using the same model but are not meant to have both relationships simultaneously: A teacher shouldn't be mentored and a student shouldn't lead mentorships.
In order to roperly manage Teachers and Students and prevent confusion, you could create additional models that inherit the User Model and define the relationships for them (instead of the User Model), so you can limit the fields and the relationships for each one of them.
User Model:
// No relationships
Teacher Model:
// Extending from User allows you to have all the User Model functionality
class Teacher extends User
{
public function mentorias(){
return $this->hasMany(Mentoria::class)->with('students');
}
}
Student Model:
// Extending from User allows you to have all the User Model functionality
class Student extends User
{
public function mentoria(){
return $this->belongsTo(Mentoria::class)->with('teacher');
}
}
Mentoria Model:
public function teacher(){
return $this->belongsTo(Teacher::class);
}
public function students(){
return $this->hasMany(Student::class);
}
In the end, you'll have all the info you need from the mentorship when you call objects like this:
Teacher::with('mentorias')->get();
// This will show the Teacher's mentorias and the students in each one
Student::with('mentoria')->get();
// This will show the Student's mentoria and its teacher
Mentoria::with(['teacher', 'students'])->get();
// This will show the teacher and the students for each mentoria

Yii2. Models related

I have 3 models: Items, Serials and SerialsCategories. When I show Item form (to create or update) I need to show the serials which belongs to a categoryId selected in a previous step. A serial can belong to more than one category.
Right now I have on my Item model:
public function getSerialsTypeByCategory() {
return (new SerialType)->getByCategory($this->itemCategoryId);
}
On my SerialType model:
public function getByCategory($itemCategoryId) {
return SerialTypeItemCategory::find()->select(['serialTypeId'])->where(['itemCategoryId' => $itemCategoryId])->all();
}
This is working, it does what I need but ... Is this the proper way? is there a better way?
it's not wrong what you are doing. but there is something more -
check this link:
Working with Relational Data
if you use ->hasOne and ->hasMany to define relations, your model gains some extra benefits, like joining with lazy or eager loading:
Item::findOne($id)->with(['categories'])->all();
with a relation, you can also use ->link and ->unlink, to add/delete related data without having to think about linked fields.
Further, it is easy to define relations via junction table:
class Order extends ActiveRecord
{
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->viaTable('order_item', ['order_id' => 'id']);
}
}

Database relationship name & best practice for it, in my laravel app

I have a database schema in my laravel app. In that schema, there are three models - Boss, Employee & Review.
A Boss can have many employees. An Employee can have many bosses(we can consider bosses from previous jobs). An Employee can Review his/her Boss. Boss can't Review anyone, so only Employee can review.
Thus, there are these relationships -
Employee & Boss has many-to-many relationship
Employee & Review has one-to-many relationship ( one Employee can Review multiple Bosses, thus having multiple reviews from his/her side ).
As you can see there is no direct relationship between Boss & Review. But, the query is -
What are the reviews for Boss 'x'?
To answer this query in laravel, I first thought that it had 'hasManyThrough' relationship. But, 'hasManyThrough' works only when Boss & Employee have 'one-to-many' relationship. That is, when one Boss can have multiple Employee but not vice-versa.
So, my question is - Is there a relationship which is applicable in this scenario(like polymorphic etc.)? If yes, how to use it in laravel?
No need for that extra table with reviews.
Here's all you need - first the tables:
bosses: id, ...
employees: id, ...
boss_employee: id, boss_id, employee_id, review (nullable), ...
Then the models:
// Boss model
public function employees()
{
return $this->belongsToMany('Employee')->withPivot('review');
}
public function reviews()
{
return $this->hasMany('Review');
}
// Employee model
public function bosses()
{
return $this->belongsToMany('Boss')->withPivot('review');
}
public function reviews()
{
return $this->hasMany('Review');
}
// Review model
protected $table = 'boss_employee';
public function boss() // or eg. reviewed
{
return $this->belongsTo('Boss');
}
public function employee() // or eg. reviewer
{
return $this->belongsTo('Employee');
}
Now, with this setup you can do this:
// get boss and his review
$boss = $employee->bosses->first();
$review = $boss->pivot->review;
// get review for given boss of an employee
$review = $employee->reviews()->where('boss_id', $bossId)->first();
// get bosses with reviews
$bosses = Boss::whereHas('reviews', function ($q) {
$q->whereNotNull('review');
})->get();
// and so on and so forth
You can enhance your Review model by adding global scope so it returns only the rows from boss_employee table having not null review field. This would be pretty much the same as SoftDeletingScope, but the conditions need to be swapped - by default whereNotNull, instead of whereNull in the SoftDeletingScope.

Querying with relationship not working

I am trying to grab an InvoiceDetails record and the matching Product record via the product foreign key.
This isn't working:
$r = InvoiceDetail::with('products')->find(52184)->toArray();
The 2 database calls are
SELECT * FROM `invoice_details` WHERE `id` = '52184' LIMIT 1
SELECT * FROM `products` WHERE `products`.`id` in ('0')
Where am I going wrong?
Table Structure of invoice details:
Schema::create('invoice_details', function (Blueprint $table) {
$table->increments('id');
$table->integer('invoice_id')->unsigned();
$table->integer('product_id')->unsigned();
$table->integer('quantity');
$table->foreign('product_id')->references('id')->on('products')->onDelete('restrict')->onUpdate('cascade');
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade');
});
Table structure for Products:
Schema::create('products', function(Blueprint $table)
{
$table->increments('id');
$table->string('name');
});
Products Model:
class Product extends \Eloquent
{
public function products()
{
return $this->hasMany('InvoiceDetail');
}
}
Invoice Details Model:
class InvoiceDetail extends \Eloquent
{
public function details()
{
return $this->belongsTo('Invoice');
}
public function products()
{
return $this->belongsTo('Product');
}
}
Your relationships are weird. (Okay, that wasn't really any longer.)
Assuming that an Invoice can belong to many Products (with specific details about each such as quantity), and that a Product can belong to many Invoices, you have a classic pivot table scenario. In which case, you're doing extra work and making life more difficult for yourself than it has to be.
If that's the case, there are a few steps you can take to reduce your code and make life easier:
Remove the InvoiceDetails model. Laravel can handle pivot tables on its own pretty well. So unless you have something really custom that you need the pivot table model to handle, you don't need it.
Update your Product model. You have a products() method in the Product model. That doesn't really make any sense. Don't products belong to invoices? Let's fix that.
class Product extends Eloquent
{
public function invoices()
{
return $this->belongsToMany('Invoice', 'invoice_details', 'product_id', 'invoice_id');
}
}
The additional parameters indicate the pivot table name, the column name for the Product model identifier, and the column name for the Invoice model identifier, respectively.
Update your Invoice model. You didn't paste it here, but I'll assume it has a relationship for invoice details. If not, well, oops! Because an invoice can belong to many products, essentially the inverse of the products relationship we just defined, it's defined it pretty much the exact same way.
class Invoice extends Eloquent
{
public function products()
{
return $this->belongsToMany('Product', 'invoice_details', 'invoice_id', 'product_id');
}
}
You now have a many-to-many relationship between Products and Invoices, that is retrieved using intuitive relationship methods! Huzzah.
Hey wait, where's my quantity?
You'll have to figure that one out on your own. :)
Got the answer (there is 2 hours of my life I'm never getting back) I had to manually add the fk and pk. So in my InvoiceDetails model it should have looked like this
public function products()
{
return $this->belongsTo('Product', 'product_id', 'id');
}

Categories