I am having trouble figuring this out.
I have the following 3 tables: -
Transactions Table
ID
Date
List item
Products Table
ID
Name
Price
Products_Transactions Table
Transaction_ID
Product_ID
Quantity
So the relationship is as follows - a transaction is made, and then the products_transactions table joins them together since a transaction can have multiple products and a product can have multiple transactions. The join_table also keeps track of the amount sold, so, for instance, a newspaper sells in transaction #1 and with a quantity of 2 (so 2 newspapers sold).
Now, I want to make a MySQL statement that finds all products sold, in a specific date interval, so I get something like this: -
3 x Newspapers
12 x Sodas
15 x Beer
So, it just counts and sums up all the products sold.
I have seriously tried everything - I am working with CakePHP so a solution provided in that would be helpful, but even just the plain SQL to achieve this might help me out.
So far, this is what I have: -
$productTransactionsTable = TableRegistry::get('products_transactions');
$productsTransactions = $productTransactionsTable->find('all');
$productsTransactions->matching('transactions', function ($q) {
return $q->where([
'transaction_date >=' => new \DateTime('-1 week'),
'transaction_date <=' => new \DateTime('now'),
'device_id IN' => $this->deviceIdsInDepartment(2)
]);
});
$productsTransactions->contain(['products']);
$productsTransactions->select([
'count' => $productsTransactions->func()->count('quantity'),
'name' => 'products.name'
]);
$productsTransactions->groupBy('products.id');
But this just gives out 1 single result that counts everything together into 1 row, like this:
/src/Controller/EconomyController.php (line 665)
[
(int) 0 => object(Cake\ORM\Entity) {
'count' => (int) 4504,
'name' => 'D Morgenbrød',
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'products_transactions'
}
]
Any help is appreciated! I am seriously stuck here!
Thank you!
I think you are counting the total number of sold quantities and grouping so you are getting above results. I think, you need to try the following approach:
$data = $query
->select([
'name' => 'products.name',
'quantity' => 'products.quantity',
])
->group('products.id');
Related
the question is about SQL and Yii2 in particular.
There are three tables:
Product {id, title}
Image {id, url}
Product_Image {product_id, image_id, main}
I'm trying to use the query builder to form a query in such a way that the result is a table like this:
Result {product_id, title, main_image}
Columns product_id, title - from the Product table
Column main_image - should contain an entry from image.url where column main = 1, otherwise return the first found image or NULL
Product::find()
->select([
'product.id AS product_id',
'product.title AS title',
'image.image_url AS main_image',
])
->leftJoin('product_image', 'product_image.product_id = product.id')
->leftJoin('image', 'image.id = product_image.image_id')
->asArray()
->all();
This query returns a lot of the same product_id if the product has multiple images. I need to have only 1 entry for each product and there was either a link to the main image, or if there is no main, then the first entry that came across, or NULL
The ideal look is:
[
0 => [
'product_id' => '1'
'title' => 'Product 1'
'main_image' => null
]
1=>[
'product_id' => '2'
'title' => 'Product 2'
'main_image' => '/images/products/2.jpg'
]
2=>[
'product_id' => '3'
'title' => 'Product 3'
'main_image' => '/images/products/3.jpg'
]
]
I tried adding
andWhere(['product_image.main' => 1])
but in that case all products that don't have a 'main' image disappear.
Please help me, what do I need to add to the SQL request (MySQL)?
UPD:
->joinWith('productImages')
->joinWith('productImages.image');
This approach searches for all existing images and puts them in the "productImages" array. I don't have to search all the images for every product.
Example: The shop has 3 different payment amount of promotion, if buy 10 get 1 credit and 20 for 2 credit...These promotions I set on config like this:
reward
0
10 amount
1 reward
1
20 amount
2 reward
2
30 amount
3 reward
So, how can I loop or foreach these can determine my purchased amount?
note: I am new to programming so please guide me to my homework.
I expect the output should:
if amount=20
then
array[0] true
array[1] true
array[3] wrong(require amount=30)
then get the final credit i get
so, all you need is to loop through your config and find if your curr amount is less then config amount:
$config = [
[
'amount' => 10,
'reward' => 1,
],
[
'amount' => 20,
'reward' => 2,
],
[
'amount' => 30,
'reward' => 3,
],
];
to loop through you can use foreach
when the condition is true there is no reason to loop more - so just break
try yourself before opening the demo
I am trying to get the number of unique Brands from my Products Table with their count from a Laravel collection.
I was able to do that using a specific query for the products but the reason i'm using a collection now is because i also want to get the Product Origins (Country), Conditions (Used / New) of the products and i thought it would be much better to use a collection from one query rather than have three separate queries for each data.
The code below works but it doesn't show the count for each unique Brand.
Here is the Table
Here is my Controller
$products = DB::table('products')
->select('products.*')
->whereNull('products.deleted_at')
->get();
$BrandCollection = collect($products);
$Brands = $BrandCollection->unique('Brand')->sortBy('Brand')->keyBy('Brand')->pluck('Brand');
So, the result i'm looking for is
HP 3
Toshiba 2
Lenovo 1
I thought it could be done using concat for the collection but since i'm on Laravel 5.2, i'm looking for other solutions.
If you really want to use collections (not Eloquent) you can do it like this:
$brandsWithCount = $BrandCollection->groupBy('Brand')->map(function($values) {
return $values->count();
})->sort()->reverse();
For example if you set $brandCollection like this:
$BrandCollection = collect([
['Brand' => 'HP'],
['Brand' => 'HP'],
['Brand' => 'HP'],
['Brand' => 'Toshiba'],
['Brand' => 'Toshiba'],
['Brand' => 'Lenovo'],
]);
result will be:
Collection {#372
#items: array:3 [
"HP" => 3
"Toshiba" => 2
"Lenovo" => 1
]
}
as expected.
There is a Collection Helper called CountBy, does exactly what you need.
Collections CountBy
$BrandCollection->countBy('Brand');
It will retourn as expected
#items: array:3 [
"HP" => 3
"Toshiba" => 2
"Lenovo" => 1
]
Simple :D
The Problem:
I have a cakephp 3.x query object with two nested associations, Organizations.Positions.Skills, that is being set to a view variable, $Organizations. I'm trying to sort the query's resulting top level array by a column in the first nested association. That is, I want to sort $Organizations by a column in Positions, specifically Positions.Ended).
public function index()
{
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
);
}
Model Info
Organizations has many Positions
Positions has many Skills
Research I've Done
order option
According to the cookbook find() has an order option: find()->order(['field']); or find('all', ['order'=>'field DESC']);
However, this only applies to fields in the table find() is being called upon. In this case, Organizations. For example, this is how it's typically used.
//This works.
//Sorts Organizations by the name of the organization in DESC.
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
->order(['organization DESC'])
);
but, trying to use it for nested associations doesn't work:
$this->set('Organizations',
this->Organizations->find(...)
->contain(...)
->order(['Organizations.Positions.ended DESC'])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Organizations.Positions.ended' in 'order clause'
and altering it to refer to the field that'll be nested doesn't work either:
//also doesn't work.
$this->set('Organizations',
$this->Organizations->find(...)
->contain(...)
->order([Positions.ended DESC])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Positions.ended' in 'order clause'
In both cases, the sql error is created when cakephp executes the PDO statement generated by the query.
sort option
Similarly, according to the cookbook, eager loading / associations has the 'sort' option:
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended'=>'DESC']
]
])
);
But, this only sorts the nested association.Specifically, it sorts the associations that are nested. It does not sort the entire resulting set by the nested association (ergo, a multidimensional sort).
For example:
The Organization, Organization C (org id 1), has two positions:
Position 5. Ended 2012
Position 4. Ended 2014
And the Organization, Organization B (org id 2), has two positions:
Position 3 Ended 2013
Position 2 Ended 2015
The above code and data results in the following array being generated when the query is evaluated:
Organizations => [
0 => [
'organization' => 'Organization A',
positions => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
1 => [
'organization' => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
],
2 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
]
]
other research
Likewise the following stackoverflow questions came up in my research:
http://stackoverflow.com/questions/26859700/cakephp-order-not-working
http://stackoverflow.com/questions/17670986/order-by-doesnt-work-with-custom-model-find-while-paginating
http://stackoverflow.com/questions/18958410/cakephp-paginate-and-sort-2nd-level-association
http://stackoverflow.com/questions/34705257/cakephp-paginate-and-sort-hasmany-association
Furthermore, I do know that PHP has its own sorting functions like sort() and multisort(); but, those can only be called once the query has been evaluated (by foreach). Alternatively, there's calling $organization->toArray() then using multisort; but, this would have to be done in the view, would break the MVC convention of separations of concerns (data and queries are manipulated by the controller and model, not the view!), and would be quite inefficient as it'll be called while the page is loading.
How then, do I sort a cakephp query by its nested associations?
Or, put more simply, how do I order/sort the query to produce the following array upon evaluation:
Organizations => [
0 => [
'organization' => 'Organization A',
'positions' => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
0 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
],
1 => [
'organization => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
]
]
Background & Context:
I'm building a [portfolio website][7] for myself with cakephp 3.2 to showcase my web dev skills and assist in my quest for a dev career. For my resume page, I'm organizing the massive amount of data with nested accordions to mimic the resume style recruiters would expect to see on an actual resume. As a result, my view does the following:
Looping through the top level view variable (Organizations)
Rendering the organization details
Looping through that organization's positions (still inside 1)
render the position details
loop through the position's relevant skills
render each skill w/ the appropriate link to filter by that skill.
List item
Only hasOne and belongsTo associations are being retrieved via a join on the main query. hasMany associations are being retrieved in a separate queries, hence the errors when you try to refer to a field of Positions.
What you want should be fairly easy to solve, on SQL level, as well as on PHP level.
SQL level
On SQL level you could join in Positions, and order by a computed max(Positions.ended) column, like:
$this->Organizations
->find('all')
->select(['max_ended' => $this->Organizations->query()->func()->max('Positions.ended')])
->select($this->Organizations)
->contain([
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended' => 'DESC']
]
])
->leftJoinWith('Positions')
->order([
'max_ended' => 'DESC'
])
->group('Organizations.id');
And that's all, that should give you the results that you want. The query will look something like:
SELECT
MAX(Positions.ended) AS max_ended,
...
FROM
organizations Organizations
LEFT JOIN
positions Positions
ON Organizations.id = (
Positions.organization_id
)
GROUP BY
Organizations.id
ORDER BY
max_ended DESC
PHP level
On PHP level it's also pretty easy to solve to using collections (note that queries are collections - kind of), however it would only make sense if you'd intend to retrieve all rows, or must deal with a set of unordered rows for whatever reason... something like:
$Organizations->map(function ($organization) {
$organization['positions'] =
collection($organization['positions'])->sortBy('ended')->toList();
return $organization;
});
$sorted = $sorted->sortBy(function ($organization) {
return
isset($organization['positions'][0]['ended']) ?
$organization['positions'][0]['ended'] : 0;
});
This could also be implemented in a result formatter, so things happen on controller or model level if you insist.
$query->formatResults(function ($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
$results = $results->map(function ($row) {
$row['positions'] =
collection($row['positions'])->sortBy('ended')->toList();
return $row;
});
return $results->sortBy(function ($row) {
return isset($row['positions'][0]['ended']) ?
$row['positions'][0]['ended'] : 0;
});
});
See
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Queries Are Collection Objects
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Collections > Sorting
This worked for me:
$this->Organizations->find('all')
->contain(['Positions' => ['sort' => ['Positions.ended' => 'DESC']]])
->contain('Postions.Skills');
I have the following method in my user class:
/**
* Get all organisations for user (if owner)
*
* #param
*/
public function getOrganisationsOwned()
{
// If the user is owner of any one or many organisations then return this list
return Organisation::leftJoin('subscription_plans', 'organisations.subscription_plan_id', '=', 'subscription_plans.id')
->where('organisations.owner_id', '=', $this->id)
->select('organisations.*', 'subscription_plans.*')
->get();
}
The method essentially queries and joins two tables. Each table has a column called title.
The output from the above generates the rows as desired with the right info, but returns only one title column, from the right table (subscription_plans) but not the column title from the left table (organisations). I also notice it is dropping the timestamps from one table also, as these are of the same column name.
I understood that
->select('organisations.*', 'subscription_plans.*')
would make the query return both columns. What am I missing? Happy new year!
PS: below is a copy of the dd() contents for the collection, with title only appearing once.
#attributes: array:44 [▼
"id" => 1
"title" => "Monthly Subscription"
"address_1" => "34 Florence Street"
"address_2" => ""
"suburb" => "Hornsby"
"state" => "NSW"
"postcode" => "2077"
"country_id" => 12
"currency_id" => 12
"time_zone_id" => 109
"phone" => "0392144497"
"website" => "http://www.Tremblay.com/est-aspernatur-et-ut-provident.html"
"business_id" => "82297955560"
"tax_registration" => 1
"logo" => "8aa656de-2bc2-4e14-dddd-e02fbcd2b76f"
"quote_terms_days" => 14
"invoice_terms_days" => 14
"fiscal_start_id" => 7
"industry_id" => 4
"company_size_id" => 3
"date_format_id" => 2
"date_time_format_id" => 20
"owner_id" => 1
"gateway_id" => "1"
"gateway_username" => "xxx"
"gateway_password" => "xxx"
"gateway_signature" => "xxx"
"gateway_accepted_cards" => "[1, 2, 3]"
"subscription_plan_id" => 1
"trial_ends_at" => "2015-11-07"
"grace_ends_at" => "2016-02-10"
"subscription_ends_at" => "2016-01-11"
"latitude" => "-33.70433500"
"longitude" => "151.10161900"
"registration" => "done"
"deleted_at" => null
"created_at" => "2016-01-01 14:59:47"
"updated_at" => "2016-01-01 14:59:47"
"amount" => "9.09"
"gst" => "0.91"
"gst_amount" => "10.00"
"billing_cycle" => "MONTH"
"trial_period_days" => 30
"grace_period_days" => 30
]
The "missing" title column contains:
'title' => 'ABC Electrical'
There is some misunderstanding as to what I suggested: instead of using *, you could list the field names one by one and provide aliases for the 2 title fields. This does not mean that you should keep the 'organisations.*', 'subscription_plans.*' and add the 2 title fields to the select list with aliases because this way you select both title fields twice, wasting memory and processor time.
You should not include the * forms in the select list, but list each field individually, with the 2 title fields marked with aliases:
public function getOrganisationsOwned()
{
// If the user is owner of any one or many organisations then return this list
return Organisation::leftJoin('subscription_plans', 'organisations.subscription_plan_id', '=', 'subscription_plans.id')
->where('organisations.owner_id', '=', $this->id)
->select('organisations.id', 'organisations.title AS org_title', ..., 'subscription_plans.subscription_plan_id', 'subscription_plans.title AS plan_title', ...)
->get();
}
Yeah, I know, listing so many field one by one is a pain in the ***, however, each field is retrieved once and only once, at it is clear that you are fetching what is needed.
#Shadow's suggestion worked, although you should note, this method allows you to select all the fields, but only "rename" columns or rather alias them so you can still access the proper value when using joins. The old value will still be overridden, but now you can use your alias with the correct value.
The below is now working:
public function getOrganisationsOwned()
{
// If the user is owner of any one or many organisations then return this list
return Organisation::leftJoin('subscription_plans', 'organisations.subscription_plan_id', '=', 'subscription_plans.id')
->where('organisations.owner_id', '=', $this->id)
->select('organisations.*', 'organisations.title AS org_title', 'subscription_plans.*', 'subscription_plans.title AS plan_title')
->get();
}