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會被設置爲表的待更新字段的值。
$valuevalue可以有兩種類型。一是常量,二是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方法

  1. 獲取Builder::insert方法編譯後的sql語句。
$insert = $grammar->compileInsert($builder, $values);

上面語句執行後,$insert的值爲

insert into `user` (`username`, `tel`) values (?, ?), (?, ?)
  1. 編譯update後面的列字段
$update = $this->compileUpdateColumns($grammar, $value);

上面的語句執行後,update的值爲

`username` = values(`username`), `tel` = ?
  1. 拼接sql語句
$query = $insert.' on duplicate key update '.$update;

上面語句執行後,$query的值爲

insert into `user` (`username`, `tel`) values (?, ?), (?, ?) on duplicate key update `username` = values(`username`), `tel` = ?
  1. 組裝預編譯sql語句的綁定參數
$bindings = $this->prepareBindingsForInsertOrUpdate($values, $value);

上面語句執行完後,$bindings值爲

array:5 [0 => "用戶名稱"
  1 => "用戶電話號碼"
  2 => "用戶名稱"
  3 => "用戶電話號碼"
  4 => "常量"
]
  1. 執行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,如果$keyExpression將直接取出$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,compileUpdateColumnsprepareBindingsForInsertOrUpdate方法都能在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方法參考了BuildermergeBindingscleanBindings方法:

<?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;
        }));
    }
	
	...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章