Querying a table with an array of json objects in RedbeanPHP - php

I'm having trouble working in Redbean PHP with querying a table with an array of json objects in a single field, and producing a report on it.
I want to get a report with subtotals of all staff with notes by the category. I know this should be easy/obvious but I'm just not getting it properly.
I have a database, with:
table clients
with columns:(int) client_id, (string) client_name, (array of json) notes
notes is an array of json with
(int) note_id, (int) note_category_id, (int) staff_id, (string) description, (memo) content, (date) note_date
table staff with columns (int) sid, (string) sname
table categories with columns (int) cat_id, (string) cat_name
So in pseudocode (since I'm still trying to figure this all out)
I need to run a query like: (with parameters in brackets)
R::getAll('Select * from Join (staff, categories, clients)
On (staff.sid=clients.services.staff_id, categories.cat_id=clients.services.note_category_id)
Where (clients.services.note_date Between [startdate] and [enddate],
categories.cat_name IN [chosencateg], staff.sname IN [pickednames])
Orderby sname Asc, cat_name Asc, note_date Desc ');
report output format:
Filters used: [picked filter choices if any]
-----------
[sname]
-- note category: [cat_name] 1
[note_date] 1 [description] 1 [content] 1
[note_date] 2 [description] 2 [content] 2
note category 1 subtotal
-- note category: [cat_name] 2
[note_date] 3 [description] 3 [content] 3
[note_date] 4 [description] 4 [content] 4
note category 2 subtotal
staff subtotal
[sname] 2 ...
I'm asking a fairly generic one because I'll have to work with a number of similar tables, and maybe seeing a query template will help my understanding.
Thanks for any help.

redbean is fantastic and - getAll is just scratching the surface and truly isn't working with redbean at all really... Read up on it here:
Here's a query template to get you started:
Query Template:
1)
R::getAll('Select * from Join (staff, categories, clients)
On (staff.sid=clients.services.staff_id, categories.cat_id=clients.services.note_category_id)
Where (clients.services.note_date Between :startdate and :enddate,
categories.cat_name IN (:chosencateg), staff.sname IN (:pickednames))
Orderby sname Asc, cat_name Asc, note_date Desc ');
You could also simply use:
2)
R::getAll('Select * from Join (staff, categories, clients)
On (staff.sid=clients.services.staff_id, categories.cat_id=clients.services.note_category_id)
Where (clients.services.note_date Between ? and ?,
categories.cat_name IN (?), staff.sname IN (?))
Orderby sname Asc, cat_name Asc, note_date Desc ');
The only difference is that query template 1 uses named parameters (so it's going to look to the array of params that you pass it to contain an associative array with parameters named in the same way as they are in the query). While template 2 requires simply an array of parameters with the indexes lined up with the order in which the ? marks appear in your query.
Anyway... the query should return an associative array of your columns representing rows. a var_dump would look something like this:
Array
(
[client_id] => 1,
[client_name] => "joe",
[noes] => "[
{note_id=1
,note_category_id=1
,staff_id=1
,description=blah blah
,content=blah blah blah blah
,content=some content for this note
,note_date=12/06/2018
}
]"
[sid] => 100,
[sname] => "some staff name"
[cat_id] => 100
[cat_name] => "some category name"
)
Notice how the notes field has just come out as a string (I know the json above is not properly formed json, I'm just trying to show an example).
I assume that what you want is to have that string converted into an array so you can work with it as if it were data and not a string. So the below should get you started with that:
Once you have it out of the database you should be able to access it like this:
$result = R::getAll($query,
['startdate'=> $mystartDate
,'enddate' => $myEndDate
,'chosencateg'=>$myChosenCategory
,'pickednames'=>$myPickedNames
]);
// this would output the json string to your screen
echo $result['notes'];
but it seems like you want to work with the json as if it were part of your data - so... you would need to decode it first.
// decode my notes field:
foreach(array_key($result) as $key) {
/* you are working with a multidimensional array in this loop
, use $key to access the row index. Each row index
will contain named column indexes that are column names from the database
*/
$result[$key]['decoded_notes'] = json_decode($result[$key]['notes'],true);
}
// I now have a new column in each row index, containing 'notes'
as another associative array
// the statement below now results in an array to string conversion error:
echo $result[someIndexNumber]['decoded_notes'];

So, I decided I would want this in MySQL (5.7) so as to use its capabilities. To do this I used string manipulation. MySQL 8 adds json_table functions which would have been nice to have.
I converted each array of JSON notes into lines of 'INSERT INTO temptable' to convert the array list into temptable rows,
one JSON object per row, adding the client_id to each object, then
EXECUTEing those statements.
SET #allnotes = (
SELECT json_arrayagg(REPLACE(`notes`,'{', CONCAT('{"id_client": ', id_client, ', '))) /* add id_client to each note object */
FROM clients
WHERE `notes` != '' AND `notes` != '[]' ); /* no empty note cases */
SET #allnotes = REPLACE(REPLACE(#allnotes ,'"[',''),']"','' ); /* flatten out outer array of each note */
SET #allnotes = REPLACE(REPLACE(#allnotes ,'{','("{'),'}','}")' ); /* INSERT INTO string formatting for the objects */
DROP TEMPORARY TABLE IF EXISTS jsonTemporary;
CREATE TEMPORARY TABLE IF NOT EXISTS jsonTemporary (anote json);
SET #allnotes = REPLACE(REPLACE(#allnotes,'[','INSERT INTO jsonTemporary (anote) VALUES '),']',';');
PREPARE astatement FROM #allnotes;
EXECUTE astatement;
/* totals */
SELECT concat(staff.last_name,", ",staff.first_name) AS sname,
categories.name AS cat_name,
count(anote->'$.id_client') AS cat_total,
FROM jsonTemporary
JOIN categories ON cast(anote->'$.note_category_id' as unsigned)=categories.id
JOIN clients ON clients.id_client=anote->'$.id_client'
JOIN staff ON staff.id=anote->'$.staff_id'
WHERE anote->'$.note_date' >= "2018-10-01" AND anote->'$.note_date' <= "2018-12-31"
GROUP BY sname, cat_name;
/* all notes */
SELECT concat(staff.last_name,", ",staff.first_name) AS sname,
categories.name AS cat_name,
anote->'$.note_date' AS n_date,
anote->'$.description' AS description,
anote->'$.content' AS content,
FROM jsonTemporary
JOIN categories ON cast(anote->'$.note_category_id' as unsigned)=categories.id
JOIN clients ON clients.id_client=anote->'$.id_client'
JOIN staff ON staff.id=anote->'$.staff_id'
WHERE anote->'$.note_date' >= "2018-10-01" AND anote->'$.note_date' <= "2018-12-31"
GROUP BY sname, cat_name;
DROP TEMPORARY TABLE IF EXISTS jsonTemporary;

Related

sql query takes ages to complete

I have two relational tables of which i both need data. The first (Main) table contains about 90k rows. The secondary table contains about 200k plus rows.
I need to add some data of the secondary table to the results i get from the first table currently i do this in two parts:
First: i select the required rows from the Main table this goes super fast.
$records = getData($conn, "
SELECT id
from Main
where contains_tags is not null
and contains_mediums is not null
and contains_techniques is not null
limit 100
");
-
Then i go over each record to add the extra data from the secondary table but this takes ages (1 min for 100 records 50min - 100 min for 5k records. In the end i need to run this query for about 5-10k rows of the Main table). Is there a way to speed this process up?
foreach ($records as $r => $record) {
$records[$r]['mediums'] = getData($conn, "SELECT medium from mediums where kleding_id = ".$record['id']."");
kleding_id = ".$record['id']."");
}
This is the desired output after both queries finish:
[1] => Array
(
[id] => 15
[mediums] => Array
(
[0] => Array
(
[medium] => wol
)
[1] => Array
(
[medium] => katoen
)
)
)
So my question is how to run this query structure efficiently in as little as time as possible.
the Main table look like this:
the Secondary table looks like this:
If anything is unclear let me know so i can clarify.
If you need improve performance
be sure you have a proper index table mediums column kleding_id
CREATE INDEX my_index ON mediums (kleding_id);
remember that limit (for not recent db version) normally work on the result ..a and not break after the first 100 is reached
You can do it in a single query. But it's for mysql Verison 5.7 & above.
SELECT id, GROUP_CONCAT(json_object('medium', mediums.medium))
FROM Main
LEFT JOIN mediums ON Main.id = mediums.kleding_id
WHERE contains_tags IS NOT NULL AND contains_mediums IS NOT NULL AND contains_techniques IS NOT NULL
GROUP BY Main.id
LIMIT 100
Also make sure you have a index on kleding_id.
There is no need to iterate over the first result and call the 2nd table multiple times. This will always be slow.
Try inner join to aggregate data from both tables in one sql statement.
SELECT
Main.id,
mediums.medium
from
Main
inner join mediums on
(
Main.id = mediums.kleding_id
)
where
contains_tags is not null
and contains_mediums is not null
and contains_techniques is not null limit 100;

Select one value of one colum of an array obtained with pdo

I have the following code :
$foo = $bdd->prepare($qry);
$foo->execute();
$result = $foo->fetchAll();
In $qry, i have a SELECT with a JOIN between tables beatles b et status s (on the columns status_id), such that the result is the following :
Code :
b.id b.firstname b.lastname b.status_id s.status_id s.status
0 John Lennon 0 0 Mort
1 Paul McCartney 1 1 Vivant
2 Ringo Starr 1 1 Vivant
3 George Harrison 0 0 Mort
(The first line is the columns' names in the tables, it isn't inside the result of the query)
I want to select in php only the s.status of ringo starr, for instance. How can I do that ?
Thanks
If you want to retrieve the value of status for Ringo Starr, but need the values of the other fields within that page, then (assuming the array has come back indexed by id), you could access it with $result[2]['status']. Otherwise, change your select statement to just select the values of id and status.
As a side note, in your SELECT statement you'll need to give aliases potentially for the b.status_id and s.status_id; when you join, the column names are taken and the table names basically ignored in terms of what is returned - so something like b.status_id AS b_status_id, s.status_id AS s_status_id would make sure you got the correct values for both of these. You may have done this already of course - it just looked as though you might have not, judging from the column names you gave.
I am not sure if you want to just pick one column from the existing result or issue a query that will get you only what you want. But a better defined query sound the most sensible
SELECT
[ your joins etc ]
WHERE b.firstname = 'Ringo'
AND b.lastname = 'Starr'
Or if you want to just get the Ringo Starr entry from your existing result
foreach ( $resultats as $result ) {
if ( $result['firstname'] == 'Ringo' && $result['lastname'] == 'Starr' ) {
echo 'Got him';
}
}
add WHERE clause with condition that satisfy your needs
alter fields clause leaving only one you need
use fetchColumn() instead of fetchAll()
You're set.

I am getting 2 rows returned when i'm really expecting one merged row, MySQL and INNER JOIN

I have the following SQL:
$queryString = "
SELECT
c.id,
c.object,
c.ref,
c.title,
o.value
FROM
organisations c
INNER JOIN
organisationType oT ON
oT.ref = c.ref
INNER JOIN
options o ON
oT.value = o.ref
WHERE c.ref = '585567'
ORDER BY
title
";
My organisations table contains information about an organisation (The following is one organisation)
id, dateModified, object, ref, title
2312, 2013-04-22 17:34:55,
{"__attributes":{"item_id":6868447,"external_id":..., 585567, Test
organisation
My organisationType table contains multiple entries for one organisation as an organisation can be a number of different types. (The following is 2 types assigned to the organisation above)
id, dateModified, relativeTable, ref, value
5, 2013-04-22 17:34:55, organisations, 585567, 3
6, 2013-04-22 17:34:55, organisations, 585567, 2
Finally my options table contains one row for each type that can be for reference:
id, dateModified, relativeTable, ref, value
1, 2013-04-22 16:36:59, organisations, 2, Client
2, 2013-04-22 16:36:59, organisations, 3, Partner
3, 2013-04-22 16:38:27, organisations, 4, Vendor
I would like to be able to return one organisation with its type embedded onto the end (o.value)
so my result might be:
[1] => stdClass Object
(
[id] => 2312
[object] => ""
[ref] => "585567"
[title] => "Test organisation"
[type] => array( [0] => "Client", [1] => "Vendor" )
)
With my current SQL, I am getting back an array of 2 rows with a duplicate of all components of the row apart from the type which has changed. It's fine, but it's not perfect.
You can't really bring back a sub array directly like that in SQL. You can bring back a concatenated field of all the values though. Something like this:-
$queryString = "
SELECT c.id,
c.object,
c.ref,
c.title,
GROUP_CONCAT(o.value) AS type
FROM organisations c
INNER JOIN organisationType oT ON oT.ref = c.ref
INNER JOIN options o ON oT.value = o.ref
WHERE c.ref = '585567'
GROUP BY c.id, c.object, c.ref, c.title
ORDER BY title
";
afaik. it is impossible by mysql to return an array as the field value. you can do it by your own code, adding "order by 1,2,3,4" to your sql expression and collecing an array in the fetch loop, comparing 1-4 columns with previous row.

Exploding data from mysql using PHP

I have a database table where I am storing all the values from an external xml file. Since my project requirement demands me to deal with this unnormalized data. I need to help to extract data in an appropriate way.
I have two web pages (one for categories) and one for products.My database table looks like this:
**product_id Code Name ProductRange ProductSubRange WebCategory**
1 1002 Lid 30l Crystal;Uni LIDs Household products
2 10433 Casa Basket Silver Casa Casa Hipster BASKET Kitchenware
3 17443 Casa Basket Ice White Casa;Laundry LAUNDRY BASKET Laundry products
4 13443 Garden tub Eden Eden Garden Pots Household products
5 199990 Black Lid 201 Crystal Crystal Lids Household products
The product that belong to more than one productRange is indicated my semicolon(;). For example,above product_id 1 with name "Lid 301" belongs to two Product Ranges "Crystal" and "Uni". Same is for product_id 3. However product 2 belongs to single ProductRange.
MY QUESTIONs:
1) How can I construct a query so that it could return "ProductRange" based on my query_string values of "Webcategory"? For example:
if I get "Household Products" as my WebCategory from query string, it could give me distinct like this:
Household Products
|-Crystal
|-Uni
|-Eden
Laundry Products
|-Casa
|-Laundry
Kitchenware
|-Casa
2) Based on extracted ProductRanges, I want to display products separately in my webpages according to the product range and webcategory. For example, if I choose "Crystal" from above, it could give me Products with product_id "1" and "5" respectively like this:
Household Products|
|-Crystal
|-Lid 301 (product_id=1)
|-Balck Lid 201 (product_id=5)
|-Uni
|-Lid 301 (product_id=1)
|-Eden
|-Garden Tub
Kitchenware|
|-Casa
|-Casa Basket silver
Laundry Products|
|-Casa
|-Casa Basket Ice White
|
|-Laundry
|-Casa Basket Ice White
Can anyone guide me how can I retrieve records from the database and what I will need to do as I am new to programming? I would appreciate if anyone could help me in this regard.
In order to get distinct product ranges based on a give WebCategory input = 'XYZ', you can use the following - don't be intimidated by the numberstable, it's just a helpful table that contains rows each with increasing integer values from 1 ... up to N where N is the maximum number of characters in your ProductRange column. These can be made by hand or using a special insert/select query like the one found here:
http://www.experts-exchange.com/Database/MySQL/A_3573-A-MySQL-Tidbit-Quick-Numbers-Table-Generation.html
SELECT DISTINCT
SUBSTRING(ProductRange FROM number FOR LOCATE(';', ProductRange, number) - number) AS ProductRange
FROM (
SELECT ProductRange, CASE number WHEN 1 THEN 1 ELSE number + 1 END number
FROM (
SELECT mydatabasetable.ProductRange, numberstable.number
FROM mydatabasetable
INNER JOIN numberstable
ON numberstable.number >= 1
AND numberstable.number <= CHAR_LENGTH(mydatabasetable.ProductRange)
WHERE WebCategory = 'XYZ'
) TT
WHERE number = 1 OR (number + 1) <= CHAR_LENGTH(ProductRange)
) TT
WHERE SUBSTRING(ProductRange FROM number FOR 1) = ';'
OR numberstable.number = 1;
In order to retrieve a result set with all values WebCategory, ProductRange and Product for your website you can use the below slightly modified version derived from the above query. So that the results will appear more meaningful at first, I added an ORDER BY clause to keep all same-category, same-product-range products in sequence one after the other. This might or might not be desired as you might prefer to do that in your application/server-script code. In that case you can remove the ORDER BY clause without doing any harm.
SELECT WebCategory,
SUBSTRING(
ProductRange
FROM number
FOR LOCATE(';', ProductRange, number) - number
) AS ProductRange,
Product
FROM (
SELECT WebCategory, ProductRange, Product,
CASE number
WHEN 1 THEN 1
ELSE number + 1
END number
FROM (
SELECT WebCategory, ProductRange, Product, numberstable.number
FROM mydatabasetable
INNER JOIN numberstable
ON numberstable.number >= 1
AND numberstable.number <= CHAR_LENGTH(ProductRange)
) TT
WHERE number = 1 OR (number + 1) <= CHAR_LENGTH(ProductRange)
) TT
WHERE SUBSTRING(ProductRange FROM number FOR 1) = ';'
OR numberstable.number = 1
ORDER BY WebCategory, ProductRange, Product
You are probably going to want to do a GROUP BY clause in your query and maybe an JOIN if the detailed data is in a different table. If I understand you correctly it would look something like this.
SELECT T.WebCategory, T.ProductRange, T2.Product FROM table T
INNER JOIN table2 T2 ON T2.ProductRange = T.ProductRange
WHERE T.WebCategory = 'Household products'
GROUP BY T.WebCategory, T.ProductRange, T2.Product
It is tough to test my query without having a database setup to test against, but something like the above should return what you are looking for. You will of course need to rename your columns based on the actual names in the second table. Overall though, this should get you started if I understood the question correctly.

MYSQL sort function, depending on the arithmetic operation using the database field values as array keys

I have table of products, and there are 2 fields: price, currency. Possible values in currency fields are 1,2,3. 1 for MDL, 2 for USD, and 3 for EUR. I have to sort my products by price in mdl currency. So i have an array with rates:
$v = array(1,11.38,15.8);
Help my please with my query, I've tried something like this, but I've got errors:
$results = $this->Product
->query("SELECT `id`,`price`,`currency` FROM products
ORDER BY price*$v[currency] DESC");
Hmm... I`ll try to explain, through an example.
My table:
id|price|currency
_________________
1 | 500 | 2
2 | 300 | 3
It shows that first products price is saved in USD, and second products price is saved in EUR. But i have to sort them in MDL valute. So i get an array of rates for each value:
$rates = array([1] = > 1, [2] => 11.50, [3] => 15.50);
So i have to order my products by the result of formula: price*value rate
in first case: 500*$rates['currency value from db, in our case 2] = 500 * 11.50 etc.
Thanks in advance.
Because of the extended example on this problem I have edited this query.
Lets assume that the currencies are alse stored in some table, lets say currency (if not, it should be anyway).
Table currency should be as follows:
ID VALUE CODE
-----------------------------
1 1 USD
2 11.38 EUR
3 15.8 MDL
Then the query should be:
SELECT p.`id`, p.`price`, p.`price` * c.`value` AS 'ratio'
FROM products p
LEFT JOIN currency c ON c.`id` = p.`currency`
ORDER BY `ratio` DESC
By this query You select the currency value from the table currency depending on the currency ID from products table and finaly the results are ordered by the ration price * currency value.
I understand that maybe You have the currencies only hardcoded as array within some config, but it really would be better to put the currencies into the DB (if it is not).
You can`t use data base column name as array keys, because mysql is later instance than php. In php you simply generate query string that is passed to database managment system.
Your query should look like this:
$results = $this->Product->query
(
"SELECT `id`,`price`,
CASE `currency`
WHEN '1' THEN $v[0]
WHEN '2' THEN $v[1]
WHEN '3' THEN $v[2]
END AS 'ratio'
FROM products
ORDER BY price*ratio DESC
"
);

Categories