I'm using ActiveRecord in my app. I need to add many objects by attribute. Currently I do it as follows:
foreach($objects as $object):
$result += $object->value;
endforeach;
But it is very slow. I think I could get the same result but more efficient by sql sum(), so de question is can ActiveRecord return me the sum() result?
The only aggregating function already implemented in the model is count(). For sum(), you'd have to use an SQL query. You can do that using different classes (Connection, Table or Model).
Here's using find_by_sql() of the Model (returns an array of models):
$result = FooModel::find_by_sql('SELECT SUM(`value`) AS sum FROM `foo_table`')[0]->sum;
Add custom model to your lib/Model.php file
public static function sum($args) {
$args = func_get_args();
$options = static::extract_and_validate_options($args);
$options['select'] = 'SUM('.$args[0]["sum"].')';
if (!empty($args) && !is_null($args[0]) && !empty($args[0]))
{
if (is_hash($args[0]))
$options['conditions'] = (isset($args[0]["conditions"]) ? $args[0]["conditions"] : array());
else
$options['conditions'] = call_user_func_array('static::pk_conditions',$args);
}
$table = static::table();
$sql = $table->options_to_sql($options);
$values = $sql->get_where_values();
return static::connection()->query_and_fetch_one($sql->to_s(),$values);
}
Then call it:
YourModel::sum(array('conditions' => 'userid = 3', 'sum' => 'your_column'));
or
YourModel::sum(array('sum' => 'your_column'));
Related
I have a function that will return how many products there are on an invoice and it seems pretty straightforward to me but maybe it can use some refactoring?
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$dbmSupplier = new Dbm_Supplier($dataset);
$whereAndOpt = $this->getConditionsAndOptions();
$where = $whereAndOpt['where'];
$opt = $whereAndOpt['opt'];
$select = 'sum(product_quantity) as sumTotal';
$invoiceTotalProductsNumber = $dbmSupplier->findFirstSimple($where, $select, $opt);
$invoiceTotalProductsNumber['sumTotal'] = (int)$invoiceTotalProductsNumber['sumTotal'];
return $invoiceTotalProductsNumber['sumTotal'];
}
How can I extract this into at least two functions?
For this kind of thing, ideally you'd use an ORM. As I'm not sure which you're using, if at all, I'd probably refactor it to something more in this direction so it feels closer to how an ORM would work:
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$extras = $this->getConditionsAndOptions();
$queryBuilder = [
'dbm' => new Dbm_Supplier($dataset),
'where' => $extras['where'],
'opt' => $extras['opt']
];
return $this->sumOfProductQuantities($queryBuilder);
}
private function sumOfProductQuantities(array $queryBuilder): int
{
$select = 'sum(product_quantity) as sumTotal';
$row = $this->queryBuilderFirstRow($queryBuilder, $select);
return (int)$row['sumTotal'];
}
private function queryBuilderFirstRow(array $qb, string $select): array
{
return $qb['dbm']->findFirstSimple($qb['where'], $select, $qb['opt']);
}
I personally think this is pretty straightforward and nicely clean, but the only optimization I can suggest is to extract getConditionsAndOptions part as it seems to be used in some other several parts of your code. Look at this please:
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$dbmSupplier = new Dbm_Supplier($dataset);
list($where, $opt) = $this->getWhereAndOpt();
$select = 'sum(product_quantity) as sumTotal';
$invoiceTotalProductsNumber = $dbmSupplier->findFirstSimple($where, $select, $opt);
$invoiceTotalProductsNumber['sumTotal'] = (int)$invoiceTotalProductsNumber['sumTotal'];
return $invoiceTotalProductsNumber['sumTotal'];
}
private function getWhereAndOpt(): array
{
$whereAndOpt = $this->getConditionsAndOptions();
return [
$whereAndOpt['where'],
$whereAndOpt['opt'],
];
}
Another point maybe name conventions. I can see you have used snail_case naming for Dbm_Supplier. It's better to be either dbm_supplier or DbmSupplier.
I cant orderBy points. Points is accessor.
Controller:
$volunteers = $this->volunteerFilter();
$volunteers = $volunteers->orderBy('points')->paginate(10);
Volunteers Model:
public function siteActivities()
{
return $this->belongsToMany(VolunteerEvent::class, 'volunteer_event_user', 'volunteer_id', 'volunteer_event_id')
->withPivot('data', 'point', 'point_reason');
}
public function getPointsAttribute(){
$totalPoint = 0;
$volunteerPoints = $this->siteActivities->pluck('pivot.point', 'id')->toArray() ?? [];
foreach ($volunteerPoints as $item) {
$totalPoint += $item;
}
return $totalPoint;
}
But I try to sortyByDesc('points') in view it works but doesn't work true. Because paginate(10) is limit(10). So it doesn't sort for all data, sort only 10 data.
Then I try to use datatable/yajra. It works very well but I have much data. so the problem came out
Error code: Out of Memory
You could aggregate the column directly in the query
$volunteers = $this->volunteerFilter();
$volunteers = $volunteers->selectRaw('SUM(pivot.points) AS points)')->orderByDesc('points')->paginate(10);
I'm working on an application where users have different types of events. Each event type has its own database table/Laravel model. Right now I'm performing multiple database queries to fetch the events from the tables. After that I merge them manually by using for-each loops and creating a uniform structure.
Because the code is really long, I give you guys an example code here:
$output = [];
$events1 = EventType1::where('user',$user_id)->get();
foreach ($events1 as $ev1) {
$output[] = [
"id" => $ev1->id,
"date" => $ev1->id,
"attribute3" => $ev1->attributeA
];
}
$events2 = EventType2::where('user',$user_id)->get();
foreach ($events2 as $ev2) {
$output[] = [
"id" => $ev2->id,
"date" => $ev2->id,
"attribute3" => $ev2->someOtherAttribute
];
}
// More fetches here....
// ...
// ...
usort($output, function ($a, $b) {
return strcmp($a["date"], $b["date"]);
});
return $output;
So right now I want to improve the performance by using Pagination. But the way I fetch the data, I don't think it will work?!
Can someone help me how to fetch, merge and paginate all the events the proper way?
Is there a way to union all the data using Eloquent?
Thanks
If you want with pagination you can follow this. I have used one of my projects for merging different model data as well as including pagination. If you don't need pagination just ignore pagination method calling part.
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
public function events(){
$events1 = EventType1::where('user',$user_id)->get();
$events2 = EventType2::where('user',$user_id)->get();
$collection = new Collection();
$collection = $collection->merge($events1);
$collection = $collection->merge($events2);
$merge = $this->paginate($collection, $limit);
return $merge;
}
public function paginate($items, $perPage = 10, $page = null, $options = [])
{
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
$items = $items instanceof Collection ? $items : Collection::make($items);
return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
}
Details
I want to
Count all my distributors that I query
Send it along within the JSON file
I know that I have 89 distributors in total, I did this dd(count($distributors));
I am sure what is the best practice for this.
Here is what I have tried
I initialize $count = 0;
Increment by 1 every time the loop execute $count = $count + 1;
Send the result toArray 'count' => $count->toArray() as part of my distributors array
Here is my code
public function index()
{
$distributors = [];
$count = 0;
foreach(
// Specific Distributors
Distributor::where('type','!=','OEM')->where('type','!=','MKP')
->get() as $distributor){
// Variables
$count = $count + 1;
$user = $distributor->user()->first();
$distributors[$distributor->id] = [
'user' => $user->toArray(),
'distributor' => $distributor->toArray(),
'hq_country' => $distributor->country()->first(),
'address' => $distributor->addresses()
->where('type','=','Hq')->first()->toArray(),
'count' => $count->toArray()
];
}
return Response::json($distributors);
}
Result
The code won't run, due to my $distributor array is not exist ...
It will run, if I take 'count' => $count->toArray() off .
Updated
I am using Laravel 4.0
The code is part of my UrlController.php
It really doesn't make a lot of sense to add this kind of count to your result. It is much simpler to just send the result and let the client do the counting. Because the information on how much distributors you actually have is right in your array of distributors. It's just it's length.
Here's an example with javascript (and $.ajax although that doesn't really matter)
$.ajax({
url: 'get/distributors',
method: 'GET'
}).done(function(data){
var count = data.length;
});
Model:
class Distributor extends Eloquent {
public function country()
{
return $this->hasOne('Country');
}
public function addresses()
{
return $this->hasMany('Address');
}
public function hqAddress()
{
return $this->addresses()->where('type', 'Hq')->first();
}
public function user()
{
return $this->hasOne('User');
}
}
Controller:
$distributors = Distributor::whereNotIn('type', ['OEM', 'MKP'])
->with('country', 'user')->get();
$count = 0;
$distributors->each(function(Distributor $distributor) use(&$count) {
$distributor->count = $count;
$count++;
});
return Response::json($distributors);
Sorry, I can be wrong.
I am not laravel expert.
But what is this fragment is about?
this Index function is part of Model?
#evoque2015 is trying to put some custom array into $distributors[$distributor->id]?
if that is the goal, could you do any test with any simple value of 'count' that sholud work in your opinion?
My guess is : 'count' index is not acceptable for your Distributor::where function
(if it is acceptable - show as the value of 'count' that doesn't break your code even if return wrong result/data ).
So I would try to change the name of this parameter either to 'my_custom_count',
should it be declared somewhere in Distributor model declaration?
Found this :
Add a custom attribute to a Laravel / Eloquent model on load?
It seems to prove my guess.
So we need to change model class like:
class Distributor extends Eloquent {
protected $table = 'distributors';//or any you have already
public function toArray()
{
$array = parent::toArray();
$array['count'] = $this->count;
return $array;
}
.....
}
and probably more changes in model or just add 'count' column to the table :-)
I'm using the addColumnCondition function as I like how it forms the queries for multiple queries. But I can't find anything in the documentation to change it's comparison operation from the simple = needle to a LIKE %needle%. There is a function that does a LIKE in addSearchCondition() but then it means to get the same query formation result, I'll have to do some for loops and merge conditions which I'd like to avoid if there is a better solution.
Here's the code
foreach($query_set as $query){
foreach($attributes as $attribute=>$v){
$attributes[$attribute] = $query;
}
$criteria->addColumnCondition($attributes, 'OR', 'AND');
}
And I'm getting the condition formed like
(business_name=:ycp0 OR payment_method=:ycp1) AND (business_name=:ycp2 OR payment_method=:ycp3)
So is there a way to configure the function to use LIKE %:ycp0% instead of the simple =:ycp0.
It seems, this feature is not provided by Yii's addColumnCondition method.
therefore i would recommend a way of overriding the method of CDbCriteria class and customize it your own way.
you need to create a new class called "AppCriteria", then place it inside protected/models
The code for the new class should look like,
i.e
class AppCriteria extends CDbCriteria {
public function addColumnCondition($columns, $columnOperator = 'AND', $operator = 'AND', $like = true) {
$params = array();
foreach ($columns as $name=>$value) {
if ($value === null)
$params[] = $name.' IS NULL';
else {
if ($like)
$params[] = $name.' LIKE %'.self::PARAM_PREFIX.self::$paramCount.'%';
else
$params[] = $name.'='.self::PARAM_PREFIX.self::$paramCount;
$this->params[self::PARAM_PREFIX.self::$paramCount++] = $value;
}
}
return $this->addCondition(implode(" $columnOperator ", $params), $operator);
}
}
Note: The 4th param of addColumnCondition, $like = true. you can set it to $like = false and allow the function to work with equal conditions. (A = B)
i.e
(business_name=:ycp0 OR payment_method=:ycp1) AND (business_name=:ycp2 OR payment_method=:ycp3)
if $like = true, it will allow you to have like condition. (A like %B%)
i.e
(business_name LIKE %:ycp0% OR payment_method LIKE %:ycp1%) AND (business_name LIKE %:ycp2% OR payment_method LIKE %:ycp3%)
Now Here's the working code,
$criteria = new AppCriteria();
foreach($query_set as $query){
foreach($attributes as $attribute=>$v){
$attributes[$attribute] = $query;
}
$criteria->addColumnCondition($attributes, 'OR', 'AND');
}