Optimising eloquent query - laravel

I have a need to attach a custom select on an eloquent query, but I find the whole thing a bit hard to understand. This regards a forum system where each Forum object needs to know how many Topic relations and Comment relations are registered. This is what I've got so far:
in Forum.php
public function getNumTopics () {
return Topic::where('forum_id', '=', $this->id)->count();
}
public function getNumComments () {
return Comment::wherein('topic_id', Topic::where('forum_id', '=', 1)->lists('id'));
}
Controller returning json
public function getCategories () {
$categories = ForumCategory::with('forums')->get();
foreach ($categories as $cat) {
if ($cat->forums->count() > 0) {
foreach ($cat->forums as $forum) {
/* #var $forum Forum */
$forum->num_topics = $forum->getNumTopics();
$forum->num_posts = $forum->getNumComments();
}
}
}
return Response::json($categories, 200);
}
The call takes approx 1300ms to return 5 Forums in three categories. I suspect this is because this executes around 16 queries instead of one. Is there a way to attach the "num_topics" an "num_posts" as properties on select, so that I only execute one query?
Edit
What I basically want is for Eloquent to produce something like this when I ask for Forum::all():
select f.*,
ifnull(count(t.id), 0) num_topics,
ifnull(count(c.id), 0) num_posts
from forums f left join topics t on t.forum_id = f.id
left join comments c on c.topic_id = t.id
group by f.id

You will have to query those tables either way so your best bet should be to just eager load everything and count it after it's been retrieved instead of making more calls to the database just to find a count.
public function getCategories () {
$categories = ForumCategory::with('forums.topics.comments')->get();
foreach($categories as $category) {
foreach($category->forums as &$forum) {
$forum->num_topics = $forum->topics()->count();
$forum->num_comments = 0;
foreach($forum->topics as $topic) {
$forum->num_comments += $topic->comments()->count();
}
}
}
return Response::json($categories, 200);
}
This will attach num_topics and num_comments to each Forum object.
It's fewer calls but it's grabbing more information in one sweep so it may be faster or slower, will have to do some testing. The good news is that because it's grabbing everything, no additional queries should be needed.

Related

Any short way for handling multiple queries in laravel?

I need to get multiple results from different queries on one table.
For example I need to get Count, Sum, Average of one table. Should I do like this or is there a shorter way?
public function index()
{
$count = Patient::all()->count();
$dateCount = Patient::where('date', date("Y-m-d"))->count();
$loanAmount = DB::table('patients')->sum('loan_amount');
$payAmount = DB::table('patients')->sum('pay_amount');
return view('index', compact('count','dateCount','loanAmount' ,'payAmount'));
}
If you see All queries are for one table to get specific results, So basically is there a short way to get these results not by single queries for each?
You can do this by DB query as below :
$data=\DB::table('patients')
->selectRaw('count(id) as count,sum(loan_amount) as loanAmount,sum(pay_amount) as payAmount,sum(case when date = "'.date("Y-m-d").'" then 1 else 0 end) AS dateCount')
->first();
You can also do this using eloquent but it will return you collection.
$data=Patient::selectRaw('count(id) as count,sum(loan_amount) as loanAmount,sum(pay_amount) as payAmount,sum(case when date = "'.date("Y-m-d").'" then 1 else 0 end) AS dateCount')
->first();
You can do something like this.
public function index()
{
$patient = Patient::all();
$count = $patient->count();
$dateCount = Patient::today()->count();
$loanAmount = $patient->sum('loan_amount');
$payAmount = $patient->sum('pay_amount');
return view('index', compact('count','dateCount','loanAmount' ,'payAmount'));
}
Also you can create scope in your patient model:
public function scopeToday($query) {
return $query->where('date', date("Y-m-d"));
}

Laravel, Eloquent, getting related items from a morph relationship (advice)

The title of the question may seem weird, but I'll explain it briefly. I'm not experiencing any issues yet, but considering any advice regarding the following approach.
I have the following tables:
articles
hasMany(tags)
movies
hasMany(tags)
series
hasMany(tags)
subs (morph)
media_id
media_type
user_id
All of the first three tables have many tags, where the subs table have a Morph relation between them all.
The concept is simple, the user can subscribe to movies and/or series and NOT articles, let's say, opt-in a record from movies.
The following record will be inserting:
media_id => 1, media_type => movie, user_id => 1
Let's say we have 10K records like this in subs table. Now, I create a record in articles with specific tags. I want to notify all users in subs, who are opted-in movies and/or series that have exactly the same tags as the record I'm targeting from articles.
TL;DR: Notifying users that are interested in specific tags from movies or series.
To achieve this, I added the morphTo in the subs model:
public function media(){
return $this->morphTo();
}
The logic is straightforward:
Get the article.
Get the tags() relationship and save it into a variable.
Check if tags are not empty, if not, continue
Get all the subs.
Get the morphTo relationship and get its tags.
Compare and continue.
$article = MediaArticle::find($notificationable_id);
$article_tags = $article->tags;
$subbed_users = array();
if (empty($article_tags) === false) {
$subs = NotificationMediaSub::all();
foreach ($subs as $sub) {
$sub_tags = $sub->media->tags;
foreach ($sub_tags as $sub_tag) {
foreach ($article_tags as $article_tag_title) {
if (strcasecmp($sub_tag->title, $article_tag_title->title) === 0) {
array_push($subbed_users, $sub->user_id);
}
}
}
}
// continue here
}
I find this approach really intensive and may cause the server to slow down a lot. Is there a better approach to achieve my goal?
=========================
Approach
I defined the media_tags_relationship as a Model, inside it:
public function media() {
if ($this->taggable_type === AdminHelper::MORPH_MEDIA_TRANSLATION_MOVIE['original']) {
return $this->belongsTo(MediaMovie::class, 'taggable_id', 'id');
} elseif ($this->taggable_type === AdminHelper::MORPH_MEDIA_TRANSLATION_SERIES['original']) {
return $this->belongsTo(MediaSeries::class, 'taggable_id', 'id');
}
return $this->belongsTo(MediaMovie::class, 'taggable_id', 'id')->where('id', 0);
}
Now, I'm fetching the subs like this:
$article = MediaArticle::find($notificationable_id);
$article_tags = $article->tags;
$subbed_users_tokens = array();
if (empty($article_tags) === false) {
$article_tags = $article_tags->map(function ($tag) {
return $tag->title;
});
$related_tags = MediaTag::whereIn("title", $article_tags)->get();
$related_tags = $related_tags->map(function ($tag) {
return $tag->id;
});
$tags_relationship = MediaTagsRelationship::whereIn("tag_id", $related_tags)->where("taggable_type", "movie")->orWhere("taggable_type", "series")->get();
foreach ($tags_relationship as $tag_relationship) {
$media = $tag_relationship->media()->with('subs')->get();
if (empty($media) === false) {
$media = $media[0];
$subs = $media->subs->map(function ($sub) {
return $sub->firebase_token;
});
array_push($subbed_users_tokens, $subs);
}
}
}

Paginate with Eloquent but without instantiation Models

We have two Models:
SimpleModel (id, country, code)
ComplexRelatedModel (id, name, address)
SimpleModel has many ComplexRelatedModel, then
class Product extends Model
{
protected $fillable = [
'name'
];
/* hasOne */
public function complexRelatedChild()
{
return $this->hasOne(self::class, 'parent_id', 'id');
}
}
If we do
$simples = SimpleModel
->with('complexRelatedChild')
->simplePaginate(100000 /* a lot! */);
And we need only do
foreach ($simples as $simple) {
echo $simple->complexRelatedChild->name;
}
Any ComplexChild has hydratated and ready. This takes a lot of memory in my case. And we need just one field without any funciton or feature of Model.
It's possible use some data field from related object or with eloquent this isn't possible?
Not sure I completely understand your question. You want to only load one field from the complexRelatedChild relation to keep memory limit down?
You could do:
$simples = SimpleModel::with(['complexRelatedChild' => function($query){
return $query->select(['id', 'name']);
})
->simplePaginate(100000);
Which can be simplified to:
$simples = SimpleModel::with('complexRelatedChild:id,name')
->simplePaginate(100000);
However if I were you, I would try to paginate less items than 100000.
Update:
You could use chunk or cursor functions to process small batches of SimpleModel and keep memory limit down.
SimpleModel::chunk(200, function ($simples) {
foreach ($simples as $simple) {
}
});
or
foreach (SimpleModel::cursor() as $simple) {
}
See the documentation for more information

Laravel include relationship result if one exists

I am trying to write a query where all items are returned (products) and if a relationship exists for that particular item (many to many) then that information is included too. When I include the relationship at the moment on the query it only returns items that have that relationship rather thatn every single item, regardless of whether that relationship exists or not.
Here is my query at the moment:
public static function filterProduct($vars) {
$query = Product::query();
if((array_key_exists('order_by', $vars)) && (array_key_exists('order', $vars))) {
$query = $query->orderBy($vars['order_by'], $vars['order']);
}
if(array_key_exists('category_id', $vars) && $vars['category_id'] != 0) {
$query = $query->whereHas('categories', function($q) use ($vars) {
return $q->where('id', $vars['category_id']);
});
}
if(array_key_exists('manufacturer_id', $vars)) {
$query = $query->whereHas('manufacturer', function($q) use ($vars) {
return $q->where('id', $vars['manufacturer_id']);
});
}
$query = $query->whereHas('options', function($q) use ($vars) {
});
As you can see, when an item has the 'options' relationship I need to have that particular row include details of that relationship in the returned date. With the code as it is though it is only returning items that have this relationship rather than every single item.
Can someone advise me as to how this is achieved please?
Thanks!
I feel a bit stupid as it was so simple but it was solved by adding this:
$query = $query->with('options');

how to make "follow" button change to "following" social network codeigniter

I am making a social network using Codeigniter and I'm trying to make the users view show a button when you are already following a person how do I do that??
Model
<?php
class User_model extends CI_Model {
function get_All(){
$this->db->select('*');
$this->db->from('membership');
// $this->db->join('following', 'following.id = membership.id');
$q = $this->db->get();
if($q->num_rows() > 0) {
foreach ($q->result() as $rows) {
$data[] = $rows;
}
return $data;
}
}
It's a little bit of work. Assuming that all users can be searched, you might have a list of all users, and of the 'friends' of all users. Then you run through the two comparing them. The following is an idea:
Database Members: All members |ID_User|...other user stuff...
Database Followers: Lists all friendship |ID_User|ID_Follower|
You need two queries for this: One for all, and one for Followers where ID_User=$UserID. As you run through all members as you suggest, then you can
function get_Followers(){
$friends= $this->db->query('SELECT * FROM Followers WHERE ID_User=$UserID');
$follow_array=$row->ID_Follower;
}
Now:
$follow_array //A list of all friends.
$q //Your original list of members
Then you can use:
if($q->num_rows() > 0) {
foreach ($q->result() as $rows) {
if(in_array($rows->ID_Follower, $Follower_array))
{
$data=true;
}
else
{
$data=false;
}
}
return $data;
}
Beware however, this may be a very server-heavy operation.
I'm going to assume you're talking about visiting a persons profile and if you are following that person you see a button. That only requires one query and logic in the view itself.
Going on this assumption you're already passing the person who's profile you're viewing to the view so all you need is a list of people the viewer is following to compare that id to.
I'm not really sure what your query in your code is doing since there's no description of your database so I'll give the code the way the tables should be laid out, which is a user table and a following table that is just a join table on the users. Assuming the following table has 3 columns id, userId, followingId where userId is you and followingId is the person you are following.
Controller
$data['following'] = $this->modelName->getFollowing($userId); //pass current userId
$this->load->view('viewName',$data);
Model:
public function getFollowing($userId)//$userId to be passed as the person viewing.
{
$this->db->where('userId',$userId);
$data = $this->db->get('following');
if($this->db->num_rows() > 0)
{
return $data;
} else {
$data=array();
$return $data;
}
Then in the view it's a simple if statement.
if(in_array($profileId,$following))
{
echo button;
}

Resources