Laravel Eloquent model architecture and pivot table with composite keys - laravel

I have a Laravel specific problem, maybe caused by creating database model without previous Eloquent knowledge, and now I am really struggling with correctly retrieving required data.
Situation:
There are 4 entity models: Connection, Order, Seat, AdditionalItem.
Connection has X seats. You can create an Order, for Y connections, and for X seats for every connection, if available. And for every connection seat you can have N Additional items.
So for example, I can create and order for one connection with 2 seats, and another connection with 3 seats. And for some seats I can order additional items.
What I did is this:
I created a pivot table called "order_connection_seats", with composite keys "order_id", "connection_id", "seat_id" and some other attributes. It made total sense to me, because without need to create autoincrement primary keys I can easily store what i need. What looks to me little dirty is the solution for additional items. I have another pivot table "order_connection_seat_additional_items" with keys "order_id", "connection_id", "seat_id" and "additional_item_id".
The problem is, that when I try to correctly select data to be able to iterate over it, the query gets quite complicated and even doesn't work correctly.
What I need to do is something like (without lazy loading):
$orders = Order::query()->with(['connections' => ...])->get();
foreach ($orders as $order) {
foreach ($order->connections as $connection) {
foreach ($connection->seats as $seat) {
foreach ($seat->additionalItems as $additionalItem) {
}
}
}
}
OR
$connections = Connection::query()->with(['orders' => ...])->get();
foreach ($connections as $connection) {
foreach ($connection->orders as $order) {
foreach ($order->connection->seats as $seat) {
foreach ($seat->additionalItems as $additionalItem) {
}
}
}
}
Is this achievable with Eloquent, or is the data model incorrect and should be made differently to ge the desired result?
Thank you in advance.

Related

Get data through pivot table in Laravel

I got 3 tables. Table 1 & 2 has their ids as foreign keys in third one(pivot).
Relations for first one is
$this->hasMany("App\Pivot","game_id");
, second is
$this->belongsToMany("App\Pivot","army_id");
and pivot has relationships with both of them i.e belongsTo.
My schema:
I tried accessing it in controller of first one like this:
$games= Game::with("armies")->get();
Result that i get is array of games where instead of individual army data , i get collection from pivot table.
I can loop through it and get it that way, is there more elegant way of doing it?
If you are using pivot table this is the way how to do it.
Games Model
public function armies()
{
return $this->belongsToMany(App\Armies::class, 'pivot_table', 'game_id', 'army_id');
}
Armies Model
public function armies()
{
return $this->belongsToMany(App\Games::class, 'pivot_table', 'army_id', 'game_id');
}
Access the relationship like this..
Controller
App\Games::first()->armies()->get();
or
App\Games::first()->armies
or
App\Games::find(1)->armies
If you're going to use an intermediate table like that I'd probably do something like this:
Games model
public function armies()
{
return $this->belongsToMany('App\Armies');
}
Armies model
public function games()
{
return $this->belongsToMany('App\Games');
}
I'd keep the table structures all the same but rename the "pivot" table to armies_games since this is what Laravel will look for by default. If you want to keep it named Pivots, you'll need to pass it in as the second argument in belongsToMany.
With this, you don't really need the Pivot model, you should just be able to do:
$armies = Game::first()->armies()->get();
or
$armies = Game::find(3)->armies()->orderBy('name')->get();
or
$game = Game::first();
foreach ($game->armies as $army) {
//
}
etc.

Laravel HasManyThrough CrossDatabaseRelation

strange question:
I have 3 Models
Order
with id as PK
Orderline
with id as PK and order_id as FK. brand and partnumber are two separatet colums
Article
with combined PK brand and partnumber
**which is on another database **
One Order hasMany Orderlines. Every Orderline hasOneArticle.
i had make a function within order:
public function articles()
{
$foreignKeys = [
'brand_id',
'article_number',
];
$localKeys = [
'brand',
'partnumber',
];
return $this->hasManyThrough('App\Models\Masterdata\Articles','App\Models\Oms\OrderLine',$foreignKeys,$localKeys,'id','id');
}
How can i retrieve all Attributes from articles through Order?
I tried something like this:
$order = Order::find($orderid)->articles();
dd($order);
//did not work
$order = Order::with('orderlines.articles')->where('id','=',$orderid)->get();
Do you have an idea for me?
You can configure more than one database in the database.php config, and specify the $connection name in the Model class.
For the most part, Eloquent doesn't do JOINs, it just does IN statements on the key values from the preceding query, and programmatically marries the results together after the fact. Partly to avoid the mess of keeping table aliases unique, and partly to offer this kind of support- that your relations don't need to live in the same database.
In other words, what you have there should work just fine. If there's a specific error getting thrown back though, please add it to your post.

Laravel / Eloquent: Is it possible to select all child model data without setting a parent?

I have various parent/child relationships, drilling down a few levels. What I want to know is if its possible to do something like this:
$student = Student::find(1);
$student->bursaries()->enrolments()->courses()->where('course','LIKE','%B%');
(With the end goal of selecting the course which is like '%B%'), or if I would have to instead use the DB Query builder with joins?
Models / Relationships
Student:
public function bursaries() {
return $this->hasMany('App\StudentBursary');
}
StudentBursary:
public function enrolments() {
return $this->hasMany('App\StudentBursaryEnrolment');
}
If what you want is to query all courses, from all enrollments, from all bursaries, from a students, then, unfortunately, you are one table too many from getting by with the Has Many Through relationship, because it supports only 3 tables.
Online, you'll find packages that you can import / or answers that you can follow to provide you more though of solutions, for example:
1) How to use Laravel's hasManyThrough across 4 tables
2) https://github.com/staudenmeir/eloquent-has-many-deep
Anyhow, bellow's something you can do to achieve that with Laravel alone:
// Eager loads bursaries, enrolments and courses, but, condition only courses.
$student = Student::with(['bursaries.enrolments.courses' => function($query) {
$query->where('course','LIKE','%B%');
}])->find(1);
$enrolments = collect();
foreach($student->bursaries as $bursary) {
$enrolments = $enrolments->merge($bursary->enrolments);
}
$courses = collect();
foreach ($enrolments as $enrolment) {
$courses = $courses->merge($enrolment->courses);
}
When you do $student->bursaries() instead of $student->bursaries, it returns a query builder instead of relationship map. So to go to enrolments() from bursaries() you need to do a bursaries()->get(). It should look like this.
$student->bursaries()->get()[0]->enrolments(), added the [0] because im using get(), you can use first() to avoid the [0]
$student->bursaries()->first()->enrolments()
But I'm not sure if it will suffice your requirement or not.

Querying Distant Relationships

I have three tables: users, accounts and hotels. Users and Accounts are connected with belongstoMany relation and Accounts and Hotels are connected the same way. Each User has Accounts and those Accounts have Hotels.
When I have Auth::user(), how I can return all hotels?
$accounts = Auth::user()->accounts()->get();
With the above statement, I can get all Accounts. How can I return all Hotels?
WHAT I TRIED?
public function index(Request $request)
{
$accounts = Auth::user()->accounts()->get();
$hotels = collect();
foreach ($accounts as $key => $a) {
$h = $a->hotels();
$hotels = $hotels->toBase()->merge($h);
}
dd($hotels);
return view('hotels.index',compact('hotels'));
}
but this code dont return me hotels collection which I can use in blade view files
Case 1
In the case you have a relationship as shown in the diagram below
What you are looking for is the hasManyThrough relationship.
From the Laravel documentation
The "has-many-through" relationship provides a convenient shortcut for accessing distant relations via an intermediate relation
In your case, on your User model, you can define the relationship
public function hotels()
{
return $this->hasManyThrough('App\Hotel', 'App\Account');
}
To then get your collection of hotels you can simply use
$hotels = Auth::user()->hotels;
You can also provide extra arguments to the hasManyThrough function to define the keys that are used on each table, great examples of this are given in the documentation linked above!
Case 2
If, instead, you have a relation as shown in the following diagram
It is a little more tricky (or, at least, less clean). The best solution I can think of, that uses the fewest queries is to use with.
$accounts = Auth::user()->accounts()->with('hotels')->get();
Will give you a collection of accounts, each with a hotels child. Now, all we have to do is get the hotels as a standalone collection, this is simple with some neat collection functions provided by Laravel.
$hotels = $accounts->flatMap(function($account) {
return $account->hotels;
})->unique(function ($hotel) {
return $hotel->id;
});
This will do the job of creating a collection of hotels. In my opinion, it would be cleaner and more efficient to simply make a new relationship as shown below.
And then to perform queries, using basic Eloquent methods.

Putting eloquent results in another table to doing where queries in Laravel 5.4

For some special reasons I used append attributes in my model and now when I want to do where queries on custom attributes, for example "category", I face an error with this meaning that eloquent could not found column with "category" name!
To solve this problem I guess if I put my query's result into a temp table, I could do what I want!
Have someone any Idea about that? If it's useful to me, How can I transfer my results to the temp table?
You won't be able to limit the database query using a Model accessor's dynamic field, since that field obviously doesn't exist in the database.
However, the Collection object has fairly robust filtering capabilities, so you could filter the Collection results using the dynamic fields after the database has been queried. This is not as performant as filtering out the results before they are retrieved from the database, but you may be a situation where the performance isn't that critical or the code cleanliness/maintenance cost outweighs the performance cost.
As an example, given the following Model:
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
}
The following query will not work because the category field doesn't actually exist in the table:
$childrenBooks = Book::where('category', 'child')->get(); // error: no category field
However, the following will work, because you're calling where() on the Collection of Models returned from the database, and the Models do have access to the dynamic field:
$childrenBooks = Book::get()->where('category', 'child');
The problem in this case is that, while it does work, it will get all the books from the database and create a Model instance for each one, and then you filter through that full Collection. The benefit, however, is that you don't have to duplicate the logic in your accessor method. This is where you need to weigh the pros and cons and determine if this is acceptable in your situation.
An intermediate option would be to create a Model scope method, so that your accessor logic is only duplicated in one place (if it can be duplicated for a query):
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
public function scopeCategory($query, $category)
{
if ($category == 'child') {
return $query->where('target_age', '<', 13);
}
if ($category == 'teen') {
return $query->where(function ($query) {
return $query
->where('target_age', '>=', 13)
->where('target_age', '<', 18);
});
}
return $query->where('target_age', '>=', 18);
}
}
Then you can use this query scope like so:
$childrenBooks = Book::category('child')->get();
The benefit here is that the logic applies to the actual query, so the records are limited before they are returned from database. The main problem is that now your "category" logic is duplicated, once in an accessor and once in a scope. Additionally, this only works if you can turn your accessor logic into something that can be handled by a database query.
You can create temporary tables using raw statements. This post goes fairly in depth over it:
https://laracasts.com/discuss/channels/laravel/how-to-implement-temporary-table

Resources