Laravel Eloquent eager loading multiple relationships to same model generates multiple queries - mysql

I am currently working on a project where translations are a core feature. Implementing this with Eloquent has been a great experience so far, but the problem unfolds when a model has multiple relations to one model.
Say you have a model called Post:
<?php
namespace App;
// posts (
// id
// title
// content
// )
class Post extends \Illuminate\Database\Eloquent\Model {
public function title() {
// posts.title -> translations.reference
return $this->hasMany('App\Translation', 'reference', 'title');
}
public function content() {
// posts.content -> translations.reference
return $this->hasMany('App\Translation', 'reference', 'content');
}
}
And a Translation model:
<?php
namespace App;
// translations (
// id
// reference
// language
// value
// )
class Translation extends \Illuminate\Database\Eloquent\Model {
public function language() {
return $this->hasOne('App\Language', 'id', 'language');
}
}
And finally a Language model:
<?php
namespace App;
// languages (
// id
// code
// name
// )
class Language extends \Illuminate\Database\Eloquent\Model { }
Then the following will successfully generate the expected output:
Post::with([
'title.language',
'content.language'
])->get();
[
{
"id":1,
"title":[
{
"id":68,
"reference":"5efd07083a002",
"language":{
"id":1,
"code":"en",
"name":"English"
},
"value":"Title in english"
}
],
"content":[
{
"id":70,
"reference":"5efd0708f1c82",
"language":{
"id":1,
"code":"en",
"name":"English"
},
"value":"Lorem in English"
}
]
}
]
THE PROBLEM
Is that Eloquent sent duplicate (or very similar) queries to the database, making me wonder if there is a better solution:
[
{
"query":"select * from `translations` where `translations`.`reference` in (?)",
"bindings":[
"5efd07083a002"
],
"time":186.14
},
{
"query":"select * from `languages` where `languages`.`id` in (?, ?)",
"bindings":[
1,
2
],
"time":170.69
},
{
"query":"select * from `translations` where `translations`.`reference` in (?)",
"bindings":[
"5efd0708f1c82"
],
"time":171.93
},
{
"query":"select * from `languages` where `languages`.`id` in (?, ?)",
"bindings":[
1,
2
],
"time":142.38
}
]
After doing some research I could not find any answers to this problem except "it's not possible" with no explanation.
What is the best solution for this problem? Is there something that I am missing or is this really impossible?
Thankful for any answers!

Related

Laravel: Parse JSON object with array of "sub-objects" to model instance

In my (Laravel) application receive a JSON which looks like:
{
"name": "order 1",
"customer": "cus123",
"orderItems": [
{
"amount": 1,
"name": "cola",
"price": "2.10"
},
{
"amount": 3,
"name": "fanta",
"price": "2.00"
},
]
}
I have create 2 models in Laravel, one Order and one OrderItem. I want to parse the received JSON to one Order instance $order.
I can get this done so by doing this in my OrderController:
class OrderController extends Controller
{
public function store(Request $request) {
$order = new Order();
$order->forceFill($request->toArray());
}
}
It's possible to access properties now like $order->name and $order->customer in the store function of the controller. When i access the $order->orderItems i receive an array with "orderItemsbut as array, not as instance ofOrderItem`.
I want that $order->orderItems returns an array of OrderItem instances. I tried the following in Order but this does not work as 'orderItems' is not a OrderItem::class but is an array with multiple "OrderItems".
protected $casts = [
'orderItems' => OrderItem::class,
];
How can i achieve that $order->orderItems returns an array of OrderItem instances?
Thanks for any help in advance!
Try to add the following to your controller
validation
manual storing your Order
manual storing each of your order items
.
class OrderController extends Controller
{
public function store(Request $request)
{
$your_rules = [
'name' => 'required|string',
'customer' => 'required|string', // related to customer id ?
'orderItems' => 'array',
'orderItems.*.name' => 'string',
'orderItems.*.amount' => 'integer|gte:1',
'orderItems.*.price' => 'numeric|between:0,99.99',
];
$validated = $request->validate($your_rules);
$order = Order::create([
'name' => $validated['name'],
'customer' => $validated['customer'], // is this customer id or name ?
]);
// I assume you already declare relationship to OrderItem inside your Order model
foreach ($validated['orderItems'] as $orderItem) {
// this array only is optional
$orderItem = Arr::only($orderItem, ['name', 'amount', 'price');
$order->orderItems()->save($orderItem);
}
// reload saved order items
$order->load('orderItems');
dd($order);
}
}
You can also create multiple children in single command.
$order->orderItems()->saveMany([
new OrderItem(['name' => '...', ... ]),
new OrderItem(['name' => '...', ... ]),
]);
Read here for more info https://laravel.com/docs/9.x/eloquent-relationships#the-save-method
You can move this into your model as extra custom method.
For example:
public function saveOrderItems(array $orderItems): void
{
$this->orderItems()->saveMany($orderItems);
}
And you call it as $order->saveOrderItems($orderItems);
P.S.
Dont forget to declare relationship in Order model.
public function orderItems()
{
return $this->hasMany(OrderItem::class);
}
I think you are confuse with the whole Model relationship. Checkout the documentation here, you need to define proper relationship and foreign key between your Order and OrderItem model.
Then your model should be like this;
//Order.php
class Order extends Model {
protected $fillable = [
'name',
'customer',
];
public function items() {
return $this->hasMany(OrderItem::class);
}
}
//OrderItem.php
class OrderItem extends Model {
protected $fillable = [
'amount',
'name',
'price'
];
public function order() {
return $this->belongsTo(Order::class);
}
}
Then your store method
public function store( Request $request ) {
$request->validate([
'name' => 'required',
'customer' => 'required|exists:customers_table,id',
'orderItems' => 'required|array'
]);
$order = Order::create( $request->except('orderItems') );
$items = $order->items()->createMany( $request->input('orderItems') );
}

How can I format the array on a specific format in my laravel controller function?

I'm currently creating a laravel vue spa, and just wondering on how can I get designation names with these designation id's with the same structure. This is the json of designation id's:
[
[
1,
5
],
[
1,
3
]
]
This is my getDesignations function in EmployeesController.php:
public function getDesignations($id) {
$employee_designation_ids = Employees::find($id)->pluck('designation_id')->toArray();
$designation_name = [];
foreach ($employee_designation_ids as $employee_designation_id) {
$designation = Designations::where('id', '=', $employee_designation_id);
//$designation_name[] = $designation;
}
return $employee_designation_ids;
}
If you want specifically that format, you can do it like this (with a lot of guesses in my part since you did not share your Tables structures)
public function getDesignations($id) {
$employee_designation_ids = Employees::find($id)->designation_id;
return [[$id, $employee_designation_ids]];
}
But why are you returning a double table for a single row result.
Thanks for all your help! I've managed to fix it with this method in my controller:
public function getDesignations(Request $request, $id) {
$employee_designation_ids = Employees::where('id', $id)->pluck('designation_id');
return Designations::whereIn('id', $employee_designation_ids[0])->pluck('name');
}

Eloquent many to many relationship without middle table

I'm new to Eloquent and struggling mightily with the following.
In my database (mysql 5.7), there are 2 tables and structured as below.
article:
{
_id: 1,
title: "xxx",
content: "xxx",
tag_ids: [
4,
5
]
}
tag:
{
_id: 4,
tag: "tag1"
}
In ArticleModel, has a cast
protected $casts = [
"tags" => "array"
];
It is possible to make a many to many relationship without a middle table?
Any help would be greatly appreciated!
I created a package with JSON relationships: https://github.com/staudenmeir/eloquent-json-relations
You can create a many-to-many relationship like this:
class Article extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
protected $casts = [
'tag_ids' => 'array'
];
public function tags()
{
return $this->belongsToJson(Tag::class, 'tag_ids');
}
}
class Tag extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
public function articles()
{
return $this->hasManyJson(Article::class, 'tag_ids');
}
}

Include ressource link in Sequelize result

I'm building a rest api that uses Sequelize to interact with the database. A query looks like this:
function read_category(req, res) {
Category.findById(req.params.categoryId, {rejectOnEmpty: true}).then(category => {
res.json(category);
}).catch(Sequelize.EmptyResultError, function () {
res.status(404).json({message: 'No category found'});
}
).catch(function (err) {
res.send(err);
}
);
}
Now I want the category object that is returned from Sequelize and then returned to the user to include the linkto the ressource. I could do:
category.dataValues.link = config.base_url + 'categories/' + category.dataValues.id;
Which would result in:
{
"id": 1,
"name": "TestCategory 1",
"position": 1,
"createdAt": "2018-08-19T11:42:09.000Z",
"updatedAt": "2018-08-19T11:42:09.000Z",
"link": "http://localhost:3000/categories/1"
}
Since I have more routes than this one I'm wondering if there's a dynamic way to add the link property to every category. I don't want to save it in the database because the base-url might differ.
Thanks!
Better way to do it is , create a getter method :
const Category = sequelize.define( 'category' , {
....
your_fields
....
},
{
getterMethods:{
link() {
return config.base_url + 'categories/' + this.id;
}
}
});
module.exports = Category;
Then
Category.findAll(...).then(categories => {
// Now there is no need to append data manually , it will added each time when you query
console.log(categories); // <-- Check the output
})

Laravel many-to-many relationship OrderBy

I have 2 models in a Many to Many relationship. Let's say User and Role.
I want to sort my users based on the ASC/DESC of a field in Role.
My User & Role classes:
class User extends Model
{
public function roles()
{
return $this->belongsToMany('App\Role','role_user');
}
class Role extends Model
{
public function users()
{
return $this->belongsToMany('App\User','role_user');
}
I can sort the roles in each user but I cant sort the users
$query = User::with(array('roles'=> function ($query)
{
$query->select('role_name')->orderBy('role_name','asc');
}))->get();
I have also tried:
$query = User::with(roles)->orderBy('role_name','asc')->get();
But the error says column role_name does not exist.
Ideal result should look like this:
[
{
user_id:6
roles: [
"Admin",
"Baby"
]
},
{
user_id:2
roles: [
"Baby"
]
},
{
user_id:11
roles: [
"Baby",
"Cowboy"
]
}
]
I'd appreciate any help.
As user can have many roles i think you can concatenate role names and then order users by concatenated string. Try this:
User::selectRaw('group_concat(roles.name order by roles.name asc) as role_names, users.id')->
join('role_user','users.id','=','role_user.user_id')->
join('roles', 'roles.id','=','role_user.role_id')->
groupBy('user_id')->
orderBy('role_names','desc')->
get()
Please try the below modification in roles() function in User Class and then fetch it with User.
class User extends Model
{
public function roles()
{
return $this->belongsToMany('App\Role','role_user')
->selectRaw('id,'role_name')
->orderby('role_name');
}
}
$query = User::with(roles)->get();
Hope this will be useful for you.