Yii Framework Active Record INSERT … ON DUPLICATE KEY UPDATE
Recently I was programming in the Yii Framework and ran into a situation where I wanted to use INSERT … ON DUPLICATE KEY UPDATE in an Active Record. In my research, I discovered that this is specific to MySQL. In my case, this was still be a valuable requirement for my Web Application and was worth extending Yii to get working. My approach was as follows:
- Extend CMysqlCommandBuilder:
* Added updateAllAttributesOnDuplicate function: updates the record if a duplicate is found
* Added updateNoAttributesOnDuplicate function: does not update the record if a duplicate is found (but does not throw an exception)
* Overrode the function “createInsertCommand” to implement the onDuplicate logic
- Extend CActiveRecord:
- Overrode the function “getCommandBuilder” to use the new Command Builder
- Added updateAllAttributesOnDuplicate and updateNoAttributesOnDuplicate functions to call into the command builder and change how INSERT … ON DUPLICATE KEY UPDATE works
- Extend Active Record Class
- Overrode the function “init” to set the proper INSERT … ON DUPLICATE KEY UPDATE action
- Extend CActiveRecord:
Although INSERT … ON DUPLICATE KEY UPDATE will almost always be used for non-incrementing or composite Primary Keys, this code will also work for auto-incrementing Primary Keys. If an auto-increment key is found, the logic in “createInsertCommand” will always attempt to use MySQL’s “LAST_INSERT_ID(expr)” function to set the LAST_INSERT_ID to that of the updated record (described at http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html).
Here are the 2 class files you will need plus an example of how to extend your Active Record class:
- Place the EMysqlCommandBuilder File in {app-path}/protected/components/EMysqlCommandBuilder.php
<? class EMysqlCommandBuilder extends CMysqlCommandBuilder { private $_onDuplicate=false; /** * acts like a normal insert */ public function nothingOnDuplicate(){ $this->_onDuplicate=false; } /** * does not update the record if a duplicate is found */ public function updateNoAttributesOnDuplicate(){ $this->_onDuplicate='updateNoAttributes'; } /** * updates the record if a duplicate is found */ public function updateAllAttributesOnDuplicate(){ $this->_onDuplicate='updateAllAttributes'; } /** * return current state of ON DUPLICATE KEY UPDATE */ public function getInsertOnDuplicate(){ return $this->_onDuplicate; } /** * Creates an INSERT command. * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). * @param array $data data to be inserted (column name=>column value). If a key is not a valid column name, the corresponding value will be ignored. * @return CDbCommand insert command */ public function createInsertCommand($table,$data) { $this->ensureTable($table); $fields=array(); $values=array(); $placeholders=array(); $i=0; foreach($data as $name=>$value) { if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) { $fields[]=$column->rawName; if($value instanceof CDbExpression) { $placeholders[]=$value->expression; foreach($value->params as $n=>$v) $values[$n]=$v; } else { $placeholders[]=self::PARAM_PREFIX.$i; $values[self::PARAM_PREFIX.$i]=$column->typecast($value); $i++; } } } if($fields===array()) { $pks=is_array($table->primaryKey) ? $table->primaryKey : array($table->primaryKey); foreach($pks as $pk) { $fields[]=$table->getColumn($pk)->rawName; $placeholders[]='NULL'; } } $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; //begin onDuplicate logic if ($this->_onDuplicate!=false){ $autoIncrement=false; $updateFields=array(); if ($this->_onDuplicate=='updateAllAttributes'){ $updateFields=$fields; } foreach($table->columns as $columnSchema){ if ($columnSchema->autoIncrement){ $autoIncrement=$columnSchema->rawName; break; } } if ($autoIncrement!==false || (!empty($updateFields) || $this->_onDuplicate=='updateNoAttributes')){ $sql.=" ON DUPLICATE KEY UPDATE"; if ($autoIncrement!==false){ if (($autoIncrementIndex=array_search($autoIncrement,$updateFields))!==false){ unset($updateFields[$autoIncrementIndex]); $updateFields=array_values($updateFields); } $sql.=" {$autoIncrement}=LAST_INSERT_ID({$autoIncrement})"; } if ($this->_onDuplicate=='updateAllAttributes'){ if (!empty($updateFields)){ if ($autoIncrement!==false){ $sql.=','; } $updateFieldsSql=array(); foreach($updateFields as $updateField){ $updateFieldsSql[]=" {$updateField}=VALUES({$updateField})"; } $sql.=implode(',',$updateFieldsSql); } } else if ($this->_onDuplicate=='updateNoAttributes' && $autoIncrement===false){ $firstColumn=reset($table->columns); $sql.=" {$firstColumn->rawName}={$firstColumn->rawName}"; } } } //end onDuplicate logic $command=$this->getDbConnection()->createCommand($sql); foreach($values as $name=>$value) $command->bindValue($name,$value); return $command; } }
- Place the EActiveRecord File in {app-path}/protected/components/EActiveRecord.php
<? abstract class EActiveRecord extends CActiveRecord { private $_builder; public function getCommandBuilder(){ if($this->_builder!==null) return $this->_builder; else return $this->_builder=new EMysqlCommandBuilder($this->getDbConnection()->getSchema()); } public function nothingOnDuplicate(){ $this->getCommandBuilder()->nothingOnDuplicate(); } public function updateNoAttributesOnDuplicate(){ $this->getCommandBuilder()->updateNoAttributesOnDuplicate(); } public function updateAllAttributesOnDuplicate(){ $this->getCommandBuilder()->updateAllAttributesOnDuplicate(); } }
- Extend EActiveRecord with your Active Record class and initialize according to how you want the INSERT … ON DUPLICATE KEY UPDATE to work for that Active Record.
<? class MyActiveRecord extends EActiveRecord { public function init(){ $this->updateAllAttributesOnDuplicate(); return parent::init(); } //The rest of your normal active record class goes below here... /** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return EmName the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } //etc. }