Laravel Database-数据库的CURD操作

思考并回答以下问题:

  • Exception::getPrevious—返回异常链中的前一个异常。什么意思?

前言

当Connection对象构建初始化完成后,我们就可以利用DB来进行数据库的CURD(Create、Update、Retrieve、Delete)操作。本篇文章,我们将会讲述Laravel如何与PDO交互,实现基本数据库服务的原理。

源码

run

Laravel中任何数据库的操作都要经过run这个函数,这个函数作用在于重新连接数据库、记录数据库日志、数据库异常处理:

Illuminate\Database\Connection.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 运行一条SQL语句并记录其执行上下文。
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function run($query, $bindings, Closure $callback)
{
$this->reconnectIfMissingConnection();

$start = microtime(true);

// 在这里,我们将运行此查询。
// 如果发生异常,我们将确定它是否是连接丢失引起的。
// 如果是,我们将尝试重新建立连接,并使用新的连接重新运行查询。
try
{
$result = $this->runQueryCallback($query, $bindings, $callback);
}
catch (QueryException $e)
{
$result = $this->handleQueryException(
$e, $query, $bindings, $callback
);
}

// 运行查询后,我们将计算运行时间,
// 然后记录查询,绑定和执行时间,以便在开发人员需要它们时报告它们。
// 我们将以毫秒为单位记录时间。
$this->logQuery(
$query, $bindings, $this->getElapsedTime($start)
);

return $result;
}

重新连接数据库reconnect

如果当前的pdo是空,那么就会调用reconnector重新与数据库进行连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 如果PDO连接丢失,重新连接数据库。
*
* @return void
*/
protected function reconnectIfMissingConnection()
{
if (is_null($this->pdo))
{
$this->reconnect();
}
}

/**
* 重新连接数据库。
*
* @return void
*
* @throws \LogicException
*/
public function reconnect()
{
if (is_callable($this->reconnector))
{
$this->doctrineConnection = null;

return call_user_func($this->reconnector, $this);
}

throw new LogicException('Lost connection and no reconnector available.');
}

运行数据库操作

数据库的CURD操作会被包装成为一个闭包函数,作为runQueryCallback的一个参数,当运行正常时,会返回结果,如果遇到异常的话,会将异常转化为QueryException,并且抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 运行一个SQL语句。
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function runQueryCallback($query, $bindings, Closure $callback)
{
// 为了执行该语句,我们将简单地调用回调,该回调实际上将针对PDO连接运行SQL。
// 然后,我们可以计算执行时间并在内存中记录查询SQL,绑定和时间。
try
{
$result = $callback($query, $bindings);
}

// 如果在尝试运行查询时发生异常,我们将格式化错误消息以包含SQL绑定,
// 这将使该异常对开发人员有很大帮助,而不仅仅是数据库的错误。
catch (Exception $e)
{
throw new QueryException(
$query, $this->prepareBindings($bindings), $e
);
}

return $result;
}

数据库异常处理

当pdo查询返回异常的时候,如果当前是事务进行时,那么直接返回异常,让上一层事务来处理。

如果是由于与数据库事情连接导致的异常,那么就要重新与数据库进行连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 活跃事务数。
*
* @var int
*/
protected $transactions = 0;

/**
* 处理查询异常。
*
* @param \Illuminate\Database\QueryException $e
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function handleQueryException(QueryException $e, $query, $bindings, Closure $callback)
{
if ($this->transactions >= 1)
{
throw $e;
}

return $this->tryAgainIfCausedByLostConnection(
$e, $query, $bindings, $callback
);
}

与数据库失去连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 处理在查询执行期间发生的查询异常。
*
* @param \Illuminate\Database\QueryException $e
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback)
{
if ($this->causedByLostConnection($e->getPrevious()))
{
$this->reconnect();

return $this->runQueryCallback($query, $bindings, $callback);
}

throw $e;
}

Illuminate\Database\DetectsLostConnections.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

namespace Illuminate\Database;

use Illuminate\Support\Str;
use Throwable;

trait DetectsLostConnections
{
/**
* 确定给定的异常是否由丢失的连接引起。
*
* @param \Throwable $e
* @return bool
*/
protected function causedByLostConnection(Throwable $e)
{
$message = $e->getMessage();

return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'ORA-03114',
'Packets out of order. Expected',
'Adaptive Server connection failed',
'Communication link failure',
'connection is no longer usable',
'Login timeout expired',
'Connection refused',
'running with the --read-only option so it cannot execute this statement',
]);
}
}

数据库日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* All of the queries run against the connection.
*
* @var array
*/
protected $queryLog = [];

/**
* 将查询记录在连接的查询日志中。
*
* @param string $query
* @param array $bindings
* @param float|null $time
* @return void
*/
public function logQuery($query, $bindings, $time = null)
{
$this->event(new QueryExecuted($query, $bindings, $time, $this));

if ($this->loggingQueries)
{
$this->queryLog[] = compact('query', 'bindings', 'time');
}
}

想要开启或关闭日志功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 指明是否正在记录查询。
*
* @var bool
*/
protected $loggingQueries = false;

/**
* 在连接上启用查询日志。
*
* @return void
*/
public function enableQueryLog()
{
$this->loggingQueries = true;
}

/**
* 在连接上禁用查询日志。
*
* @return void
*/
public function disableQueryLog()
{
$this->loggingQueries = false;
}

Select查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 对数据库运行一条select语句。
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending())
{
return [];
}

// 对于select语句,我们将简单地执行查询并返回数据库结果集的数组。
// 数组中的每个元素都是数据库表中的一行,并且可以是数组或对象。
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));

$this->bindValues($statement, $this->prepareBindings($bindings));

$statement->execute();

return $statement->fetchAll();
});
}

数据库的查询主要有以下几个步骤:

  • 获取$this->pdo成员变量,若当前未连接数据库,则进行数据库连接,获取pdo对象。
  • 设置pdo数据fetch模式。
  • pdo进行sql语句预处理。
  • pdo绑定参数sql语句执行,并获取数据。

getPdoForSelect获取pdo对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 获取用于选择查询的PDO连接。
*
* @param bool $useReadPdo
* @return \PDO
*/
protected function getPdoForSelect($useReadPdo = true)
{
return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}

/**
* 获取当前的PDO连接。
*
* @return \PDO
*/
public function getPdo()
{
if ($this->pdo instanceof Closure)
{
return $this->pdo = call_user_func($this->pdo);
}

return $this->pdo;
}

/**
* 获取用于读取的当前PDO连接。
*
* @return \PDO
*/
public function getReadPdo()
{
if ($this->transactions > 0)
{
return $this->getPdo();
}

if ($this->recordsModified && $this->getConfig('sticky'))
{
return $this->getPdo();
}

if ($this->readPdo instanceof Closure)
{
return $this->readPdo = call_user_func($this->readPdo);
}

return $this->readPdo ?: $this->getPdo();
}

getPdo这里逻辑比较简单,值得我们注意的是getReadPdo。为了减缓数据库的压力,我们常常对数据库进行读写分离,也就是只要当写数据库这种操作发生时,才会使用写数据库,否则都会用读数据库。这种措施减少了数据库的压力,但是也带来了一些问题,那就是读写两个数据库在一定时间内会出现数据不一致的情况,原因就是写库的数据未能及时推送给读库,造成读库数据延迟的现象。为了在一定程度上解决这类问题,Laravel增添了sticky选项,从程序中我们可以看出,当我们设置选项sticky为真,并且的确对数据库进行了写操作后,getReadPdo会强制返回主库的连接,这样就避免了读写分离造成的延迟问题。

还有一种情况,当数据库在执行事务期间,所有的读取操作也会被强制连接主库。

prepared设置数据获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 连接的默认fetchMode。
*
* @var int
*/
protected $fetchMode = PDO::FETCH_OBJ;

/**
* 配置PDO准备语句。
*
* @param \PDOStatement $statement
* @return \PDOStatement
*/
protected function prepared(PDOStatement $statement)
{
$statement->setFetchMode($this->fetchMode);

$this->event(new StatementPrepared(
$this, $statement
));

return $statement;
}

pdo的setFetchMode函数用于为语句设置默认的获取模式,通常模式有-以下几种:

  • PDO::FETCH_ASSOC 从结果集中获取以列名为索引的关联数组。
  • PDO::FETCH_NUM 从结果集中获取一个以列在行中的数值偏移量为索引的值数组。
  • PDO::FETCH_BOTH 这是默认值,包含上面两种数组。
  • PDO::FETCH_OBJ 从结果集当前行的记录中获取其属性对应各个列名的一个对象。
  • PDO::FETCH_BOUND 使用fetch()返回TRUE,并将获取的列值赋给在bindParm()方法中指定的相应变量。
  • PDO::FETCH_LAZY 创建关联数组和索引数组,以及包含列属性的一个对象,从而可以在这三种接口中任选一种。

pdo的prepare函数

prepare函数会为PDOStatement::execute()方法准备要执行的SQL语句, SQL语句可以包含零个或多个命名(:name)或问号(?)参数标记,参数在SQL执行时会被替换。

不能在SQL语句中同时包含命名(:name)或问号(?)参数标记,只能选择其中一种风格。

预处理SQL语句中的参数在使用PDOStatement::execute()方法时会传递真实的参数。

之所以使用prepare函数,是因为这个函数可以防止SQL注入,并且可以加快同一查询语句的速度。关于预处理与参数绑定防止SQL漏洞注入的原理可以参考:Web安全之SQL注入攻击技巧与防范.

pdo的bindValues函数

在调用pdo的参数绑定函数之前,Laravel对参数值进一步进行了优化,把时间类型的对象利用grammer的设置重新格式化,false也改为0。

pdo的参数绑定函数bindValue,对于使用命名占位符的预处理语句,应是类似:name形式的参数名。对于使用问号占位符的预处理语句,应是以1开始索引的参数位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 准备要执行的查询绑定。
*
* @param array $bindings
* @return array
*/
public function prepareBindings(array $bindings)
{
$grammar = $this->getQueryGrammar();

foreach ($bindings as $key => $value) {
// 我们需要将DateTimeInterface的所有实例转换为实际的日期字符串。
// 每个查询语法都维护自己的日期字符串格式,因此我们只要求语法从日期中获取格式。
if ($value instanceof DateTimeInterface)
{
$bindings[$key] = $value->format($grammar->getDateFormat());
}
elseif (is_bool($value))
{
$bindings[$key] = (int) $value;
}
}

return $bindings;
}

/**
* 将值绑定到给定语句中的参数。
*
* @param \PDOStatement $statement
* @param array $bindings
* @return void
*/
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value)
{
$statement->bindValue(
is_string($key) ? $key : $key + 1, $value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}

insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 对数据库运行insert语句。
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function insert($query, $bindings = [])
{
return $this->statement($query, $bindings);
}

/**
* 执行一条SQL语句并返回布尔值。
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending())
{
return true;
}

$statement = $this->getPdo()->prepare($query);

$this->bindValues($statement, $this->prepareBindings($bindings));

$this->recordsHaveBeenModified();

return $statement->execute();
});
}

这部分的代码与select非常相似,不同之处有以下几个:

  • 直接获取写库的连接,不会考虑读库。
  • 由于不需要返回任何数据库数据,因此也不必设置fetchMode。
  • recordsHaveBeenModified函数标志当前连接数据库已被写入。
  • 不需要调用函数fetchAll。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 指出是否有任何记录被修改。
*
* @param bool $value
* @return void
*/
public function recordsHaveBeenModified($value = true)
{
if (! $this->recordsModified)
{
$this->recordsModified = $value;
}
}

update、delete

affectingStatement这个函数与上面的statement函数一致,只是最后会返回更新、删除影响的行数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 对数据库运行一条update语句。
*
* @param string $query
* @param array $bindings
* @return int
*/
public function update($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}

/**
* 对数据库运行一条delete语句。
*
* @param string $query
* @param array $bindings
* @return int
*/
public function delete($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}

/**
* 运行一条SQL语句并获取受影响的行数。
*
* @param string $query
* @param array $bindings
* @return int
*/
public function affectingStatement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return 0;
}

// 对于update或delete语句,我们希望获得该语句影响的行数,并将其返回给开发人员。
// 我们首先需要执行该语句,然后使用PDO来获取受影响的对象。
$statement = $this->getPdo()->prepare($query);

$this->bindValues($statement, $this->prepareBindings($bindings));

$statement->execute();

$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);

return $count;
});
}

事务

transaction数据库事务

为保持数据的一致性,对于重要的数据我们经常使用数据库事务,transaction函数接受一个闭包函数,与一个重复尝试的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php

namespace Illuminate\Database\Concerns;

use Closure;
use Throwable;

trait ManagesTransactions
{
/**
* 在事务中执行闭包
*
* @param \Closure $callback
* @param int $attempts
* @return mixed
*
* @throws \Throwable
*/
public function transaction(Closure $callback, $attempts = 1)
{
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++)
{
$this->beginTransaction();

// 我们将简单地在try/catch块中执行给定的回调,如果捕获到任何异常,
// 我们可以回滚该事务,这样就不会使这些事务实际持久化到数据库或以永久方式存储。
try
{
$callbackResult = $callback($this);
}
// 如果我们捕获到异常,我们将回滚此事务,如果没有超出尝试次数,就重试。
// 如果超出,我们将抛出异常,让开发人员处理未捕获的异常。
catch (Throwable $e)
{
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);

continue;
}

try
{
$this->commit();
}
catch (Throwable $e)
{
$this->handleCommitTransactionException(
$e, $currentAttempt, $attempts
);

continue;
}

return $callbackResult;
}
}
}

开始事务

数据库事务中非常重要的成员变量是$this->transactions,它标志着当前事务的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 启动一个新的数据库事务。
*
* @return void
*
* @throws \Throwable
*/
public function beginTransaction()
{
$this->createTransaction();

$this->transactions++;

$this->fireConnectionEvent('beganTransaction');
}

可以看出,当创建事务成功后,就会累加$this->transactions,并且启动event,创建事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 在数据库中创建一个事务。
*
* @return void
*
* @throws \Throwable
*/
protected function createTransaction()
{
if ($this->transactions == 0)
{
$this->reconnectIfMissingConnection();

try
{
$this->getPdo()->beginTransaction();
}
catch (Throwable $e)
{
$this->handleBeginTransactionException($e);
}
}
elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints())
{
$this->createSavepoint();
}
}

如果当前没有任何事务,那么就会调用pdo来开启事务。

如果当前已经在事务保护的范围内,那么就会创建SAVEPOINT,实现数据库嵌套事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 在数据库中创建一个保存点。
*
* @return void
*
* @throws \Throwable
*/
protected function createSavepoint()
{
$this->getPdo()->exec(
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace Illuminate\Database\Query\Grammars;

class Grammar extends BaseGrammar
{
/**
* 编译SQL语句以定义一个保存点。
*
* @param string $name
* @return string
*/
public function compileSavepoint($name)
{
return 'SAVEPOINT '.$name;
}
}

如果创建事务失败,那么就会调用handleBeginTransactionException:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 从事务开始处理异常。
*
* @param \Throwable $e
* @return void
*
* @throws \Throwable
*/
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e))
{
$this->reconnect();

$this->getPdo()->beginTransaction();
}
else
{
throw $e;
}
}

如果创建事务失败是由于与数据库失去连接的话,那么就会重新连接数据库,否则就要抛出异常。

事务异常

事务的异常处理比较复杂,可以先看一看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 处理运行事务语句时遇到的异常。
*
* @param \Throwable $e
* @param int $currentAttempt
* @param int $maxAttempts
* @return void
*
* @throws \Throwable
*/
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
// 陷入僵局时,MySQL会回滚整个事务,因此我们不能只是重试查询。
// 我们必须彻底消除该异常,并让开发人员以另一种方式处理它。
// 我们也会减少transactions。
if ($this->causedByConcurrencyError($e) &&
$this->transactions > 1)
{
$this->transactions--;

throw $e;
}

// 如果有异常,我们将回滚该事务,然后我们可以检查是否超出了最大尝试次数,
// 如果没有,我们将返回并在循环中再次尝试此查询。
$this->rollBack();

if ($this->causedByConcurrencyError($e) &&
$currentAttempt < $maxAttempts)
{
return;
}

throw $e;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace Illuminate\Database;

use Illuminate\Support\Str;
use PDOException;
use Throwable;

trait DetectsConcurrencyErrors
{
/**
* 确定给定的异常是否由并发错误(例如死锁或序列化失败)引起。
*
* @param \Throwable $e
* @return bool
*/
protected function causedByConcurrencyError(Throwable $e)
{
if ($e instanceof PDOException && $e->getCode() === '40001')
{
return true;
}

$message = $e->getMessage();

return Str::contains($message, [
'Deadlock found when trying to get lock',
'deadlock detected',
'The database file is locked',
'database is locked',
'database table is locked',
'A table in the database is locked',
'has been chosen as the deadlock victim',
'Lock wait timeout exceeded; try restarting transaction',
'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction',
]);
}
}

这里可以分为四种情况:

  • 单一事务,非死锁导致的异常

单一事务就是说,此时的事务只有一层,没有嵌套事务的存在。数据库的异常也不是死锁导致的,一般是由于sql语句不正确引起的。这个时候,handleTransactionException会直接回滚事务,并且抛出异常到外层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try 
{
return tap($callback($this), function ($result) {
$this->commit();
});
}
catch (Exception $e)
{
$this->handleTransactionException($e, $currentAttempt, $attempts);
}
catch (Throwable $e)
{
$this->rollBack();
throw $e;
}

接到异常之后,程序会再次回滚,但是由于$this->transactions已经为0,因此回滚直接返回,并未真正执行,之后就会抛出异常。

  • 单一事务,死锁异常

有死锁导致的单一事务异常,一般是由于其他程序同时更改了数据库,这个时候,就要判断当前重复尝试的次数是否大于用户设置的maxAttempts,如果小于就继续尝试,如果大于,那么就会抛出异常。

  • 嵌套事务,非死锁异常

如果出现嵌套事务,例如:

1
2
3
4
5
6
7
8
9
\DB::transaction(function(){
...
// 直接或间接调用另一笔交易:
\DB::transaction(function() {
...
...
}, 2);//attempt twice
},
2);//attempt twice

如果是非死锁导致的异常,那么就要首先回滚内层的事务,抛出异常到外层事务,再回滚外层事务,抛出异常,让用户来处理。也就是说,对于嵌套事务来说,内部事务异常,一定要回滚整个事务,而不是仅仅回滚内部事务。

  • 嵌套事务,死锁异常

嵌套事务的死锁异常,仍然和嵌套事务非死锁异常一样,内部事务异常,一定要回滚整个事务。

但是,不同的是,mysql对于嵌套事务的回滚会导致外部事务一并回滚:InnoDB Error Handling,因此这时,我们仅仅将$this->transactions减一,并抛出异常,使得外层事务回滚抛出异常即可。

回滚事务

如果事务内的数据库更新操作失败,那么就要进行回滚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 回滚活跃的数据库事务。
*
* @param int|null $toLevel
* @return void
*
* @throws \Throwable
*/
public function rollBack($toLevel = null)
{
// 我们允许开发人员回滚到某个交易级别。
// 在尝试回滚到该级别之前,我们将验证该给定的交易级别是否有效。
// 如果不是,我们只会退出,不会尝试任何事情。
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;

if ($toLevel < 0 || $toLevel >= $this->transactions)
{
return;
}

// 接下来,我们将实际在此数据库中执行此回滚并触发回滚事件。
// 我们还将当前交易级别设置为传递给此方法的给定级别,因此从现在开始就正确了。
try
{
$this->performRollBack($toLevel);
}
catch (Throwable $e)
{
$this->handleRollBackException($e);
}

$this->transactions = $toLevel;

$this->fireConnectionEvent('rollingBack');
}

回滚的第一件事就是要减少$this->transactions的值,标志当前事务失败。

回滚的时候仍然要判断当前事务的状态,如果当前处于嵌套事务的话,就要进行回滚到SAVEPOINT,如果是单一事务的话,才会真正回滚退出事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 在数据库内执行回滚。
*
* @param int $toLevel
* @return void
*
* @throws \Throwable
*/
protected function performRollBack($toLevel)
{
if ($toLevel == 0)
{
$this->getPdo()->rollBack();
}
elseif ($this->queryGrammar->supportsSavepoints())
{
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
);
}
}

/**
* 编译SQL语句以执行保存点回滚。
*
* @param string $name
* @return string
*/
public function compileSavepointRollBack($name)
{
return 'ROLLBACK TO SAVEPOINT '.$name;
}

提交事务

提交事务比较简单,仅仅是调用pdo的commit即可。需要注意的是对于嵌套事务的事务提交,commit函数仅仅更新了$this->transactions,而并没有真正提交事务,原因是内层事务的提交对于mysql来说是无效的,只有外部事务的提交才能更新整个事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 提交活跃的数据库事务。
*
* @return void
*
* @throws \Throwable
*/
public function commit()
{
if ($this->transactions == 1)
{
$this->getPdo()->commit();
}

$this->transactions = max(0, $this->transactions - 1);

$this->fireConnectionEvent('committed');
}
0%