Cakephp 2.0: complex find query - php

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]);
}

Related

CakePHP - MySQL Order by CAST to INT not working

With CakePHP 2.x and MySQL 5.6.40 i'm creating the following pagination:
$options = array(
'contain' => array(
'Category',
'ProductImage'
),
'limit' => 10,
'order' => array(
'Product.year' => 'DESC',
'Product.month' => 'DESC'
)
);
But i forgot my products.month column is varchar so i have to do a cast to convert the Product.month to integer:
$options = array(
'contain' => array(
'Category',
'ProductImage'
),
'limit' => 10,
'order' => array(
'Product.year' => 'DESC',
'CAST(Product.month as INT)' => 'DESC'
)
);
But on the Query output, it removes the Order By CAST as INT
SELECT
...
FROM
`web_quality`.`products` AS `Product`
LEFT JOIN `web_quality`.`categories` AS `Category` ON (
`Product`.`category_id` = `Category`.`id`
)
WHERE
`Product`.`category_id` IN (
SELECT
id
FROM
categories
WHERE
tag = 'revistas'
AND company_id IN (
SELECT
id
FROM
companies
WHERE
tag = 'dr'
)
)
AND NOT (
(
(`Product`.`year` = '2022')
AND (`Product`.`month` = '2')
)
)
AND `Product`.`enabled` = '1'
ORDER BY
`Product`.`year` DESC,
LIMIT 10
Also it doesn't generate any kind of error, if i try manually the query with the CAST as INT it works, but i have to do it using the CakePHP Model, what could be wrong ?
Thank you so much !
From what I remember you should use a virtual field for that, as the paginator checks whether the given field exists on the model, and whitelisting an SQL snippet doesn't seem like an overly good idea.
So in your model you'd do something like:
public $virtualFields = array(
'month_number' => 'CAST(Product.month as INT)',
);
and then you order by Product.month_number.
See also
Cookbook > Models > Virtual fields
Cookbook > Core Libraries > Components > Pagination > Control which fields used for ordering

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

Laravel - artisan - cannot insert with null column

I have this table structure which I created partly with laravel builder:
public function up() {
DB::statement('
CREATE TABLE `tbl_permission` (
`permission_id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
`id_module` smallint(5) unsigned NOT NULL,
`name` varchar(50) NOT NULL,
`create` TINYINT NOT NULL DEFAULT 0,
`view` TINYINT NOT NULL DEFAULT 0,
`is_composition` tinyint(1) NOT NULL DEFAULT "0",
`suffix_czech_name` VARCHAR(100),
`permission_table` VARCHAR(50),
FOREIGN KEY (`id_module`) REFERENCES `tbl_modules` (`id_module`)
) COLLATE utf8_czech_ci;
');
}
then I have another migration with insert:
public function up() {
DB::table('tbl_permission')->insert([
['name' => 'account_bad_rooms', 'id_module' => 1, 'create' => 0, 'view' => 1, 'is_composition' => 0, 'suffix_czech_name' => 'name'],
['name' => 'account', 'id_module' => 1, 'create' => 1, 'view' => 1, 'is_composition' => 0, 'suffix_czech_name' => 'name'],
['name' => 'accountRoomIdConfig', 'id_module' => 1, 'create' => 1, 'view' => 1, 'is_composition' => 0, 'suffix_czech_name' => 'name', 'permission_table' => 'accountRoomIdConfig']
]);
}
When I use migration then everything works without any error except I don't have any inserted data. I figured out that is because in two inserts I don't have column permission_table which can be null. When I add this column, then all inserts have same structure, migration is fine. Problem is that I have more than 70 inserts and some have column permission_table and some does not have it. Is somehow possible to insert all data without same structure?
If you do an insert without setting permission_table value you will get a sql error value list does not match column list.
Even if default value is null. you still have to pass the column in all rows with a value of '' even if it does not have a value.
public function up() {
DB::table('tbl_permission')->insert([
[
'name' => 'account_bad_rooms',
'id_module' => 1,
'create' => 0,
'view' => 1,
'is_composition' => 0,
'suffix_czech_name' => 'name'
'permission_table' => ''
],
[
.... next record
]
]);
}
Hope this help.

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).

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

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'])
);
}
}

Categories