Inverse of belongsToMany - laravel

I got two Models:
Order
Invoice
Each Order can have many Invoices - and an Invoice can belong to many Orders.
So I can search for an Order and check: "Hey, which Invoices have been created for this Order?"
The other way round each Invoice can belong to multiple Orders, because maybe a customer ordered two products on the same day and so it would be great he'd only get one Invoice, which includes both orders.
So this is how I did this:
Invoice
public function orders()
{
return $this->belongsToMany(Order::class);
}
Order
public function invoices()
{
return $this->belongsToMany(Invoice::class, 'invoice_order');
}
This does work - but it does not seem right to change the table to the intermediate table invoice_order here. Do you have any thoughts on this? :-)
Thanks in advance for your thoughts :-)

Seperating the relation into a seperate pivot table is the commonly used method in laravel (and in most other frameworks) for many to many relationships.
It's easy to maintain, easy to get related models using many to many relationship, and if someone else needs to work on it in the future, they'll probably have used it in the past as well so wouldn't end up burning their heads.
The other method you could use is to create a json column on one of the tables (you can create on both tables as well if you want, but that's just extra overhead). Then you can store the ids of the related models in this json column. You can then join the tables using the json related commands provided by your database. Eloquent does not support relationships on json, but you can use this package staudenmeir/eloquent-json-relations to build relationships on json fields.
So overall, I'd suggest keeping a pivot table like the standard way, but if that just won't do, then you can try the json column method

Related

Laravel models, database and pivot tables question

Hello I am working with Laravel,
I have to create two simple models, let's say Stores and Books.
Stores can have one or multiple Books and Books can belong to many Stores.
Of course I will use a many to many relationship, with a pivot table.
Books the can have different prices depending the store.
I think a separate table can only complicate things, in my mind the pivot table associating books and stores should have a price column, but pivot tables only contains store_id and book_id.
Should I create a book_prices and associate it with books and to stores? What is the best approach?
You are free and able to set other attributes on your pivot table. You can read more about it in the docs.
https://laravel.com/docs/9.x/eloquent-relationships#retrieving-intermediate-table-columns
You have to define the relationship accordingly, the following should clarify how this works. In this example you use the many-to-many relationship and add the price column to every retrieved pivot model.
public function books()
{
return $this->belongsToMany(Book::class)
->withPivot('price')
}
For example, you are able to access the pivot column in a loop like this
foreach ($shop->books as $book)
{
echo $book->pivot->price;
}
You can define additional columns for your pivot table in the migration for the pivot table, and then when defining the relationship use withPivot to define the additional columns so they come through in the model:
return $this->belongsToMany(Book::class)->withPivot('price');
(Adapted from the Laravel documentation, see https://laravel.com/docs/9.x/eloquent-relationships#retrieving-intermediate-table-columns)
Depends on the complexity of your case, but yes, you have two options for it. Let's say that the pivot table is called as book_store:
Directly adds price column to book_store. This is obviously the simpler option. The drawbacks are:
The history of the price changes isn't logged. You'll have to create another table for logging if you want to keep this history information.
Changes made to price will directly change the price of the related book_store record. Meaning that a price is being updated "live" e.g users cannot update the price now but "publish" it some time later just like this example in the doc.
Create a new, different table to store the price. This may seems relatively more complex, but it may also be more future-proof.
Basically, you get 2 things that you miss in the first option above.
Don't think too much about book_store being a pivot table. One way to see it is like this: book_store IS a pivot table from books and stores tables viewpoints, but it's also just a normal SQL table which could relate to any other tables using any kind of relationships.
If you want to implement this, make sure to create a primary-key in the book_store table.
Alast, it all depends on what you need. Feel free to ask if you need more insight about this. I hope this helps.

LatestOfMany() of BelongsToMany() relationship

I've been using latestOfmany() for my hasMany() relation to define them as hasOne() for quite a while now. Lately I've been in need of the similar application but for belongsToMany() relationships. Laravel doesn't have this feature unfortunately.
My codebase as follows:
Document
id
upload_date
identifier_code
Person
id
name
DocumentPerson (pivot)
id
person_id
person_id
token
My objective is: define relationship for fetching the first document (according to upload_date) of Person. As you can see it's a many-to-many relationship.
What I have tried so far:
public function firstDocument()
{
return $this->hasOne(DocumentPerson::class)->oldestOfMany('document.upload_date');
//this was my safe bet but oldestOfMany() and ofMany() doesn't allow aggregating on relationship column.
}
public function firstDocument()
{
return $this->belongToMany(Document::class)->oldestOfMany('upload_date')
}
public function firstDocument()
{
return $this->belongToMany(Document::class)->oldest()->limit(1);
}
public function firstDocument()
{
return $this->hasOneThrough(Document::class, DocumentPerson::class, 'id', 'document_id', 'id', 'person_id')->latestOfMany('upload_date');
}
At this point I'm almost positive current relationship base doesn't support something like this, so I'm elaborating alternative methods to solve this. My two choices:
Add a column called first_document_id on Person table, go through that with belongsTo() simple and fast performance-wise. But downside is I'll have to implement so many event-listeners to make sure it is always consistent with actual relationships. What if Document's upload_date is updates etc. (basically database inconsistency)
Add a order column on pivot (document_person) table, which will hold order of related Documents by upload_date. This way I can do hasOne(DocumentPerson::class)->oldestOfMany('order');//or just ofMany() and be done with it. This one also poses the risk of database inconsistency.
It's fair to say I'm at a crossroads here. Any idea and suggestion is welcomed and appreciated. Thank you. Please read the restrictions to prevent suggesting things that are not feasible for my situation.
Restrictions:
(Please)
It should strictly be a relationship. I'll be using it on various places, it definitely has to be relationship so I can eager load and query it. My next objective involves querying by this relationship so it is imperative.
Don't suggest accessors, it won't do well with my case.
Don't suggest collection methods, it needs to be done in query.
Don't suggest ->limit() or ->take() or ->first(), those are prone to cause inconsistent results with eager loading.
Update 1
Q: Why first document of a person has to be a relationship ?
A: Because further down the line I'll be querying it in various different instances. Example queries where it'll be utilized:
Get all the users whose first document (according to upload_date) upload_date between 2022-01-01 and 2022-06-08. (along with 10 other scopes and filters)
Get all the users whose first document (according to upload_date) identifier_code starts with "Lorem" and id bigger than 100.
These are just to name a few, there are many cases where I really gotta query it in various fashions. This is the reason that I desperately need it to be a relationship, so I can query it with ease using Person::whereHas('firstDocument',function($subQuery){ return $subQuery->someScope1()->anotherScope2()->where(...); }
If I only needed to display it, yeah sure eager loading with closure would do well, or even collection methods, or accessors would suffice. But since ability to query it is the need, relationship is of the essence. Keep in mind Person table has around 500k record, hence the need for querying it on the database layer.
Alright here's the solution I've elected to go with (among my choices, explained in the question). I implemented the "adding order column on pivot" table. Because it scales better and is rather flexible compared to other options. It allows for querying the last document, first document, third document etc. Whilst it doesn't even require any aggregate functions (Max, min like ->latestOfMany() applies) which is a performance boost. Given these constraints this solution was the way to go. Here's how I applied it in case someone else is thinking about something similar.
Currently the only noticeable downside to this approach is inability to access any additional pivot data.
Added new column for order:
//migration
$table->unsignedTinyInteger('document_upload_date_order')->nullable()->after('token');
$table->index('document_upload_date_order');//for performance
Person.php (Model)
//... other stuff
public function personalDocuments()
{//my old relationship, which I'll still keep for display/index purposes.
return $this->belongsToMany(Document::class)->withPivot('token')->where('type_slug','personal');
}
//NEW RELATIONSHIP
public function firstDocument()
{//Eloquent relationship, allows for querying and eager loading
return $this->hasOneThrough(
Document::class,
DocumentPerson::class,//pivot class for the pivot table
'person_id',
'id',
'id',
'document_id')
->where('document_upload_date_order',1);//magic here
SomeService.php
public function determineDocumentUploadDateOrders(Person $person){
$sortLogic=[
['upload_date', 'asc'],
['created_at', 'asc'],
];
$documentsOrdered=$person->documents->sortBy($sortLogic)->values();//values() is for re-indexing the array keys
foreach ($documentsOrdered as $index=>$document){
//updating through pivot tables ORM model
DocumentPerson::where('id',$document->pivot->id)->update([
'document_upload_date_order'=>$index+1,
'document_id'=>$document->id,
'person_id'=>$document->pivot->person_id,
]);
}
}
I hooked determineDocumentUploadDateOrders() into various event-listeners and model events so whenever association/disassociation occurs, or upload_date of a document changes I simply call determineDocumentUploadDateOrders() with corresponding Person and this way it is always kept in sync with actual.
Implemented it fully and it is providing consistent results with great performance. Of course it brought a bit of an overhead with keeping it in sync. But nonetheless, It did the job whilst meeting the requirements. Honestly I found this approach far more reliable than some in-official eloquent relationships and similar alternatives.
I have encountered a similar situation years back.
the best workaround on a situation like this is to use #staudenmeir package eager limit
Load the trait use \Staudenmeir\EloquentEagerLimit\HasEagerLimit; on both model (parent and related model)
then try the code below
public function firstDocument() {
return $this->documents()->latest()->limit(1);
}
public function documents() {
return $this->belongsToMany(Document::class);
}
just to add, Eager loading with limit does not work with built laravel eloquent, you would have to build your own raw queries to achieve it which can turn into a nightmare. that eager limit package from staudenmeir should have been merge with laravel source code 😆

How to order by the number of relationships

I am trying to create a "tag cloud" that lists the tag with the most relationships on a many-to-many pivot table schema.
I'm afraid I don't even know what to search in order to begin. I would appreciate any links or code examples, but prefer references so I can learn this once and for all.
I have a table named companies and a table named categories. There is a pivot table named category_company and the proper relationships are setup and working great.
Simply using withCount() should get you there.
$companies = Company::withCount('categories');
Now you are able to access the count like so.
foreach ($companies as $company) {
$company->categories_count; // gives out the count for this companies categories.
}
See the docs.

how do I make relationships for 4 tables including pivot table

I have many to many relationships. Imagine I have 3 tables. Something like this :
users.
roles.
role_user.
(This example is also provided in laravel's docs).
Now I'm doing this : $user->roles() which returns roles with Pivot attributes . but what I actually want to do is move forward and also get the appropriate data from the 4th table. something like this $user->roles()->types(); and the difficult thing is that this types() belongs to pivot table.
Do you know how to do this kind of thing ? where Do I write types() function?
Assuming your "Roles" model has the relationship set you may try
$user->roles()->with("types")
Source docs: https://laravel.com/docs/5.7/eloquent-relationships#eager-loading

Relate two entities via a third one

I am trying to figure it out how to relate two entities using another one they both have in common. My model is the following:
A Customer has a list of Codes
A Promotion has a list of Codes
A Code has a list of Customers and a list of Promotions
I would like to add to the Promotion Entity a list of Customers. This list would include all customers who at least have the same codes as in the promotion (can have more). Ideally I would like to model this a field but I am not sure if this is possible. I have found no information.
Can it be done?
Thanks,
You can define
Customer Entity.
Code entity which has #ManyToMany relationship with Customer. ( This is required if customer can have code even when there is no promotion.)
Promotion Entity which has #ManyToMany relationship with Customer
and #ManyToMany with Code.
At the end I ended up doing the following
public Set<Customers> getCustomers(){
Set<Customers> customers = null;
for (Code c : codes)
if(customers == null)
customers = c.getCustomers();
else
customers.retainAll(c.getCustomers());
return customers;
}
I do a Eager retrieve for the Codes, and in the codes an Eager retrieve for the Customers. So far is performing well, but I would prefer this to be all calculated in the DBMS and not on the controller side. Still have not find a nice way to to it.

Resources