laravel數據庫——使用on duplicate key update
在laravel數據庫的查詢構造器中,
insert
方法可以進行批量插入,數據庫ORM中提供了updateOrCreate
方法支持插入/更新,但updateOrCreate
不能批量處理。並且updateOrCreate
要先查詢再更新,一次updaeOrCreate
要執行兩次SQL命令。
/**
* @mixin \Illuminate\Database\Query\Builder
*/
class Builder
{
...
/**
* Create or update a record matching the attributes, and fill it with values.
*
* @param array $attributes
* @param array $values
* @return \Illuminate\Database\Eloquent\Model
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
...
}
使用mysql提供的on duplicate key update
方法顯示具有很大優勢。
簡介
使用on duplicate key update
肯定要拼接sql語句。可以通過foreach
循環$values
數組來直接拼接sql語句,這種方式過於簡單粗暴,擴展性不好且可能存在SQL注入的風險。在本文中,採用laravel提供的語法器Grammar
類進行sql命令的編譯。
insertOrUpdate方法
在數據庫模型Model類中添加如下代碼:
/**
* insert or update a record
*
* @param array $values
* @param array $value
* @return bool
*/
public function insertOrUpdate(array $values, array $value)
{
$connection = $this->getConnection(); // 數據庫連接
$builder = $this->newQuery()->getQuery(); // 查詢構造器
$grammar = $builder->getGrammar(); // 語法器
// 編譯插入語句
$insert = $grammar->compileInsert($builder, $values);
// 編譯重複後更新列語句。
$update = $this->compileUpdateColumns($grammar, $value);
// 構造查詢語句
$query = $insert.' on duplicate key update '.$update;
// 組裝sql綁定參數
$bindings = $this->prepareBindingsForInsertOrUpdate($values, $value);
// 執行數據庫查詢
return $connection->insert($query, $bindings);
}
/**
* Compile all of the columns for an update statement.
*
* @param Grammar $grammar
* @param array $values
* @return string
*/
private function compileUpdateColumns($grammar, $values)
{
return collect($values)->map(function ($value, $key) use ($grammar) {
return $grammar->wrap($key).' = '.$grammar->parameter($value);
})->implode(', ');
}
/**
* Prepare the bindings for an insert or update statement.
*
* @param array $values
* @param array $value
* @return array
*/
private function prepareBindingsForInsertOrUpdate(array $values, array $value)
{
// Merge array of bindings
$bindings = array_merge_recursive($values, $value);
// Remove all of the expressions from a list of bindings.
return array_values(array_filter(array_flatten($bindings, 1), function ($binding) {
return ! $binding instanceof \Illuminate\Database\Query\Expression;
}));
}
使用方法
創建一個model的實例,在實例對象上調用該方法。例如:
$user = ['username' => '用戶名稱', 'tel' => '用戶電話號碼'];
$users = [$user, $user];
$model = new \App\Models\User();
// 處理一條記錄
$model->insertOrUpdate($user, ['username' => DB::raw('values(`username`)'), 'tel' => '常量']);
// 處理多條記錄
$model->insertOrUpdate($users, ['username' => DB::raw('values(`username`)'), 'tel' => '常量']);
$values參數
- 插入或更新一條記錄,直接將一維數組作爲
insertOrUpdate
方法的第一個參數進行調用。
$user = ['username' => '用戶名稱', 'tel' => '用戶電話號碼'];
$model->insertOrUpdate($user, ['tel' => DB::raw('values(`tel`)')]);
因爲insertOrCreate
方法中使用Illuminate\Database\Query\Builder::insert()
方法進行插入,所以兼容一條或多條記錄。
insert
方法的介紹參見查詢構造器 |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區
- 插入多條記錄,用數組包裹多條記錄的數組爲二維數組作爲
insertOrUpdate
方法的第一個參數進行調用。
$user = ['username' => '用戶名稱', 'tel' => '用戶電話號碼'];
$users = [$user, $user];
$model->insertOrUpdateinsertOrUpdate($users, ['username' => DB::raw('values(`username`)'), 'tel' => '常量']);
其實,即使是使用一維數組作爲insertOrUpdate
的參數,在執行insert
操作時,依然會包裹成一個二維數組。在\Illuminate\Database\Query\Builder
類中,可以看到調用insert
方法進行的操作。
class Builder
{
...
/**
* Insert a new record into the database.
*
* @param array $values
* @return bool
*/
public function insert(array $values)
{
// Since every insert gets treated like a batch insert, we will make sure the
// bindings are structured in a way that is convenient when building these
// inserts statements by verifying these elements are actually an array.
if (empty($values)) {
return true;
}
if (! is_array(reset($values))) {
$values = [$values];
}
...
}
}
$value參數
$value
參數指定了on duplicate key update
後要執行的語句。$value
數組中key
即下標會被設置爲表的待更新字段名,$value
數組中的value
會被設置爲表的待更新字段的值。
$value
的value
可以有兩種類型。一是常量,二是Illuminate\Database\Query\Expression
類的對象。
對於常量類型的value
值,會被當做PDO
預編譯語句的綁定參數處理。
對於Illuminate\Database\Query\Expression
類型的value
值,會被當做sql命令拼接到sql語句中。可以通過DB::raw()
方法創建該對象。在自定義方法prepareBindingsForInsertOrUpdate
中會在格式化綁定參數時過濾掉該類型的$value
數組的value
。
/**
* Prepare the bindings for an insert or update statement.
*
* @param array $values
* @param array $value
* @return array
*/
private function prepareBindingsForInsertOrUpdate(array $values, array $value)
{
// Merge array of bindings
$bindings = array_merge_recursive($values, $value);
// Remove all of the expressions from a list of bindings.
return array_values(array_filter(array_flatten($bindings, 1), function ($binding) {
return ! $binding instanceof Expression;
}));
}
代碼詳解
insertOrUpdate方法
- 獲取
Builder::insert
方法編譯後的sql語句。
$insert = $grammar->compileInsert($builder, $values);
上面語句執行後,$insert
的值爲
insert into `user` (`username`, `tel`) values (?, ?), (?, ?)
- 編譯
update
後面的列字段
$update = $this->compileUpdateColumns($grammar, $value);
上面的語句執行後,update
的值爲
`username` = values(`username`), `tel` = ?
- 拼接sql語句
$query = $insert.' on duplicate key update '.$update;
上面語句執行後,$query
的值爲
insert into `user` (`username`, `tel`) values (?, ?), (?, ?) on duplicate key update `username` = values(`username`), `tel` = ?
- 組裝預編譯sql語句的綁定參數
$bindings = $this->prepareBindingsForInsertOrUpdate($values, $value);
上面語句執行完後,$bindings
值爲
array:5 [▼
0 => "用戶名稱"
1 => "用戶電話號碼"
2 => "用戶名稱"
3 => "用戶電話號碼"
4 => "常量"
]
- 執行sql命令
$connection->insert($query, $bindings);
compileUpdateColumns方法
該方法主要生成待更新表的字段語句。
對於$value
數組中的下標$key
,通過$grammar->wrap($key)
,如果是字符串格式轉換爲` $key
` ,如果Expression
格式,使用$expression->getValue()
方法獲取到表達式裏的字符串。
對於$value
數組中的值,通過$grammer->parameter($value)
,如果是字符串格式轉換爲?
;如果是Expression
格式,使用$expression->getValue()
方法獲取到表達式裏的字符串。
上面語句會把$value
數組中的key
當做表的字段名,使用"`"符號包裹 $key
,如果$key
是Expression
將直接取出$key
中的字符串。
$this->compileUpdateColumns(['username' => DB::raw('values(`username`)'), 'tel' => '常量']);
// 返回值爲:`username` = values(`username`), `tel` = ?
prepareBindingsForInsertOrUpdate方法
該方法生成sql語句的綁定參數。
$bindings = array_merge_recursive($values, $value);
將$values
和$value
中的值合併。
array_values(array_filter(array_flatten($bindings, 1), function ($binding) {
return ! $binding instanceof Expression;
}));
將bindings
數組變爲一維數組並過濾其中的Expression
。
laravel源碼
$bindings = $this->prepareBindingsForInsertOrUpdate(
[
['username' => '用戶名稱', 'tel' => '用戶電話號碼'],
['username' => '用戶名稱', 'tel' => '用戶電話號碼']
],
['username' => DB::raw('values(`username`)'), 'tel' => '常量']
);
// array: [0 => "用戶名稱", 1 => "用戶電話號碼", 2 => "用戶名稱", 3 => "用戶電話號碼", 4 => "常量"]
insertOrUpdate
,compileUpdateColumns
和prepareBindingsForInsertOrUpdate
方法都能在laravel的Grammar
類和Builder
類中找到原型,只不過是受保護方法protected
,不能在類外部直接使用,才移至Model類中。
insertOrUpdate
方法參考了Builder
類的insert
方法:
<?php
namespace Illuminate\Database\Query;
class Builder
{
...
/**
* Insert a new record into the database.
*
* @param array $values
* @return bool
*/
public function insert(array $values)
{
// Since every insert gets treated like a batch insert, we will make sure the
// bindings are structured in a way that is convenient when building these
// inserts statements by verifying these elements are actually an array.
if (empty($values)) {
return true;
}
if (! is_array(reset($values))) {
$values = [$values];
}
// Here, we will sort the insert keys for every record so that each insert is
// in the same order for the record. We need to make sure this is the case
// so there are not any errors or problems when inserting these records.
else {
foreach ($values as $key => $value) {
ksort($value);
$values[$key] = $value;
}
}
// Finally, we will run this query against the database connection and return
// the results. We will need to also flatten these bindings before running
// the query so they are all in one huge, flattened array for execution.
return $this->connection->insert(
$this->grammar->compileInsert($this, $values),
$this->cleanBindings(Arr::flatten($values, 1))
);
}
...
}
compileUpdateColumns
源碼爲:
<?php
namespace Illuminate\Database\Query\Grammars
class MySqlGrammar extends Grammar
{
...
/**
* Compile all of the columns for an update statement.
*
* @param array $values
* @return string
*/
protected function compileUpdateColumns($values)
{
return collect($values)->map(function ($value, $key) {
if ($this->isJsonSelector($key)) {
return $this->compileJsonUpdateColumn($key, new JsonExpression($value));
}
return $this->wrap($key).' = '.$this->parameter($value);
})->implode(', ');
}
...
}
prepareBindingsForInsertOrUpdate
方法參考了Builder
的mergeBindings
和cleanBindings
方法:
<?php
namespace Illuminate\Database\Query;
class Builder
{
...
/**
* Merge an array of bindings into our bindings.
*
* @param \Illuminate\Database\Query\Builder $query
* @return $this
*/
public function mergeBindings(self $query)
{
$this->bindings = array_merge_recursive($this->bindings, $query->bindings);
return $this;
}
/**
* Remove all of the expressions from a list of bindings.
*
* @param array $bindings
* @return array
*/
protected function cleanBindings(array $bindings)
{
return array_values(array_filter($bindings, function ($binding) {
return ! $binding instanceof Expression;
}));
}
...
}