Optimizing Eloquent Relationship Retrieval - mysql

I have an interface which displays a list of communities on the platform. Communities have members and in turn members/profiles can befriend one another. On the listing page each community card needs to display the number of members (in the community) and the number of friends (friends of logged in profile) from those members.
Here's an illustration of how a community card looks like
I'm getting the communities with the members first:
$communities = $loggedInProfile->communities->load('members')->take(15);
And then iterating over the communities and then the members to find out which ones are friends with the logged in user.
foreach ($communities as $key => $community) {
$friends = [];
foreach ($community->members as $member) {
if ($loggedInProfile->isFriendWith($member)) {
array_push($friends, $member);
}
}
$community->members_who_are_friends = $friends;
}
My issue is that this is very taxing in terms of the number of queries when the associations get large. Is there a better way of retrieving these relationships without having to use nested for loops? I'm also indexing all data with Elasticsearch. Would a retrieval of this sort be better with Elasticsearch? Also would this be a good use case for hasThrough?
Update
The members relationship:
public function members()
{
return $this->belongsToMany('App\Profile', 'community_members', 'community_id', 'profile_id')->withTimestamps();
}
The isFriendWith relationship:
public function isFriendWith(Model $recipient)
{
return $this->findFriendship($recipient)->where('status', Status::ACCEPTED)->exists();
}
The check is done on a table called friendships. The status column (which can be either 0 or 1) is checked to see if friends or not.
The findFriendship check:
private function findFriendship(Model $recipient)
{
return Friendship::betweenModels($this, $recipient);
}
Database structure:
-Profiles migration
Schema::create('profiles', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
});
-Communities migration (the foreign key is the owner of the community)
Schema::create('communities', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('profile_id');
$table->foreign('profile_id')->references('id')->on('profiles');
$table->string('slug')->unique();
});
-Community_members migration
Schema::create('community_members', function (Blueprint $table) {
$table->primary(['profile_id', 'community_id']);
$table->unsignedInteger('profile_id');
$table->foreign('profile_id')->references('id')->on('profiles');
$table->unsignedInteger('community_id');
$table->foreign('community_id')->references('id')->on('communities');
$table->timestamps();
});
-Friendships migration
Schema::create('friendships'), function (Blueprint $table) {
$table->increments('id');
$table->morphs('sender');
$table->morphs('recipient');
$table->tinyInteger('status')->default(0);
$table->timestamps();
});

In your line:
$communities = $loggedInProfile->communities->load('members')->take(15);
load() is used to perform Lazy Eager loading, i.e. you load the members after the communities have been retrieved, resulting in a different query for every community. You could extract the whole data with a single query using with(). Also, take(15) is performed on the resulting collection and not on the query. Try this:
$communities = $loggedInProfile->communities()->with('members')->take(15)->get();

Related

A crazy database structure - suggestions

I'm working on a project where I need to create three Many-to-Many relationships between 4 models. Here is how it goes:
FAQ Categories can have many FAQ Subcategories and vice versa.
FAQ Groups can have many FAQ Subcategories and vice versa.
FAQs can have many FAQ groups and vice versa.
To all the database experts out there, how should I design this database schema in Laravel? Should I have three different pivot tables? Should I use polymorphic relationships?
I've used polymorphic relationships before, but I'm struggling with implementing it in this scenario.
I would do something like this:
FAQ Categories Table
Schema::create('faq_categories', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
});
Schema::table('faq_categories', function (Blueprint $table) {
$table->unsignedInteger('parent_id')->nullable();
$table->foreign('parent_id')->references('id')->on('faq_categories')->onDelete('cascade');
});
FAQ Groups Table
Schema::create('faq_groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
});
FAQs Table
Schema::create('faqs', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
});
As you can see I wouldn't create a FAQ Sub Categories table, because it's cleaner to have a category table referencing itself to a parent category (also important to make that foreign key nullable to be able to create a top level category).
Now to setup the relationships between the tables, we can do this:
FAQ Categories - FAQ Groups (Many to Many)
Schema::create('faq_category_faq_group', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('faq_category_id');
$table->foreign('faq_category_id')->refrences('id')->on('faq_categories')->onDelete('cascade');
$table->unsignedInteger('faq_group_id');
$table->foreign('faq_group_id')->refrences('id')->on('faq_groups')->onDelete('cascade');
});
FAQs - FAQ Groups (Many to Many)
Schema::create('faq_faq_group', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('faq_id');
$table->foreign('faq_id')->refrences('id')->on('faqs')->onDelete('cascade');
$table->unsignedInteger('faq_group_id');
$table->foreign('faq_group_id')->refrences('id')->on('faq_groups')->onDelete('cascade');
});
Should I use polymorphic relationships?
I don't think polymorphic relationship's would make any sense in this scenario. I would stick with standard many to many.
In your model classes you should setup all the relationships like referenced in the docs.
You can do this:
FaqCategory Model
class FaqCategory extends Model
{
/**
* Get the category's parent category.
*/
public function parent()
{
return $this->belongsTo('App\FaqCategory');
}
/**
* Get the category's sub categories.
*/
public function sub_categories()
{
return $this->hasMany('App\FaqCategory', 'parent_id');
}
/**
* Get the category's faq groups.
*/
public function faq_groups()
{
return $this->belongsToMany('App\FaqGroup');
}
}
FaqGroup Model
class FaqGroup extends Model
{
/**
* Get the group's faq categories.
*/
public function faq_categories()
{
return $this->belongsToMany('App\FaqCategory');
}
/**
* Get the group's faqs.
*/
public function faqs()
{
return $this->belongsToMany('App\Faq');
}
}
Faq Model
class Faq extends Model
{
/**
* Get the faq's faq groups.
*/
public function faq_groups()
{
return $this->belongsToMany('App\FaqGroup');
}
}
One concern is whether FAQ Categories and Subcategories should be different tables. Maybe should be the same table in order to give you the option to have more levels in the future.
i.e.
faq_categories(id, parent_id, name etc.)
Then, in my opinion, you need just different pivot tables. I can not see any reason to use polymorphic relationships.

Laravel ManytoMany: How to attach a product to a user upon user registration?

I am new to Laravel and I am trying to create an application for our customers to confirm ownership of their products.
I created the following tables and models:
users table
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
products table
Schema::create('products', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->integer('etc');
$table->timestamps();
});
user_product table
Schema::create('user_product', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id')->index();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->unsignedBigInteger('product_id')->index();
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
$table->timestamps();
});
user model
public function products()
{
return $this->belongsToMany('App\Product', 'user_product')->withTimestamps();
}
product model
public function users()
{
return $this->belongsToMany('App\User');
}
So here’s where I’m stuck…
I want to have a product displayed together with the user registration form. Upon registration, the user has to confirm ownership of the product. So I need to associate the product displayed with the user account that is about to be created. Not sure how to do that though…
I am using ManyToMany because I want to have all the products in the database, even if they belong to a user or not. After the association between the user and the product, you cannot access that product without being logged in.
I hope it makes a bit of sense, still working on the logistics behind this :)
Any idea of how to reach this scenario?
Any help would be much appreciated :)
If the product can only belong to a single user and a user can have many products you should use One To Many, and create a foreing key in the product table by calling user and would use the create () or save () function.
Using softdelete in the User model you do not risk deleting the user and their products.
But if you want a Many to Many relationship anyway use the attach () function as follows:
$user->produtcs()->attach($productId);
or else
$product->users()->attach($userId)
But both models already have to be in the database, both User and Product.
If you want to know which User owns the product you can add a user_owner_id field in the user_product table and add it in the attach like this:
$user->produtcs()->attach($productId, ['user_owner_id' => $userId]);

How to swap two indexed fields with laravel migrations?

In the Models of a many to many relationship I have accidentally identified the foreign key names in reverse. This is done in both related Models so the relationship works. It's in production.
In Articles:
public function categories()
{
return $this->belongsToMany(ArticleCategory::class, 'article_category_article', 'article_category_id', 'article_id');
}
and in ArticleCategory:
public function articles()
{
return $this->belongsToMany(Article::class, 'article_category_article', 'article_id', 'article_category_id');
}
As you can see, both foreign keys are reversed.
It doesn't bother me because it works throughout the project. In the article_category_article table both values are recorded in the 'wrong' column.
But what if I'd like to swap it anyway. The Models are easy, but what about the pivot table? I have tried with a laravel migration:
public function up()
{
Schema::table('article_category_article', function (Blueprint $table) {
$table->renameColumn('article_id', 'temporarily');
$table->renameColumn('article_category_id', 'article_id');
$table->renameColumn('temporarily', 'article_category_id');
});
}
without success, it predictably runs into the error There is no column with name 'temporarily' on table 'article_category_article'
Splitting it up in 2 migration files ran into the same error.
I have the tendency to let it be. The question is: can it be done? I presume swapping the columns inside MySQL (without migrations), re-index the tables and adapt the Models is a possibility. Any ideas? I can test it out on a local server.
Two separate queries work for me:
public function up()
{
Schema::table('article_category_article', function (Blueprint $table) {
$table->renameColumn('article_id', 'temporarily');
$table->renameColumn('article_category_id', 'article_id');
});
Schema::table('article_category_article', function (Blueprint $table) {
$table->renameColumn('temporarily', 'article_category_id');
});
}

Many to many relationship in Voyager Admin Panel

I get this error when I try to add a new item with the many to many relationship using the BREAD creator for voyager:
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'id' in
field list is ambiguous (SQL: select id from products inner join
order_product on products.id = order_product.product_id
where order_product.order_id is null) (View:
D:\WindowsFolders\Documents\PHP\Voyager-test\vendor\tcg\voyager\resources\views\bread\edit-add.blade.php)
I have followed the documentation and not really sure where I am having the issue. To me it looks like my many to many setup should work in normal Laravel, but it throws that error when I try to add a new order item in the voyager panel. And the documentation really don't specify what the extra field in the parent table should be(where I put a comment).
Here is my order and order-pivot table;
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id');
$table->text('comment')->nullable();
$table->integer('price');
$table->boolean('sent');
$table->boolean('paid');
$table->string('products'); // This thing
$table->timestamps();
});
Schema::create('order_product', function (Blueprint $table) {
$table->increments('id');
$table->integer('order_id')->unsigned();
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
$table->integer('product_id')->unsigned();
$table->foreign('product_id')->references('id')->on('products');
$table->integer('amount');
$table->timestamps();
});
}
Here is the product table:
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->nullable();
$table->text('description');
$table->string('image');
$table->integer('price');
$table->string('slug')->unique();
$table->timestamps();
});
}
Here is the relationship function:
public function products()
{
return $this->belongsToMany(Product::class, 'order_product');
}
And lastly a picture from the Order BREAD part:
The MySQL error message is quite clear in this case: both products and order_product tables have id fields. The select list of the sql statement in the question simply has id, therefore MySQL cannot decide which table the id field should be selected from.
You need to prefix the field name with the table name to make it clear:
select products.id from ...
I had a similar issue and solved it by changing the relationship key from id to product_id inside the json options in Voyager Admin

Save data with 3 relations eloquent

I am trying to save data with eloquent relationship.
I have following three tables: User Table, Category Table and Post Table.
Post Table
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('category_id')->unsigned();
$table->integer('user_id')->unsigned();
$table->string('heading');
$table->timestamps();
$table->foreign('category_id')->references('id')->on('categories');
$table->foreign('user_id')->references('id')->on('users');
});
Relations:
Category:
public function posts() {
return $this->hasMany('App\Post');
}
Post:
public function user() {
return $this->belongsTo('App\User');
}
public function category() {
return $this->belongsTo('App\Category');
}
User:
public function posts($category) {
return $this->hasMany('App\Post');
}
My Problem is that, how can I save post just by passing the Heading in create function. I want to use the relationship. As an example I want to use this kind of code:
$data = ['heading' => $heading];
$user->posts()->category()->create($data);
Is this possible to do this kind of stuff ?
Or any another simple way to achieve this.
EDIT
I need to create post by using this kind of relationship.
As per the process:
user will fill up the form from which I will get the data along with
the category id.
Now I need to create data for that user related with the given category id.
It's because after you call posts() method you won't get to the model's relation (only the query builder) so you will not access category() relation method. It's because posts are one-to-many relation and you don';t know exacly which record you refer to create data.
EDIT
If you want to create new post entry the the best way to sole this is:
$data = ['heading' => $heading, 'category_id' => $putHereCategoryId];
$user->posts()->create($data);
You'll need to obtain somehow the id of the desire category for the new post's entry.