Laravel query builder vs Raw - laravel

Im quite new to laravel and so far I realy enjoy eloquent and querybuilder, but as soon as the queries become more complex, my head starts to hurt... I have just finished 2 working examples after quite some time, maybe you guys can help me optimize it. I will first give the examples, then the (dis)advantages
So first the DB::select... which honestly, I think is more readable then the 2nd example.. or maybe I am just doing the builder thing wrong xD.
$shared_slugs = Magic::in_query($this->shared_slugs);
$items = DB::select("
SELECT * FROM f_admin_items
WHERE
active = 1
AND (
slug IN $shared_slugs
OR
:treshhold <= :urank AND id IN (SELECT admin_item_id FROM f_user_access WHERE user_id = :uid)
OR
:treshhold > :urank AND `rank` >= :urank
)
ORDER BY `order` ASC
", [
'treshhold' => $this->threshold_rank,
'urank' => $user_rank,
'uid' => $user_id
]);
Overall pretty effective with the named parameter binding. Basicly the menu items always have to be active, plus (1 OR 2 OR 3 ). For example dashboard is accepted as shared slug.. Otherwise there are some ranking checks.
Now I have been trying hard to do this with eloquent en query builder =/ I didn't think setting up a relation was not necessary because I only use this for the menu and a middleware. In this case the f_user_access table containing only admin_item_id and user_id, doesn't really function as a pivot and isn't used anywhere else.
$items =
$this->where( 'active', 1 )
->where( function ($query) {
$query->whereIn( 'slug', $this->shared_slugs )
->orWhere(function ($query) {
$query->whereRaw( $this->threshold_rank.' <= '.$this->user_rank )
->whereIn('id', function ($query) {
$query->select('admin_item_id')->from('f_user_access')->where('user_id', $this->user_id)->get();
});
})
->orWhere(function ($query) {
$query->whereRaw( $this->threshold_rank.' > '.$this->user_rank )
->where( 'rank', '>=', $this->user_rank );
});
})->orderBy( 'order', 'ASC' )->get();
What I like about this second one is the fact I can pass $shared_slugs as an array, I don't have to convert it into a string first. But apart from that I really hate the looks of this, where(function($query){}) etc etc... On top of that the $ids passed to this function, are not accessible in the where.functions, so I had to define them in the class first.
The first one I like because of the named binding and it doesn't read that bad to be :S, also the vars are accessible.. Downside, I have to call this function to convert the shared slugs into an array.
Is it really that bad to not use eloquent and querybuilder? at least in some cases... and what would you guys do to make the second example better? =/
UPDATE::
Due to the answers and feedback, I ditched the raw sql. The current function has 5 scopes in it, and a (small) model is made for the user_access.
$items =
$this->active() //Only active items AND... 1, 2 or 3
->where( function ($query) {
$query->shared_slug() // 1
->orWhere(function ($query) { // OR 2
$query->access_required()
->whereIn('id', UserAccess::get_access( $this->user_id ) );
})
->orWhere(function ($query) { // OR 3
$query->no_access_required()
->whereRaw( 'rank >= '.$this->user_rank );
});
})->ASC()->get();

The major benefit of using the query builder is it abstracts you away from the language used by your storage of choice, i.e. MySQL, Oracle, SQLite, etc. If you ever switch database types, you can be stuck with a lot of refactoring raw SQL. Believe me, it's not pretty when you start on a project and learn this is the case.
There are always caveats, however, which is precisely why Eloquent has the capability to work with raw statements, as well.

Related

Using Query Builder to execute multiple WhereExists

Here is the relevant part of the ERD:
Post is an ad of a computer or laptop.
A computer or laptop contains several parts (processor, memory, storage etc). A Postpart represents one such part.
A component is particular model of hardware, e.g. Intel Core i9 (9th gen) or Samsung 970 Evo Plus.
Since each component has their own set of attributes, we have created a polymorphic relation between processors, memories, (storage, keyboard and several other components types not shown in the diagram)
Now I need to search for all the posts that comply with the following conditions.
Have one or more of the specified words in title or description
Have got a processor with core_clock between 1.8 and 2.4 GHz.
Have got a total memory of 32 GB or more. (i.e. we need to sum up memories of each post)
In the good old SQL world, we could probably do something like:
SELECT * FROM posts p WHERE title LIKE `%Acer Aspire%` AND description LIKE `%something else%`
WHERE EXISTS
(
SELECT id FROM components c
INNER JOIN processors pr ON c.componentable_id = pr.id
INNER JOIN postparts pp ON p.id = pp.post_id
WHERE pr.core_clock BETWEEN 1.8 AND 2.4
)
AND
(
(SELECT SUM(total) FROM components c
INNER JOIN memories m ON c.componentable_id = m.id
INNER JOIN postparts pp ON p.id = pp.post_id) >= 32
)
...
and continue like that for all other components. Now I'm trying to achieve that using Eloquent or Query Builder. Here is what I have written so far:
$query = Post::where('title', 'LIKE', '%' . $searchText . '%')->
orWhere('description', 'LIKE', '%' . $searchText . '%');
$query = $query->whereExists(
function ($query) {
$query->select("1")->from('postparts')->
join('components', 'postparts.component_id', '=', 'component.id')->
join('processors', 'processors.id', '=', 'component.componentable_id')->
where('postparts.post_id', 'posts.id')->
where('processors.core_clock', '>=', 1.8)->
where('processors.core_clock', '<=', 2.4);
});
$query = $query->where(
function ($query) {
$query->join('postparts', 'postparts.post_id', '=', 'posts.id')->
join('components', 'postparts.component_id', '=', 'component.id')->
join('memories', 'memories.id', '=', 'component.componentable_id')->
sum('total') >= 32;
});
This doesn't work and complains about access violation or syntax error in the query. What am I doing wrong here?
N.B. I also have setup polymorphic relationship between the Laravel models and can successfully fetch all (polymorphic) components of a single Post using Post::with('Postparts.Component')->find($post_id). However I'm not sure if Eloquent will really be as efficient as Query Builder or a raw query. If it is, how can I filter Posts based on polymorphic child models without actually fetching them into a collection?
With the right relation declared being belongToMany between Post::class and Component::class, and mophTo between component and memory/processor
$posts = Post::whereHas('components', function(Builder $compQB) {
$comQB->whereHasMorph(
'componentable',
[Processor::class],
function (Builder $query) {
$query->whereBetween('core_clock', [1.8, 2.4]);
}
)
})->whereHas('components', function(Builder $compQB) {
$comQB->whereHasMorph(
'componentable',
[Memory::class],
function (Builder $query) {
$query->where('capacity', '>=', 32);
}
)
})
->where('description',....)
->where('title', .....)
->get();
I dont know how the sum memory part works, but i hope this gives you at least a good hint on the structure of the query builder.
I don't think you can use ->select('1') directly.
you should use raw expression:
->select(DB::raw(1))
see whereExists example.

Avoid overriding eloquent data

This is my collection of data I need:
$author_id = 12345;
$newsTagCollection = NewsTag::
->where('author_id', $author_id);
$postTodaycnt =$newsTagCollection->where('post_date', '>', \Carbon\Carbon::today())->get()->count();
$postWeekcnt =$newsTagCollection->where('post_date', '>', \Carbon\Carbon::now()->subDays(7))->get()->count();
dd($postWeekcnt);
$newsTags = $newsTagCollection->simplePaginate(12);
What I'm trying is to avoid calling the Model multiple times and just past post_date on postTodayCount and PostWeekCount. But $postWeekcnt seem to get value same as $postTodaycnt.
How do I not replace the value and get $postTodaycnt, $postWeekcnt and $newsTags value from $newsTagCollection as desired?
Thank you
Good question.
Answer to that is Optimization. Laravel provide Collection which is very helpful when it comes to such scenarios.
// This will return a *Collection* of NewsTag model.
$newsTagCollection = NewsTag::where('author_id', $author_id)->get(); // One query. Use ->get() here to fetch all the data
// Here no new queries will triggered.
$postTodaycnt = $newsTagCollection->where('post_date', '>', \Carbon\Carbon::today())->count(); // No need to use get() again
$postWeekcnt =$newsTagCollection->where('post_date', '>', \Carbon\Carbon::now()->subDays(7))->count();

Eloquent: Do I need to do this query raw?

I'm rewriting parts of legacy software using laravel/eloquent.
my understanding of typical eloquent is good and I've done some projects in laravel but not so much for legacy code.
I'm trying to do this in eloquent which creates 79 rows:
INSERT IGNORE INTO `ps_module_shop` (`id_module`, `enable_device`, id_shop) (SELECT `id_module`, `enable_device`, 555 FROM ps_module_shop WHERE `id_shop` = 1);
I've read you can only do bulk insert with model::insert but I'm not sure how to do so with this query specifically, other than pure sql.
This is how I'm doing it for now, but raw sql feels more elegant for this query:
$blkInsert = [];
$tplModuleShop = ModuleShop::where('id_shop', 1)->get();
foreach($tplModuleShop as $mshop) {
$blkInsert[] = [
'id_module' => $mshop->id_module,
'enable_device' => $mshop->enable_device,
'id_shop' => $shop->id_shop,
];
}
ModuleShop::insert($blkInsert);
Note: I know this is a pivot table, but it is legacy db that uses composite keys so I decided to treat it as its own model.
Honestly this might just be easier using your sql statement... here is my attempt:
DB::table('ps_model_shop')
->insert(DB::table('ps_module_shop')
->select(['id_module', 'enable_device'])
->where('id_shop', 1)
->get()
->map(function($module){
$module['id_shop'] = 555;
})
->toArray()
);
We found a way to do it with DB::select (although still doing the sql part "raw")
$fixCollection = function($collection) {
return json_decode(json_encode($collection), true);
};
ModuleShop::insert(
$fixCollection(
\DB::select("SELECT `id_module`, `enable_device`, {$shop->id_shop} as id_shop FROM ps_module_shop WHERE `id_shop` = 1")
)
);

Return year count even when count is 0

I'm trying to work on a chart with a few datasets, however for the purposes of asking this question, I will only include one.
At the moment if I have the following in my controller:
$maintenancesForklifts =
DB::table("equipment_attachments")
->join('equipment as equip', 'equip.id', '=', 'equipment_attachments.unitID')
->select(DB::raw('year(date) as year'), DB::raw("COUNT(*) as count"))
->where('attachmentCategory','Maintenance')
->where('equip.unit_type',3)
->orderBy(DB::raw("year(date)"))
->groupBy(DB::raw("year(date)"))
->get();
I will get this returned:
[{"year":2005,"count":2},{"year":2006,"count":2},{"year":2010,"count":4},{"year":2011,"count":1},{"year":2012,"count":2},{"year":2013,"count":1},{"year":2014,"count":10},{"year":2015,"count":7},{"year":2016,"count":6},{"year":2017,"count":19},{"year":2018,"count":4}]
As you can see there are a few years missing if I went from say 2000 to the present day. Do you know how I could return the results so that they would come back with the years that have a zero count?
One way you could do this:
$years = collect(range(2000, now()->format('Y')))->map(function ($year) use ($maintenancesForklifts) {
return $maintenancesForklifts->firstWhere('year', $year) ?? (object)['year' => $year, 'count' => 0];
});

Querying related table data with Eloquent

i have a problem trying to get records from a model based on a related table.
I have two tables one called leads and one called recycle_logs, my app will basically send the same record in the leads table like once a month, and when it does so i'll store a new record in recycle_logs.
The problem is that i need to select all leads with a specific campaign_id value and that have a specific status that is different from invalid, so far so good, now the problem is i need to get them only if they don't have any recycleLogs associated with them OR if the last recycleLog associated with that particular lead is older than 30 days ago for instance.
What i currently have looks somewhat like this.
$leads = $this->leadModel->where(Lead::CAMPAIGN_ID, $this->campaignID)
->where(Lead::DUPLICATED, Lead::DUPLICATED_NO)
->where(Lead::LEAD_STATUS, "!=" ,Lead::LEAD_STATUS_INVALID)
->orderBy(Lead::CREATED_AT, 'desc')
->with(
['leadRecyclingLog' => function($query) {
$query->where(LeadRecyclingLog::CREATED_AT, '<', (new Carbon())->subDays($this->configRecyclingDays))
->orWhere(LeadRecyclingLog::ID, null);
}]
)
->get();
What exactly am i doing wrong? It always selects the same number of records regardless of me adding or removing recycleLogs
I've managed to get it done through a raw SQL query which i'll post below in case it helps anyone, i'd still like to know how to do it in Eloquent/Query Builder.
SELECT * FROM `leads` LEFT JOIN `lead_recycling_logs` ON `leads`.`guid` = `lead_recycling_logs`.`original_lead_guid` WHERE `leads`.`campaign_id` = :campaignID AND `leads`.`duplicated` = 0 AND `leads`.`lead_status` != :invalidStatus AND (`lead_recycling_logs`.`id` IS NULL OR `lead_recycling_logs`.`created_at` < :recyclingDate) ORDER BY `leads`.`created_at` DESC
Try this:
$leads = $this->leadModel->where(Lead::CAMPAIGN_ID, $this->campaignID)
->where(Lead::DUPLICATED, Lead::DUPLICATED_NO)
->where(Lead::LEAD_STATUS, "!=" ,Lead::LEAD_STATUS_INVALID)
->orderBy(Lead::CREATED_AT, 'desc')
->where(function($q) {
$q->whereHas('leadRecyclingLog', function($q) {
$q->where(LeadRecyclingLog::CREATED_AT, '<', (new Carbon())->subDays($this->configRecyclingDays));
})
->orWhereHas('leadRecyclingLog', '<', 1); // Where count of the relationship is smaller than 1
})->get();
I assumed the first part of the query is working well (up until the relationship).
What you're looking for is ->whereHas(relationship), not ->with(relationship). ->with(relationship) will attach the associated results to the original model (the query for the original model will not be affected by ->with()). ->whereHas(relationship) filters the original model by the condition.
Got it to work through #devk 's help
$leads = $this->leadModel->where(Lead::CAMPAIGN_ID, $this->campaignID)
->where(Lead::DUPLICATED, Lead::DUPLICATED_NO)
->where(Lead::LEAD_STATUS, "!=" ,Lead::LEAD_STATUS_INVALID)
->orderBy(Lead::CREATED_AT, 'desc')
->where(function($q) {
$q->whereHas('leadRecyclingLog', function($q) {
$q->where(LeadRecyclingLog::CREATED_AT, '<', (new Carbon())->subDays($this->configRecyclingDays));
})
->doesntHave('leadRecyclingLog', 'or');
})->get();

Resources