How do I elegantly retrieve DB data with a one-to-many? - php

I'm querying against a database to retrieve some information that includes a one to many relationship. Consider:
CREATE TABLE `movies` (
`id` int(10) unsigned NOT NULL auto_increment,
`title` varchar(50) NOT NULL,
`desc` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `movies` VALUES ('1', 'The Princess Bride', 'A fantastic film of epic adventure, action, and love.');
CREATE TABLE `showtimes` (
`movie_id` int(10) unsigned NOT NULL,
`showtime_id` int(10) unsigned NOT NULL auto_increment,
`starttime` timestamp NOT NULL,
PRIMARY KEY (`movie_id`,`showtime_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `showtimes` VALUES ('1', '1', '2011-09-19 20:00:00'), ('1', '2', '2011-09-19 23:00:00'), ('1', '3', '2011-09-20 13:00:00');
The way I'd like to receive the information is something like this.
$movies[1] = array(
'id' => 1,
'title' => 'The Princess Bride',
'desc' => 'A fantastic film of epic adventure, action, and love.',
'showtimes' => array(
'1' => '2011-09-19 20:00:00',
'2' => '2011-09-19 23:00:00',
'3' => '2011-09-20 13:00:00'));
This seems to be the most sensible way for me to go through the data. If I'm printing all the showtimes for the theatre, I can do something simple like:
foreach($movies as $movie)
{
//pretend there's style stuff here
echo $movie['title'] . "&mdash" . $movie['desc'];
foreach($movie['showtime'] as $time)
{
if ($time > $_SERVER['REQUEST_TIME'])
{
echo $time;
}
}
}
Unfortunately, that's not what I get back from a standard query. Something like:
SELECT * FROM `movies` INNER JOIN `showtimes` ON `movies`.`id` = `showtimes`.`movie_id`;
Yields:
$movies[1] = array('id' => 1, 'title' => 'The Princess Bride', 'desc' => 'A fantastic film of epic adventure, action, and love.', 'starttime' => '2011-09-19 20:00:00');
$movies[2] = array('id' => 1, 'title' => 'The Princess Bride', 'desc' => 'A fantastic film of epic adventure, action, and love.', 'starttime' => '2011-09-19 23:00:00');
$movies[3] = array('id' => 1, 'title' => 'The Princess Bride', 'desc' => 'A fantastic film of epic adventure, action, and love.', 'starttime' => '2011-09-20 13:00:00');
which isn't quite what I'm going for. In larger result sets with more data I'm also mildly curious about the effect of returning so much duplicated data (consider wider rows with tens of joins).
I know I can use a construct like GROUP_CONCAT() to append those showtimes together, but I'm left splitting them apart later. For simple things like timestamps it's easy as I can choose a delimiter that wont appear in that format, if I was splitting reviews for example it would be a bit tougher.
I can do something lame like iterate over all the movies, querying showtimes from within that loop, but there's no way that will ever be web scale.
I can execute the join in-query, then iterate over those results and append on duplicate primary key, but that seems to lack elegance as well.
The Question
Is there an elegant way to get what I'm looking for? On this particular application I'm using Zend Framework, if it's built into that (or another framework) that would be pretty keen.
thanks

Do some kind of join (your choice) on the two tables and then loop through the results.
$query = "SELECT * FROM movies LEFT JOIN showtimes ON showtimes.movie_id = movies.id";
$result = $db->query($query);
while($row = $result->fetch()){
if(isset($movies[$row['id']])
$movies[$row['id']]['showtimes'][] = $row['starttime'];
else{
$movies[$row['id']] = array(
'id'=>$row['id'],
'title'=>$row['title'],
'desc'=>$row['desc'],
'showtimes'=>array($row['starttime'])
);
}
}

Related

SQL Query using INSERT INTO...SELECT and ON DUPLICATE UPDATE (codeigniter 3.x)

I'm currently creating a little tool for logs and i have an hard time working with mysql tables.
Here are my two tables:
CREATE TABLE site_logs
(
id int auto_increment,
level VARCHAR(45),
severity varchar(45),
uri varchar(255),
message VARCHAR(255),
trace varchar(255),
device varchar(45),
os varchar(45),
browser varchar(45),
ip varchar(45),
created_ at datetime
);
CREATE TABLE site_logs_stats
(
id int auto_increment,
log_id int,
date datetime,
count int,
);
Here is the situation: Every hour i parse my logs file and retrieve an $result array where i got each error and the number of occurences during the hour.
Next I insert the values in a table like this :
$result = array_values($result);
foreach ($result as $res) {
$this->db->insert('site_logs',array(
'level' => $res['level'],
'severity' => $res['severity'],
'uri' => $res['uri'],
'message' => $res['short_message'],
'trace' => $res['short_trace'],
'device' => $res['device'],
'os' => $res['os'],
'browser' => $res['browser'],
'ip' => $res['ip']
));
What I want to do:
In the site_logs table i only want unique errors (if there is the same error a 14:00 and 16:00 i want it to be writen one time in this table). If the error got the same level, severity, uri and trace then i considere this is the same error.
In the second table i the history of each unique error ex: if the same error appears at 14:00 and at 16:00 i want to have the site_logs.log_id 2 times, the hours they're writen and the number of occurence during the slot ( ex 14:00 -> 15:00 and 15:00-> 16:00).
I tried a lot of different queries but failed.
EDIT:
This is now how i insert datas in site_logs table:
$result = array_values($result);
foreach ($result as $res) {
$this->db->replace('site_logs',array(
'level' => $res['level'],
'severity' => $res['severity'],
'uri' => $res['uri'],
'message' => $res['short_message'],
'trace' => $res['short_trace'],
));
with unique key on ID, it seems to work pretty well, i've deleted other fields for an easier sorting and avoid duplicated except by ip/ browser etc...
So now i work on the second part where i must insert site_logs.id in site_log_stats and count occurence each hours. (i'm thinking about 'last_insert' or something like this.

PHP MySQL Duplicate entry exception - InsertMulti with OnDuplicate

I am using ThingEngineer/PHP-MySQLi-Database-Class and I am trying to perform a multiple insert while using OnDuplicate. The goal is to insert a new product record if the 'sku' does not already exist. If the 'sku' does exist then the 'name' should be updated instead of creating a new entry.
MySQL Schema:
CREATE TABLE `products` (
`product_pk` bigint(9) NOT NULL,
`product_id` int(20) UNSIGNED NOT NULL,
`name` varchar(255) NOT NULL,
`sku` varchar(16) NOT NULL,
`category` int(10) DEFAULT NULL,
`last_update` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `products`
ADD PRIMARY KEY (`product_pk`),
ADD UNIQUE KEY `sku` (`sku`);
ALTER TABLE `products`
MODIFY `product_pk` bigint(9) NOT NULL AUTO_INCREMENT;
PHP:
$sDate = date("Y-m-d H:i:s");
$lastid = $db->rawQuery('SELECT MAX( product_id ) AS max FROM products');
(!$lastid || !isset($lastid[0]['max'])) ? $pid = 0 : $pid = $lastid[0]['max']++;
foreach ($data as $item){
if (isset($item['sku']) && !null == $item['sku']){
$prod[$pid]['product_id'] = $pid;
$prod[$pid]['sku'] = $item['sku'];
$prod[$pid]['name'] = substr($item['product-name'],0,255);
$prod[$pid]['last_update'] = $sDate;
$pid++;
}
}
$utfEncodedArray =encodeArray($prod, 'UTF-8');
$db->onDuplicate('name', 'product_pk');
$db->insertMulti('products', $utfEncodedArray);
function encodeArray($array, $type)
{
foreach($array as $key => $value)
{
if (is_array($value)){ $array[$key] = encodeArray($value, $type);}else{ $array[$key] = mb_convert_encoding($value, $type);}
}
return $array;
}
The error I receive is:
Uncaught mysqli_sql_exception: Duplicate entry 'ABC123' for key 'sku'
Here is a sample of the array $utfEncodedArray used on the insertMulti call:
Array(
[1] => Array
(
[product_id] => 1
[sku] => ABC123
[name] => product1
[last_update] => 2018-09-08 18:55:20
)
[2] => Array
(
[product_id] => 2
[sku] => ABC124
[name] => product2
[last_update] => 2018-09-08 18:55:20
)
)
Steps I have tried so far:
Dropped the 'products' table and created it again. Multiple times.
Tried using 'sku' instead of 'product_pk' in the onDuplicate call.
Tried multiple collation types
Tried using unique key on both 'sku' and 'product_id'
When I attempted this method all entries were inserted correctly but when running it again it generated duplicates instead of updating the existing row. Not sure how this happened seeing as both 'sku' and 'product_id' are unique.
The $prod array currently contains the same values. So every time I run this I would expect to see the 'last_updated' column to be updated every time after the initial inserts.
This is my first experience using onDuplicate and despite hours of searching and reading docs I am still lost. I was trying to let the db class handle the multiple insert from the array but I am not against trying raw queries while iterating over my array of products instead.
Of course as soon as I posted this I find the issue...
Found a fork of the database class which resolved issues with insertMulti while using onDuplicate:
should fix insertMulti() if onDuplicate() is set

SQL to ActiveRecord criteria

I am trying to reproduce the following SQL in an ActiveRecord Criteria:
SELECT COALESCE(price, standardprice) AS price
FROM table1
LEFT JOIN table2
ON (pkTable1= fkTable1)
WHERE pkTable1= 1
So far I have the following:
$price = Table1::model()->find(array(
"select" => "COALESCE(price, standardprice) AS price",
'with' => array(
'table2' => array(
'joinType' => 'LEFT JOIN',
'on' => 'pkTable1= fkTable1',
)
),
'condition' => 'pkTable1=:item_id',
'params' => array(':item_id' => 1)
));
But this results into the following error: 'Active record "Table1" is trying to select an invalid column "COALESCE(price". Note, the column must exist in the table or be an expression with alias.
The column should exist though, here are the 2 table structures:
Table1
pkTable1 int(11) - Primary key
standardprice decimal(11,2)
name varchar(255) //not important here
category varchar(255) //not important here
Table2
pkTable2 int(11) - Primary key //not important here
fkType int(11) - Foreign key //not important here
fkTable1 int(11) - Foreign key, linking to Table1
price decimal(11,2)
What exactly am I doing wrong?
You will need to use a CDbExpression for the COALESCE() expression:
$price=Table1::model()->find(array(
'select'=>array(
new CDbExpression('COALESCE(price, standardprice) AS price'),
),
'with' => array(
'table2' => array(
'joinType'=>'LEFT JOIN',
'on'=>'pkTable1=fkTable1',
),
),
'condition'=>'pkTable1=:item_id',
'params'=>array(':item_id'=>1)
));
I further believe if table2 has been linked in the relations() method in your Table1 model, the following line should be sufficient:
'with'=>array('table2'),
I've managed to solve the issue as following: Wrap the COALESCE expression in an array, and change the alias to an existing columnname in the table.
$price = Table1::model()->find(array(
"select" => array("COALESCE(price, standardprice) AS standardprice"),
'with' => array(
'table2' => array(
'joinType' => 'LEFT JOIN',
'on' => 'pkTable1= fkTable1',
)
),
'condition' => 'pkTable1=:item_id',
'params' => array(':item_id' => 1)
));
Thank you to Willemn Renzema for helping me with the array part. I'm still not entirely sure why the alias needs to be an existing column name (in this case the error was that price doesn't exist in Table1).

Cakephp 2.0: complex find query

I'm trying to pull some info out of two tables linked by the hasMany and belongsTo associations.
requisitions hasMany locations and locations belongsTo requisitions
TABLE `requisitions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`fecha_generacion` date NOT NULL,
`solicitado_a` varchar(60) NOT NULL,
`proyecto` varchar(150) NOT NULL,
`obra_no` varchar(11) NOT NULL,
`observaciones` text NOT NULL,
PRIMARY KEY (`id`)
)
and
TABLE `locations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`requisition_id` int(11) NOT NULL,
`fecha` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`name` enum('pendiente','tecnico','existencia','cotizando','generar_o','archivada')
NOT NULL DEFAULT 'pendiente',
`image_path` varchar(150) NOT NULL DEFAULT 'estado0.png',
`note` text NOT NULL,
PRIMARY KEY (`id`)
)
Requisition goes from one Location to another and I need to keep track of its current Location looking by a given Location as 'pendiente','tecnico'...
So I need to generate a list with the last Location for each Requisition and then filter that list by the Location.name
I believe the only way to do this is with a query around another query, so I'm trying to understand cakephp syntax with more simple queries first.
I was trying to search for the last 'pendiente' Location with the next code from my RequisitionsController.
$lastPendiente = $this->Requisition->Location->find('all', array(
'conditions' => array('Location.name' => 'pendiente'),
'fields' => array('MAX(Location.id) AS olderLocation', 'Location.requisition_id'),
'group' => 'Requisition.id',
));
I have the query
SELECT MAX(`Location`.`id`) AS olderLocation, `Location`.`requisition_id` FROM `petrofil_demo`.`locations` AS `Location` LEFT JOIN `petrofil_demo`.`requisitions` AS `Requisition` ON (`Location`.`requisition_id` = `Requisition`.`id`) WHERE `Location`.`name` = 'pendiente' GROUP BY `Requisition`.`id`
output...
array(
(int) 0 => array(
(int) 0 => array(
'olderLocation' => '22'
),
'Location' => array(
'requisition_id' => '29'
)
),
(int) 1 => array(
(int) 0 => array(
'olderLocation' => '5'
),
'Location' => array(
'requisition_id' => '30'
)
),
(int) 2 => array(
(int) 0 => array(
'olderLocation' => '13'
),
'Location' => array(
'requisition_id' => '31'
)
)
)
...which is great because those are exactly the last requisitions with a 'pendiente' location but here comes the second query or the condition where I'm clueless. I need to be sure my requisition last state was 'pendiente' and not another possible locations. For example my requisition_id =>30 last location is really 'tecnico' so I need to find a way to exclude it from showing on my results.
You could quit the condition from the query, put the 'name' column in the 'fields' and add an order by sentence:
$lastPendiente = $this->Requisition->Location->find('all', array(
'fields' => array('MAX(Location.id) AS olderLocation', 'Location.requisition_id', 'name'),
'group' => 'Requisition.id',
'order' => 'fecha DESC'
));
This way you only get the last status from the Location table. Then, you could iterate the results, filtering by the 'name' column and deleting those whit a 'name' diferent from 'pendiente':
foreach($lastPendiente as $k => $location){
if($location['Location']['name'] != 'pendiente'){
unset($lastPendiente[$k]);
}

Getting the column 'country_id' in 'where' clause is ambiguous - error in search query in cakephp 1.3

I am working with CakePHP 1.3 version for search functionality using Search Plugin.
I have three models:
Demo,
Country
State
Demo has two foreign keys, country_id and state_id. State has the foreign key country_id.
What I am doing is, I have search form which have country & state drop down which fetch all data from countries & states table. When i search any of country from dropdown & submit the form it will show me below error. If i search using only state dropdown i get the correct result.
When I execute the search query, I get the error
'Column 'country_id' in where clause is ambiguous'
My query is:
SELECT `Demo`.`id`, `Demo`.`demo2`, `Demo`.`desc`, `Demo`.`subject`, `Demo`.`gender`, `Demo`.`country_id`, `Demo`.`state_id`, `Demo`.`image_url`, `Country`.`id`, `Country`.`name`, `State`.`id`, `State`.`country_id`, `State`.`description` FROM `demos` AS `Demo` LEFT JOIN `countries` AS `Country` ON (`Demo`.`country_id` = `Country`.`id`) LEFT JOIN `states` AS `State` ON (`Demo`.`state_id` = `State`.`id`) WHERE `country_id` = 2
Model relationships in Demo table:
var $belongsTo = array(
'Country' => array(
'className' => 'Country',
'foreignKey' => 'country_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'State' => array(
'className' => 'State',
'foreignKey' => 'state_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
);
The controller query to fetch all Country in dropdown is:
$country=$this->Country->find('list'); //just display the list of country in dropdown
The query search the data from all fields except Country (country_id), because it will not know which country_id it is looking for from table Demo or table State. I need the country_id from the demo table to get the correct result.
As I understand you want to make a find over Demo for a specific country_id.
Well you should define which "country_id" you're using because more than one of those tables
has such a column.
Just use Demo.country_id in the conditions array:
array('conditions' => array('Demo.country_id' => 2));
And you should see some SQL generated by Cake like this:
SELECT `Demo`.`id`, `Demo`.`demo2`, `Demo`.`desc`, `Demo`.`subject`, `Demo`.`gender`, `Demo`.`country_id`, `Demo`.`state_id`, `Demo`.`image_url`, `Country`.`id`, `Country`.`name`, `State`.`id`, `State`.`country_id`, `State`.`description` FROM `demos` AS `Demo` LEFT JOIN `countries` AS `Country` ON (`Demo`.`country_id` = `Country`.`id`) LEFT JOIN `states` AS `State` ON (`Demo`.`state_id` = `State`.`id`) WHERE `Demo`.`country_id` = 2
Try this:
SELECT
Demo.id,
Demo.demo2,
Demo.desc,
Demo.subject,
Demo.gender,
Demo.country_id,
Demo.state_id,
Demo.image_url,
Country.id,
Country.name,
State.id,
State.country_id,
State.description
FROM demos AS Demo
LEFT JOIN countries AS Country ON (Demo.country_id = Country.id)
LEFT JOIN states AS State ON (Demo.state_id = State.id) WHERE Demo.country_id = 2

Categories