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
Related
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.
In CodeIgniter I'm trying to create a function. Need create array who will delete rows in db like:
$selected_items_by_id = array('1','2','3','4',); // --<<<Need Create This
$this->db->where_in('id', $selected_items_by_id);
$this->db->delete('mytable');
mytabe DB Structure of:
ID | NAME | PARENT_ID
1 Item1 0 // First Root item
2 Item2 1 // First Root sub item
3 Item3 2 // First Root sub sub item
4 Item4 3 // First Root sub sub sub item
5 Item5 0 // Second Root item
Items layout:
Item1
+Item2
++Item3
+++Item4
Item5
Here I'm getting the needed item id (from select box):
$id = $this->input->post('delete_menu_item');
Logic:
If item parent_id == 0, then item is root.
If Item is root item, in array will be only root item id
In array need $id and all $id subs (if they exist)
UPDATE
I try to make recursive function outside from CI.
Search function - to separate the necessary subaray:
function search($array, $key, $value)
{
$results = array();
if (is_array($array))
{
if (isset($array[$key]) && $array[$key] == $value)
$results[] = $array;
foreach ($array as $subarray)
$results = array_merge($results, search($subarray, $key, $value));
}
return $results;
}
I'm using this array, based on DB entries:
$array = array(
array('id' => '1', 'name' => 'Item1', 'parent_id' => '0'),
array('id' => '2', 'name' => 'Item2', 'parent_id' => '1'),
array('id' => '3', 'name' => 'Item3', 'parent_id' => '2'),
array('id' => '4', 'name' => 'Item4', 'parent_id' => '3'),
array('id' => '5', 'name' => 'Item5', 'parent_id' => '0'),
);
Recursive function:
function build_array($array,$id, $final = NULL){
$data = search($array, 'id', $id);
foreach ($data as $item):
if ($item['parent_id'] == 0){
$final[] = $item['id'];
}
else {
$parent_id = $item['parent_id'];
$final[] = $item['id'];
$final[] = $parent_id;
build_array($array, $parent_id, $final);
// Here go recursive
}
endforeach;
return $final;
}
$result = build_array($array,1);
var_dump($result);
What should be the function recursively?
If I understand what you are asking, you'll need to handle each item iteratively (in a loop).
So first you get an item, then you see if it has a parent - if so, get it's parent and start again, etc. This is one of the few cases where recursive functions can really come in handy.
You just build your final arrays as you go, until you run out of items - being aware that if you aren't careful you will get an infinite loop as you pull the same items over and over again.
Without more information on what you have to input and what you want to get as output, this is about as helpful as anyone can be. For more help you'll need to clarify what you want as output, code you've tried so far and what it's doing that you don't want, etc.
Ok. I will use the sample array you put together, only slightly different: Since ids are unique, there's no reason why they shouldn't serve as keys. Like so:
$array = array(
'1'=>array('name' => 'Item1', 'parent_id' => '0'),
'2'=>array('name' => 'Item2', 'parent_id' => '1'),
'3'=>array('name' => 'Item3', 'parent_id' => '2'),
'4'=>array('name' => 'Item4', 'parent_id' => '3'),
'5'=>array('name' => 'Item5', 'parent_id' => '0'),
);
Add the following methods to your controller/model/lib/whatever:
function children_of($arr,$id)
{
$r=array();
foreach($arr as $key=>$entry)
if($entry['parent_id']==$id) $r[]=$key;
return $r;
}
function subtree_array($arr,$id)
{
if(isset($arr[$id]))
{
$r=array($id);
foreach($this->children_of($arr,$id) as $child)
$r=array_merge($r,$this->subtree_array($arr,$child));
return $r;
}
else
return array(0); // (0) comes handy in SQL IN operations
}
children_of() returns the immediate children of $id in $arr.
subtree_array() returns an array including $id and all of its descendants in $arr. If $id is not a key of $arr, it returns array(0). That's because you say you want to use it in an SQL query, and a where clause like where xyz in () would be bogus, while where xyz in (0) wouldn't, and also would always return false since no item has a zero id (it seems to be reserved to denote root nodes).
So, the usage might be something like:
$id=$this->input->post('delete_menu_item');
$selected_items_by_id=subtree_array($aray,$id);
I tried to approach the task from the other side.
Using cascade delete, and everything is going sql side:
CREATE TABLE `navigation` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',
`url` VARCHAR(50) NOT NULL COLLATE 'utf8_unicode_ci',
`position` MEDIUMINT(8) NOT NULL DEFAULT '100',
`parent_id` INT(11) NOT NULL,
PRIMARY KEY (`id`),
INDEX `parent_id` (`parent_id`),
CONSTRAINT `FK1` FOREIGN KEY (`parent_id`) REFERENCES `navigation` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB;
Thank you for your suggestions.
OK I have a mySQL table that has a few date columns in, it is set up so that there is a create date and a update date. The UpdateTimeStamp column updates automatically using the on update CURRENT_TIMESTAMP Attributes,
Its my understanding that you can only use a one timestamp in this special way, and this is fine.
So on the createdate column I need to manually add the date that the record was created its a datetime column.
I have read that you can use the NOW() command for this. However using it in the following brings me a undefined function error. I have tried it as just 'CreateDate' => NOW(), which gives the same error or using 'CreateDate' => set_value('NOW()'), does not produce an error but no value is entered into the database for this column.
form_data = array(
'ClientID' => set_value('ClientID'),
'ClientReference' => set_value('ClientReference'),
'CreateDate' => set_value(NOW()),
'Gender' => set_value('Gender'),
'MaritalStatus' => set_value('MaritalStatus'),
'ContactNumber' => set_value('ContactNumber'),
'Nationality' => set_value('Nationality'),
'Ethnicity' => set_value('Ethnicity'),
'ClientNotes' => set_value('ClientNotes')
);
I Have tried all the answers you have provided and its still not working its letting me add to the database but the CreateDate value is 0000-00-00 00-00-00
If your mysql column type DATETIME:
'CreateDate' => set_value(date('Y-m-d H:i:s'))
if TIMESTAMP:
'CreateDate' => set_value(time())
You can have both set automatically on insert and update.
Use a default value of CURRENT_TIMESTAMP on "CreateDate" so it sets automatically when you create the record, and a trigger to update "UpdateTimeStamp" when the row is altered
CREATE TABLE Clients (
ClientID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
/* Other Columns */
CreateDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UpdateTimeStamp TIMESTAMP NULL
) ENGINE=InnoDB;
CREATE TRIGGER ClientsUpdate
BEFORE UPDATE ON Clients FOR EACH ROW
SET NEW.UpdateTimeStamp = NOW();
There is no now() function in PHP, what you are looking for is the time() function. However, this will return a timestamp such as 1370183405.
MySql has a now() function in which case you need to wrap it in quotes:-
'CreateDate' => set_value('NOW()'),
Or in PHP you could do:-
'CreateDate' => set_value((new DateTime())->format('Y-m-d H:i:s')),
Nearly right,
I tried it without the set_value in so
'CreateDate' => set_value(date('Y-m-d H:i:s')),
And that worked. dont know what difference it makes
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]);
}
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'])
);
}
}