PDOConnection.php 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types = 1);
  12. namespace think\db;
  13. use Closure;
  14. use PDO;
  15. use PDOStatement;
  16. use think\db\exception\BindParamException;
  17. use think\db\exception\DbEventException;
  18. use think\db\exception\DbException;
  19. use think\db\exception\PDOException;
  20. use think\Model;
  21. /**
  22. * 数据库连接基础类
  23. * @property PDO[] $links
  24. * @property PDO $linkID
  25. * @property PDO $linkRead
  26. * @property PDO $linkWrite
  27. */
  28. abstract class PDOConnection extends Connection
  29. {
  30. const PARAM_FLOAT = 21;
  31. /**
  32. * 数据库连接参数配置
  33. * @var array
  34. */
  35. protected $config = [
  36. // 数据库类型
  37. 'type' => '',
  38. // 服务器地址
  39. 'hostname' => '',
  40. // 数据库名
  41. 'database' => '',
  42. // 用户名
  43. 'username' => '',
  44. // 密码
  45. 'password' => '',
  46. // 端口
  47. 'hostport' => '',
  48. // 连接dsn
  49. 'dsn' => '',
  50. // 数据库连接参数
  51. 'params' => [],
  52. // 数据库编码默认采用utf8
  53. 'charset' => 'utf8',
  54. // 数据库表前缀
  55. 'prefix' => '',
  56. // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
  57. 'deploy' => 0,
  58. // 数据库读写是否分离 主从式有效
  59. 'rw_separate' => false,
  60. // 读写分离后 主服务器数量
  61. 'master_num' => 1,
  62. // 指定从服务器序号
  63. 'slave_no' => '',
  64. // 模型写入后自动读取主服务器
  65. 'read_master' => false,
  66. // 是否严格检查字段是否存在
  67. 'fields_strict' => true,
  68. // 开启字段缓存
  69. 'fields_cache' => false,
  70. // 监听SQL
  71. 'trigger_sql' => true,
  72. // Builder类
  73. 'builder' => '',
  74. // Query类
  75. 'query' => '',
  76. // 是否需要断线重连
  77. 'break_reconnect' => false,
  78. // 断线标识字符串
  79. 'break_match_str' => [],
  80. ];
  81. /**
  82. * PDO操作实例
  83. * @var PDOStatement
  84. */
  85. protected $PDOStatement;
  86. /**
  87. * 当前SQL指令
  88. * @var string
  89. */
  90. protected $queryStr = '';
  91. /**
  92. * 事务指令数
  93. * @var int
  94. */
  95. protected $transTimes = 0;
  96. /**
  97. * 重连次数
  98. * @var int
  99. */
  100. protected $reConnectTimes = 0;
  101. /**
  102. * 查询结果类型
  103. * @var int
  104. */
  105. protected $fetchType = PDO::FETCH_ASSOC;
  106. /**
  107. * 字段属性大小写
  108. * @var int
  109. */
  110. protected $attrCase = PDO::CASE_LOWER;
  111. /**
  112. * 数据表信息
  113. * @var array
  114. */
  115. protected $info = [];
  116. /**
  117. * 查询开始时间
  118. * @var float
  119. */
  120. protected $queryStartTime;
  121. /**
  122. * PDO连接参数
  123. * @var array
  124. */
  125. protected $params = [
  126. PDO::ATTR_CASE => PDO::CASE_NATURAL,
  127. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  128. PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
  129. PDO::ATTR_STRINGIFY_FETCHES => false,
  130. PDO::ATTR_EMULATE_PREPARES => false,
  131. ];
  132. /**
  133. * 参数绑定类型映射
  134. * @var array
  135. */
  136. protected $bindType = [
  137. 'string' => PDO::PARAM_STR,
  138. 'str' => PDO::PARAM_STR,
  139. 'integer' => PDO::PARAM_INT,
  140. 'int' => PDO::PARAM_INT,
  141. 'boolean' => PDO::PARAM_BOOL,
  142. 'bool' => PDO::PARAM_BOOL,
  143. 'float' => self::PARAM_FLOAT,
  144. 'datetime' => PDO::PARAM_STR,
  145. 'timestamp' => PDO::PARAM_STR,
  146. ];
  147. /**
  148. * 服务器断线标识字符
  149. * @var array
  150. */
  151. protected $breakMatchStr = [
  152. 'server has gone away',
  153. 'no connection to the server',
  154. 'Lost connection',
  155. 'is dead or not enabled',
  156. 'Error while sending',
  157. 'decryption failed or bad record mac',
  158. 'server closed the connection unexpectedly',
  159. 'SSL connection has been closed unexpectedly',
  160. 'Error writing data to the connection',
  161. 'Resource deadlock avoided',
  162. 'failed with errno',
  163. 'child connection forced to terminate due to client_idle_limit',
  164. 'query_wait_timeout',
  165. 'reset by peer',
  166. 'Physical connection is not usable',
  167. 'TCP Provider: Error code 0x68',
  168. 'ORA-03114',
  169. 'Packets out of order. Expected',
  170. 'Adaptive Server connection failed',
  171. 'Communication link failure',
  172. 'connection is no longer usable',
  173. 'Login timeout expired',
  174. 'SQLSTATE[HY000] [2002] Connection refused',
  175. 'running with the --read-only option so it cannot execute this statement',
  176. 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
  177. 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
  178. 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
  179. 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
  180. 'SQLSTATE[HY000] [2002] Connection timed out',
  181. 'SSL: Connection timed out',
  182. 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.',
  183. ];
  184. /**
  185. * 绑定参数
  186. * @var array
  187. */
  188. protected $bind = [];
  189. /**
  190. * 获取当前连接器类对应的Query类
  191. * @access public
  192. * @return string
  193. */
  194. public function getQueryClass(): string
  195. {
  196. return $this->getConfig('query') ?: Query::class;
  197. }
  198. /**
  199. * 获取当前连接器类对应的Builder类
  200. * @access public
  201. * @return string
  202. */
  203. public function getBuilderClass(): string
  204. {
  205. return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
  206. }
  207. /**
  208. * 解析pdo连接的dsn信息
  209. * @access protected
  210. * @param array $config 连接信息
  211. * @return string
  212. */
  213. abstract protected function parseDsn(array $config): string;
  214. /**
  215. * 取得数据表的字段信息
  216. * @access public
  217. * @param string $tableName 数据表名称
  218. * @return array
  219. */
  220. abstract public function getFields(string $tableName): array;
  221. /**
  222. * 取得数据库的表信息
  223. * @access public
  224. * @param string $dbName 数据库名称
  225. * @return array
  226. */
  227. abstract public function getTables(string $dbName = ''): array;
  228. /**
  229. * 对返数据表字段信息进行大小写转换出来
  230. * @access public
  231. * @param array $info 字段信息
  232. * @return array
  233. */
  234. public function fieldCase(array $info): array
  235. {
  236. // 字段大小写转换
  237. switch ($this->attrCase) {
  238. case PDO::CASE_LOWER:
  239. $info = array_change_key_case($info);
  240. break;
  241. case PDO::CASE_UPPER:
  242. $info = array_change_key_case($info, CASE_UPPER);
  243. break;
  244. case PDO::CASE_NATURAL:
  245. default:
  246. // 不做转换
  247. }
  248. return $info;
  249. }
  250. /**
  251. * 获取字段类型
  252. * @access protected
  253. * @param string $type 字段类型
  254. * @return string
  255. */
  256. protected function getFieldType(string $type): string
  257. {
  258. if (0 === stripos($type, 'set') || 0 === stripos($type, 'enum')) {
  259. $result = 'string';
  260. } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
  261. $result = 'float';
  262. } elseif (preg_match('/(int|serial|bit)/is', $type)) {
  263. $result = 'int';
  264. } elseif (preg_match('/bool/is', $type)) {
  265. $result = 'bool';
  266. } elseif (0 === stripos($type, 'timestamp')) {
  267. $result = 'timestamp';
  268. } elseif (0 === stripos($type, 'datetime')) {
  269. $result = 'datetime';
  270. } elseif (0 === stripos($type, 'date')) {
  271. $result = 'date';
  272. } else {
  273. $result = 'string';
  274. }
  275. return $result;
  276. }
  277. /**
  278. * 获取字段绑定类型
  279. * @access public
  280. * @param string $type 字段类型
  281. * @return integer
  282. */
  283. public function getFieldBindType(string $type): int
  284. {
  285. if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
  286. $bind = $this->bindType[$type];
  287. } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
  288. $bind = PDO::PARAM_STR;
  289. } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
  290. $bind = self::PARAM_FLOAT;
  291. } elseif (preg_match('/(int|serial|bit)/is', $type)) {
  292. $bind = PDO::PARAM_INT;
  293. } elseif (preg_match('/bool/is', $type)) {
  294. $bind = PDO::PARAM_BOOL;
  295. } else {
  296. $bind = PDO::PARAM_STR;
  297. }
  298. return $bind;
  299. }
  300. /**
  301. * 获取数据表信息缓存key
  302. * @access protected
  303. * @param string $schema 数据表名称
  304. * @return string
  305. */
  306. protected function getSchemaCacheKey(string $schema): string
  307. {
  308. return $this->getConfig('hostname') . ':' . $this->getConfig('hostport') . '@' . $schema;
  309. }
  310. /**
  311. * @param string $tableName 数据表名称
  312. * @param bool $force 强制从数据库获取
  313. * @return array
  314. */
  315. public function getSchemaInfo(string $tableName, $force = false)
  316. {
  317. if (!strpos($tableName, '.')) {
  318. $schema = $this->getConfig('database') . '.' . $tableName;
  319. } else {
  320. $schema = $tableName;
  321. }
  322. if (!isset($this->info[$schema]) || $force) {
  323. // 读取字段缓存
  324. $cacheKey = $this->getSchemaCacheKey($schema);
  325. $cacheField = $this->config['fields_cache'] && !empty($this->cache);
  326. if ($cacheField && !$force) {
  327. $info = $this->cache->get($cacheKey);
  328. }
  329. if (empty($info)) {
  330. $info = $this->getTableFieldsInfo($tableName);
  331. if ($cacheField) {
  332. $this->cache->set($cacheKey, $info);
  333. }
  334. }
  335. $pk = $info['_pk'] ?? null;
  336. $autoinc = $info['_autoinc'] ?? null;
  337. unset($info['_pk'], $info['_autoinc']);
  338. $bind = [];
  339. foreach ($info as $name => $val) {
  340. $bind[$name] = $this->getFieldBindType($val);
  341. }
  342. $this->info[$schema] = [
  343. 'fields' => array_keys($info),
  344. 'type' => $info,
  345. 'bind' => $bind,
  346. 'pk' => $pk,
  347. 'autoinc' => $autoinc,
  348. ];
  349. }
  350. return $this->info[$schema];
  351. }
  352. /**
  353. * 获取数据表信息
  354. * @access public
  355. * @param mixed $tableName 数据表名 留空自动获取
  356. * @param string $fetch 获取信息类型 包括 fields type bind pk
  357. * @return mixed
  358. */
  359. public function getTableInfo($tableName, string $fetch = '')
  360. {
  361. if (is_array($tableName)) {
  362. $tableName = key($tableName) ?: current($tableName);
  363. }
  364. if (strpos($tableName, ',') || strpos($tableName, ')')) {
  365. // 多表不获取字段信息
  366. return [];
  367. }
  368. [$tableName] = explode(' ', $tableName);
  369. $info = $this->getSchemaInfo($tableName);
  370. return $fetch ? $info[$fetch] : $info;
  371. }
  372. /**
  373. * 获取数据表的字段信息
  374. * @access public
  375. * @param string $tableName 数据表名
  376. * @return array
  377. */
  378. public function getTableFieldsInfo(string $tableName): array
  379. {
  380. $fields = $this->getFields($tableName);
  381. $info = [];
  382. foreach ($fields as $key => $val) {
  383. // 记录字段类型
  384. $info[$key] = $this->getFieldType($val['type']);
  385. if (!empty($val['primary'])) {
  386. $pk[] = $key;
  387. }
  388. if (!empty($val['autoinc'])) {
  389. $autoinc = $key;
  390. }
  391. }
  392. if (isset($pk)) {
  393. // 设置主键
  394. $pk = count($pk) > 1 ? $pk : $pk[0];
  395. $info['_pk'] = $pk;
  396. }
  397. if (isset($autoinc)) {
  398. $info['_autoinc'] = $autoinc;
  399. }
  400. return $info;
  401. }
  402. /**
  403. * 获取数据表的主键
  404. * @access public
  405. * @param mixed $tableName 数据表名
  406. * @return string|array
  407. */
  408. public function getPk($tableName)
  409. {
  410. return $this->getTableInfo($tableName, 'pk');
  411. }
  412. /**
  413. * 获取数据表的自增主键
  414. * @access public
  415. * @param mixed $tableName 数据表名
  416. * @return string
  417. */
  418. public function getAutoInc($tableName)
  419. {
  420. return $this->getTableInfo($tableName, 'autoinc');
  421. }
  422. /**
  423. * 获取数据表字段信息
  424. * @access public
  425. * @param mixed $tableName 数据表名
  426. * @return array
  427. */
  428. public function getTableFields($tableName): array
  429. {
  430. return $this->getTableInfo($tableName, 'fields');
  431. }
  432. /**
  433. * 获取数据表字段类型
  434. * @access public
  435. * @param mixed $tableName 数据表名
  436. * @param string $field 字段名
  437. * @return array|string
  438. */
  439. public function getFieldsType($tableName, string $field = null)
  440. {
  441. $result = $this->getTableInfo($tableName, 'type');
  442. if ($field && isset($result[$field])) {
  443. return $result[$field];
  444. }
  445. return $result;
  446. }
  447. /**
  448. * 获取数据表绑定信息
  449. * @access public
  450. * @param mixed $tableName 数据表名
  451. * @return array
  452. */
  453. public function getFieldsBind($tableName): array
  454. {
  455. return $this->getTableInfo($tableName, 'bind');
  456. }
  457. /**
  458. * 连接数据库方法
  459. * @access public
  460. * @param array $config 连接参数
  461. * @param integer $linkNum 连接序号
  462. * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式)
  463. * @return PDO
  464. * @throws PDOException
  465. */
  466. public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
  467. {
  468. if (isset($this->links[$linkNum])) {
  469. return $this->links[$linkNum];
  470. }
  471. if (empty($config)) {
  472. $config = $this->config;
  473. } else {
  474. $config = array_merge($this->config, $config);
  475. }
  476. // 连接参数
  477. if (isset($config['params']) && is_array($config['params'])) {
  478. $params = $config['params'] + $this->params;
  479. } else {
  480. $params = $this->params;
  481. }
  482. // 记录当前字段属性大小写设置
  483. $this->attrCase = $params[PDO::ATTR_CASE];
  484. if (!empty($config['break_match_str'])) {
  485. $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
  486. }
  487. try {
  488. if (empty($config['dsn'])) {
  489. $config['dsn'] = $this->parseDsn($config);
  490. }
  491. $startTime = microtime(true);
  492. $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
  493. // SQL监控
  494. if (!empty($config['trigger_sql'])) {
  495. $this->trigger('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
  496. }
  497. return $this->links[$linkNum];
  498. } catch (\PDOException $e) {
  499. if ($autoConnection) {
  500. $this->db->log($e->getMessage(), 'error');
  501. return $this->connect($autoConnection, $linkNum);
  502. } else {
  503. throw $e;
  504. }
  505. }
  506. }
  507. /**
  508. * 视图查询
  509. * @access public
  510. * @param array $args
  511. * @return BaseQuery
  512. */
  513. public function view(...$args)
  514. {
  515. return $this->newQuery()->view(...$args);
  516. }
  517. /**
  518. * 创建PDO实例
  519. * @param $dsn
  520. * @param $username
  521. * @param $password
  522. * @param $params
  523. * @return PDO
  524. */
  525. protected function createPdo($dsn, $username, $password, $params)
  526. {
  527. return new PDO($dsn, $username, $password, $params);
  528. }
  529. /**
  530. * 释放查询结果
  531. * @access public
  532. */
  533. public function free(): void
  534. {
  535. $this->PDOStatement = null;
  536. }
  537. /**
  538. * 获取PDO对象
  539. * @access public
  540. * @return PDO|false
  541. */
  542. public function getPdo()
  543. {
  544. if (!$this->linkID) {
  545. return false;
  546. }
  547. return $this->linkID;
  548. }
  549. /**
  550. * 执行查询 使用生成器返回数据
  551. * @access public
  552. * @param BaseQuery $query 查询对象
  553. * @param string $sql sql指令
  554. * @param array $bind 参数绑定
  555. * @param Model|null $model 模型对象实例
  556. * @param null $condition 查询条件
  557. * @return \Generator
  558. * @throws DbException
  559. */
  560. public function getCursor(BaseQuery $query, string $sql, array $bind = [], $model = null, $condition = null)
  561. {
  562. $this->queryPDOStatement($query, $sql, $bind);
  563. // 返回结果集
  564. while ($result = $this->PDOStatement->fetch($this->fetchType)) {
  565. if ($model) {
  566. yield $model->newInstance($result, $condition);
  567. } else {
  568. yield $result;
  569. }
  570. }
  571. }
  572. /**
  573. * 执行查询 返回数据集
  574. * @access public
  575. * @param string $sql sql指令
  576. * @param array $bind 参数绑定
  577. * @param bool $master 主库读取
  578. * @return array
  579. * @throws DbException
  580. */
  581. public function query(string $sql, array $bind = [], bool $master = false): array
  582. {
  583. return $this->pdoQuery($this->newQuery(), $sql, $bind, $master);
  584. }
  585. /**
  586. * 执行语句
  587. * @access public
  588. * @param string $sql sql指令
  589. * @param array $bind 参数绑定
  590. * @return int
  591. * @throws DbException
  592. */
  593. public function execute(string $sql, array $bind = []): int
  594. {
  595. return $this->pdoExecute($this->newQuery(), $sql, $bind, true);
  596. }
  597. /**
  598. * 执行查询 返回数据集
  599. * @access protected
  600. * @param BaseQuery $query 查询对象
  601. * @param mixed $sql sql指令
  602. * @param array $bind 参数绑定
  603. * @param bool $master 主库读取
  604. * @return array
  605. * @throws DbException
  606. */
  607. protected function pdoQuery(BaseQuery $query, $sql, array $bind = [], bool $master = null): array
  608. {
  609. // 分析查询表达式
  610. $query->parseOptions();
  611. if ($query->getOptions('cache')) {
  612. // 检查查询缓存
  613. $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
  614. $key = $cacheItem->getKey();
  615. $data = $this->cache->get($key);
  616. if (null !== $data) {
  617. return $data;
  618. }
  619. }
  620. if ($sql instanceof Closure) {
  621. $sql = $sql($query);
  622. $bind = $query->getBind();
  623. }
  624. if (!isset($master)) {
  625. $master = $query->getOptions('master') ? true : false;
  626. }
  627. $procedure = $query->getOptions('procedure') ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
  628. $this->getPDOStatement($sql, $bind, $master, $procedure);
  629. $resultSet = $this->getResult($procedure);
  630. $requireCache = $query->getOptions('cache_always') || !empty($resultSet);
  631. if (isset($cacheItem) && $requireCache) {
  632. // 缓存数据集
  633. $cacheItem->set($resultSet);
  634. $this->cacheData($cacheItem);
  635. }
  636. return $resultSet;
  637. }
  638. /**
  639. * 执行查询但只返回PDOStatement对象
  640. * @access public
  641. * @param BaseQuery $query 查询对象
  642. * @return \PDOStatement
  643. * @throws DbException
  644. */
  645. public function pdo(BaseQuery $query): PDOStatement
  646. {
  647. $bind = $query->getBind();
  648. // 生成查询SQL
  649. $sql = $this->builder->select($query);
  650. return $this->queryPDOStatement($query, $sql, $bind);
  651. }
  652. /**
  653. * 执行查询但只返回PDOStatement对象
  654. * @access public
  655. * @param string $sql sql指令
  656. * @param array $bind 参数绑定
  657. * @param bool $master 是否在主服务器读操作
  658. * @param bool $procedure 是否为存储过程调用
  659. * @return PDOStatement
  660. * @throws DbException
  661. */
  662. public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
  663. {
  664. try {
  665. $this->initConnect($this->readMaster ?: $master);
  666. // 记录SQL语句
  667. $this->queryStr = $sql;
  668. $this->bind = $bind;
  669. $this->db->updateQueryTimes();
  670. $this->queryStartTime = microtime(true);
  671. // 预处理
  672. $this->PDOStatement = $this->linkID->prepare($sql);
  673. // 参数绑定
  674. if ($procedure) {
  675. $this->bindParam($bind);
  676. } else {
  677. $this->bindValue($bind);
  678. }
  679. // 执行查询
  680. $this->PDOStatement->execute();
  681. // SQL监控
  682. if (!empty($this->config['trigger_sql'])) {
  683. $this->trigger('', $master);
  684. }
  685. $this->reConnectTimes = 0;
  686. return $this->PDOStatement;
  687. } catch (\Throwable | \Exception $e) {
  688. if ($this->transTimes > 0) {
  689. // 事务活动中时不应该进行重试,应直接中断执行,防止造成污染。
  690. if ($this->isBreak($e)) {
  691. // 尝试对事务计数进行重置
  692. $this->transTimes = 0;
  693. }
  694. } else {
  695. if ($this->reConnectTimes < 4 && $this->isBreak($e)) {
  696. ++$this->reConnectTimes;
  697. return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
  698. }
  699. }
  700. if ($e instanceof \PDOException) {
  701. throw new PDOException($e, $this->config, $this->getLastsql());
  702. } else {
  703. throw $e;
  704. }
  705. }
  706. }
  707. /**
  708. * 执行语句
  709. * @access protected
  710. * @param BaseQuery $query 查询对象
  711. * @param string $sql sql指令
  712. * @param array $bind 参数绑定
  713. * @param bool $origin 是否原生查询
  714. * @return int
  715. * @throws DbException
  716. */
  717. protected function pdoExecute(BaseQuery $query, string $sql, array $bind = [], bool $origin = false): int
  718. {
  719. if ($origin) {
  720. $query->parseOptions();
  721. }
  722. $this->queryPDOStatement($query->master(true), $sql, $bind);
  723. if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
  724. $this->readMaster = true;
  725. }
  726. $this->numRows = $this->PDOStatement->rowCount();
  727. if ($query->getOptions('cache')) {
  728. // 清理缓存数据
  729. $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
  730. $key = $cacheItem->getKey();
  731. $tag = $cacheItem->getTag();
  732. if (isset($key) && $this->cache->has($key)) {
  733. $this->cache->delete($key);
  734. } elseif (!empty($tag) && method_exists($this->cache, 'tag')) {
  735. $this->cache->tag($tag)->clear();
  736. }
  737. }
  738. return $this->numRows;
  739. }
  740. /**
  741. * @param BaseQuery $query
  742. * @param string $sql
  743. * @param array $bind
  744. * @return PDOStatement
  745. * @throws DbException
  746. */
  747. protected function queryPDOStatement(BaseQuery $query, string $sql, array $bind = []): PDOStatement
  748. {
  749. $options = $query->getOptions();
  750. $master = !empty($options['master']) ? true : false;
  751. $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
  752. return $this->getPDOStatement($sql, $bind, $master, $procedure);
  753. }
  754. /**
  755. * 查找单条记录
  756. * @access public
  757. * @param BaseQuery $query 查询对象
  758. * @return array
  759. * @throws DbException
  760. */
  761. public function find(BaseQuery $query): array
  762. {
  763. // 事件回调
  764. try {
  765. $this->db->trigger('before_find', $query);
  766. } catch (DbEventException $e) {
  767. return [];
  768. }
  769. // 执行查询
  770. $resultSet = $this->pdoQuery($query, function ($query) {
  771. return $this->builder->select($query, true);
  772. });
  773. return $resultSet[0] ?? [];
  774. }
  775. /**
  776. * 使用游标查询记录
  777. * @access public
  778. * @param BaseQuery $query 查询对象
  779. * @return \Generator
  780. */
  781. public function cursor(BaseQuery $query)
  782. {
  783. // 分析查询表达式
  784. $options = $query->parseOptions();
  785. // 生成查询SQL
  786. $sql = $this->builder->select($query);
  787. $condition = $options['where']['AND'] ?? null;
  788. // 执行查询操作
  789. return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
  790. }
  791. /**
  792. * 查找记录
  793. * @access public
  794. * @param BaseQuery $query 查询对象
  795. * @return array
  796. * @throws DbException
  797. */
  798. public function select(BaseQuery $query): array
  799. {
  800. try {
  801. $this->db->trigger('before_select', $query);
  802. } catch (DbEventException $e) {
  803. return [];
  804. }
  805. // 执行查询操作
  806. return $this->pdoQuery($query, function ($query) {
  807. return $this->builder->select($query);
  808. });
  809. }
  810. /**
  811. * 插入记录
  812. * @access public
  813. * @param BaseQuery $query 查询对象
  814. * @param boolean $getLastInsID 返回自增主键
  815. * @return mixed
  816. */
  817. public function insert(BaseQuery $query, bool $getLastInsID = false)
  818. {
  819. // 分析查询表达式
  820. $options = $query->parseOptions();
  821. // 生成SQL语句
  822. $sql = $this->builder->insert($query);
  823. // 执行操作
  824. $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
  825. if ($result) {
  826. $sequence = $options['sequence'] ?? null;
  827. $lastInsId = $this->getLastInsID($query, $sequence);
  828. $data = $options['data'];
  829. if ($lastInsId) {
  830. $pk = $query->getAutoInc();
  831. if ($pk) {
  832. $data[$pk] = $lastInsId;
  833. }
  834. }
  835. $query->setOption('data', $data);
  836. $this->db->trigger('after_insert', $query);
  837. if ($getLastInsID && $lastInsId) {
  838. return $lastInsId;
  839. }
  840. }
  841. return $result;
  842. }
  843. /**
  844. * 批量插入记录
  845. * @access public
  846. * @param BaseQuery $query 查询对象
  847. * @param mixed $dataSet 数据集
  848. * @param integer $limit 每次写入数据限制
  849. * @return integer
  850. * @throws \Exception
  851. * @throws \Throwable
  852. */
  853. public function insertAll(BaseQuery $query, array $dataSet = [], int $limit = 0): int
  854. {
  855. if (!is_array(reset($dataSet))) {
  856. return 0;
  857. }
  858. $options = $query->parseOptions();
  859. $replace = !empty($options['replace']);
  860. if (0 === $limit && count($dataSet) >= 5000) {
  861. $limit = 1000;
  862. }
  863. if ($limit) {
  864. // 分批写入 自动启动事务支持
  865. $this->startTrans();
  866. try {
  867. $array = array_chunk($dataSet, $limit, true);
  868. $count = 0;
  869. foreach ($array as $item) {
  870. $sql = $this->builder->insertAll($query, $item, $replace);
  871. $count += $this->pdoExecute($query, $sql, $query->getBind());
  872. }
  873. // 提交事务
  874. $this->commit();
  875. } catch (\Exception | \Throwable $e) {
  876. $this->rollback();
  877. throw $e;
  878. }
  879. return $count;
  880. }
  881. $sql = $this->builder->insertAll($query, $dataSet, $replace);
  882. return $this->pdoExecute($query, $sql, $query->getBind());
  883. }
  884. /**
  885. * 通过Select方式插入记录
  886. * @access public
  887. * @param BaseQuery $query 查询对象
  888. * @param array $fields 要插入的数据表字段名
  889. * @param string $table 要插入的数据表名
  890. * @return integer
  891. * @throws PDOException
  892. */
  893. public function selectInsert(BaseQuery $query, array $fields, string $table): int
  894. {
  895. // 分析查询表达式
  896. $query->parseOptions();
  897. $sql = $this->builder->selectInsert($query, $fields, $table);
  898. return $this->pdoExecute($query, $sql, $query->getBind());
  899. }
  900. /**
  901. * 更新记录
  902. * @access public
  903. * @param BaseQuery $query 查询对象
  904. * @return integer
  905. * @throws PDOException
  906. */
  907. public function update(BaseQuery $query): int
  908. {
  909. $query->parseOptions();
  910. // 生成UPDATE SQL语句
  911. $sql = $this->builder->update($query);
  912. // 执行操作
  913. $result = '' == $sql ? 0 : $this->pdoExecute($query, $sql, $query->getBind());
  914. if ($result) {
  915. $this->db->trigger('after_update', $query);
  916. }
  917. return $result;
  918. }
  919. /**
  920. * 删除记录
  921. * @access public
  922. * @param BaseQuery $query 查询对象
  923. * @return int
  924. * @throws PDOException
  925. */
  926. public function delete(BaseQuery $query): int
  927. {
  928. // 分析查询表达式
  929. $query->parseOptions();
  930. // 生成删除SQL语句
  931. $sql = $this->builder->delete($query);
  932. // 执行操作
  933. $result = $this->pdoExecute($query, $sql, $query->getBind());
  934. if ($result) {
  935. $this->db->trigger('after_delete', $query);
  936. }
  937. return $result;
  938. }
  939. /**
  940. * 得到某个字段的值
  941. * @access public
  942. * @param BaseQuery $query 查询对象
  943. * @param string $field 字段名
  944. * @param mixed $default 默认值
  945. * @param bool $one 返回一个值
  946. * @return mixed
  947. */
  948. public function value(BaseQuery $query, string $field, $default = null, bool $one = true)
  949. {
  950. $options = $query->parseOptions();
  951. if (isset($options['field'])) {
  952. $query->removeOption('field');
  953. }
  954. if (isset($options['group'])) {
  955. $query->group('');
  956. }
  957. $query->setOption('field', (array) $field);
  958. if (!empty($options['cache'])) {
  959. $cacheItem = $this->parseCache($query, $options['cache'], 'value');
  960. $key = $cacheItem->getKey();
  961. if ($this->cache->has($key)) {
  962. return $this->cache->get($key);
  963. }
  964. }
  965. // 生成查询SQL
  966. $sql = $this->builder->select($query, $one);
  967. if (isset($options['field'])) {
  968. $query->setOption('field', $options['field']);
  969. } else {
  970. $query->removeOption('field');
  971. }
  972. if (isset($options['group'])) {
  973. $query->setOption('group', $options['group']);
  974. }
  975. // 执行查询操作
  976. $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
  977. $result = $pdo->fetchColumn();
  978. if (isset($cacheItem)) {
  979. // 缓存数据
  980. $cacheItem->set($result);
  981. $this->cacheData($cacheItem);
  982. }
  983. return false !== $result ? $result : $default;
  984. }
  985. /**
  986. * 得到某个字段的值
  987. * @access public
  988. * @param BaseQuery $query 查询对象
  989. * @param string $aggregate 聚合方法
  990. * @param mixed $field 字段名
  991. * @param bool $force 强制转为数字类型
  992. * @return mixed
  993. */
  994. public function aggregate(BaseQuery $query, string $aggregate, $field, bool $force = false)
  995. {
  996. if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
  997. [$distinct, $field] = explode(' ', $field);
  998. }
  999. $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS think_' . strtolower($aggregate);
  1000. $result = $this->value($query, $field, 0);
  1001. return $force ? (float) $result : $result;
  1002. }
  1003. /**
  1004. * 得到某个列的数组
  1005. * @access public
  1006. * @param BaseQuery $query 查询对象
  1007. * @param string|array $column 字段名 多个字段用逗号分隔
  1008. * @param string $key 索引
  1009. * @return array
  1010. */
  1011. public function column(BaseQuery $query, $column, string $key = ''): array
  1012. {
  1013. $options = $query->parseOptions();
  1014. if (isset($options['field'])) {
  1015. $query->removeOption('field');
  1016. }
  1017. if (empty($key) || trim($key) === '') {
  1018. $key = null;
  1019. }
  1020. if (\is_string($column)) {
  1021. $column = \trim($column);
  1022. if ('*' !== $column) {
  1023. $column = \array_map('\trim', \explode(',', $column));
  1024. }
  1025. } elseif (\is_array($column)) {
  1026. if (\in_array('*', $column)) {
  1027. $column = '*';
  1028. }
  1029. } else {
  1030. throw new DbException('not support type');
  1031. }
  1032. $field = $column;
  1033. if ('*' !== $column && $key && !\in_array($key, $column)) {
  1034. $field[] = $key;
  1035. }
  1036. $query->setOption('field', $field);
  1037. if (!empty($options['cache'])) {
  1038. // 判断查询缓存
  1039. $cacheItem = $this->parseCache($query, $options['cache'], 'column');
  1040. $name = $cacheItem->getKey();
  1041. if ($this->cache->has($name)) {
  1042. return $this->cache->get($name);
  1043. }
  1044. }
  1045. // 生成查询SQL
  1046. $sql = $this->builder->select($query);
  1047. if (isset($options['field'])) {
  1048. $query->setOption('field', $options['field']);
  1049. } else {
  1050. $query->removeOption('field');
  1051. }
  1052. // 执行查询操作
  1053. $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
  1054. $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
  1055. if (is_string($key) && strpos($key, '.')) {
  1056. [$alias, $key] = explode('.', $key);
  1057. }
  1058. if (empty($resultSet)) {
  1059. $result = [];
  1060. } elseif ('*' !== $column && \count($column) === 1) {
  1061. $column = \array_shift($column);
  1062. if (\strpos($column, ' ')) {
  1063. $column = \substr(\strrchr(\trim($column), ' '), 1);
  1064. }
  1065. if (\strpos($column, '.')) {
  1066. [$alias, $column] = \explode('.', $column);
  1067. }
  1068. $result = \array_column($resultSet, $column, $key);
  1069. } elseif ($key) {
  1070. $result = \array_column($resultSet, null, $key);
  1071. } else {
  1072. $result = $resultSet;
  1073. }
  1074. if (isset($cacheItem)) {
  1075. // 缓存数据
  1076. $cacheItem->set($result);
  1077. $this->cacheData($cacheItem);
  1078. }
  1079. return $result;
  1080. }
  1081. /**
  1082. * 根据参数绑定组装最终的SQL语句 便于调试
  1083. * @access public
  1084. * @param string $sql 带参数绑定的sql语句
  1085. * @param array $bind 参数绑定列表
  1086. * @return string
  1087. */
  1088. public function getRealSql(string $sql, array $bind = []): string
  1089. {
  1090. foreach ($bind as $key => $val) {
  1091. $value = strval(is_array($val) ? $val[0] : $val);
  1092. $type = is_array($val) ? $val[1] : PDO::PARAM_STR;
  1093. if (self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) {
  1094. $value = '\'' . addslashes($value) . '\'';
  1095. } elseif (PDO::PARAM_INT == $type && '' === $value) {
  1096. $value = '0';
  1097. }
  1098. // 判断占位符
  1099. $sql = is_numeric($key) ?
  1100. substr_replace($sql, $value, strpos($sql, '?'), 1) :
  1101. substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
  1102. }
  1103. return rtrim($sql);
  1104. }
  1105. /**
  1106. * 参数绑定
  1107. * 支持 ['name'=>'value','id'=>123] 对应命名占位符
  1108. * 或者 ['value',123] 对应问号占位符
  1109. * @access public
  1110. * @param array $bind 要绑定的参数列表
  1111. * @return void
  1112. * @throws BindParamException
  1113. */
  1114. protected function bindValue(array $bind = []): void
  1115. {
  1116. foreach ($bind as $key => $val) {
  1117. // 占位符
  1118. $param = is_numeric($key) ? $key + 1 : ':' . $key;
  1119. if (is_array($val)) {
  1120. if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
  1121. $val[0] = 0;
  1122. } elseif (self::PARAM_FLOAT == $val[1]) {
  1123. $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0];
  1124. $val[1] = PDO::PARAM_STR;
  1125. }
  1126. $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
  1127. } else {
  1128. $result = $this->PDOStatement->bindValue($param, $val);
  1129. }
  1130. if (!$result) {
  1131. throw new BindParamException(
  1132. "Error occurred when binding parameters '{$param}'",
  1133. $this->config,
  1134. $this->getLastsql(),
  1135. $bind
  1136. );
  1137. }
  1138. }
  1139. }
  1140. /**
  1141. * 存储过程的输入输出参数绑定
  1142. * @access public
  1143. * @param array $bind 要绑定的参数列表
  1144. * @return void
  1145. * @throws BindParamException
  1146. */
  1147. protected function bindParam(array $bind): void
  1148. {
  1149. foreach ($bind as $key => $val) {
  1150. $param = is_numeric($key) ? $key + 1 : ':' . $key;
  1151. if (is_array($val)) {
  1152. array_unshift($val, $param);
  1153. $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
  1154. } else {
  1155. $result = $this->PDOStatement->bindValue($param, $val);
  1156. }
  1157. if (!$result) {
  1158. $param = array_shift($val);
  1159. throw new BindParamException(
  1160. "Error occurred when binding parameters '{$param}'",
  1161. $this->config,
  1162. $this->getLastsql(),
  1163. $bind
  1164. );
  1165. }
  1166. }
  1167. }
  1168. /**
  1169. * 获得数据集数组
  1170. * @access protected
  1171. * @param bool $procedure 是否存储过程
  1172. * @return array
  1173. */
  1174. protected function getResult(bool $procedure = false): array
  1175. {
  1176. if ($procedure) {
  1177. // 存储过程返回结果
  1178. return $this->procedure();
  1179. }
  1180. $result = $this->PDOStatement->fetchAll($this->fetchType);
  1181. $this->numRows = count($result);
  1182. return $result;
  1183. }
  1184. /**
  1185. * 获得存储过程数据集
  1186. * @access protected
  1187. * @return array
  1188. */
  1189. protected function procedure(): array
  1190. {
  1191. $item = [];
  1192. do {
  1193. $result = $this->getResult();
  1194. if (!empty($result)) {
  1195. $item[] = $result;
  1196. }
  1197. } while ($this->PDOStatement->nextRowset());
  1198. $this->numRows = count($item);
  1199. return $item;
  1200. }
  1201. /**
  1202. * 执行数据库事务
  1203. * @access public
  1204. * @param callable $callback 数据操作方法回调
  1205. * @return mixed
  1206. * @throws PDOException
  1207. * @throws \Exception
  1208. * @throws \Throwable
  1209. */
  1210. public function transaction(callable $callback)
  1211. {
  1212. $this->startTrans();
  1213. try {
  1214. $result = null;
  1215. if (is_callable($callback)) {
  1216. $result = $callback($this);
  1217. }
  1218. $this->commit();
  1219. return $result;
  1220. } catch (\Exception | \Throwable $e) {
  1221. $this->rollback();
  1222. throw $e;
  1223. }
  1224. }
  1225. /**
  1226. * 启动事务
  1227. * @access public
  1228. * @return void
  1229. * @throws \PDOException
  1230. * @throws \Exception
  1231. */
  1232. public function startTrans(): void
  1233. {
  1234. try {
  1235. $this->initConnect(true);
  1236. ++$this->transTimes;
  1237. if (1 == $this->transTimes) {
  1238. $this->linkID->beginTransaction();
  1239. } elseif ($this->transTimes > 1 && $this->supportSavepoint() && $this->linkID->inTransaction()) {
  1240. $this->linkID->exec(
  1241. $this->parseSavepoint('trans' . $this->transTimes)
  1242. );
  1243. }
  1244. $this->reConnectTimes = 0;
  1245. } catch (\Throwable | \Exception $e) {
  1246. if (1 === $this->transTimes && $this->reConnectTimes < 4 && $this->isBreak($e)) {
  1247. --$this->transTimes;
  1248. ++$this->reConnectTimes;
  1249. $this->close()->startTrans();
  1250. } else {
  1251. if ($this->isBreak($e)) {
  1252. // 尝试对事务计数进行重置
  1253. $this->transTimes = 0;
  1254. }
  1255. throw $e;
  1256. }
  1257. }
  1258. }
  1259. /**
  1260. * 用于非自动提交状态下面的查询提交
  1261. * @access public
  1262. * @return void
  1263. * @throws \PDOException
  1264. */
  1265. public function commit(): void
  1266. {
  1267. $this->initConnect(true);
  1268. if (1 == $this->transTimes && $this->linkID->inTransaction()) {
  1269. $this->linkID->commit();
  1270. }
  1271. --$this->transTimes;
  1272. }
  1273. /**
  1274. * 事务回滚
  1275. * @access public
  1276. * @return void
  1277. * @throws \PDOException
  1278. */
  1279. public function rollback(): void
  1280. {
  1281. $this->initConnect(true);
  1282. if ($this->linkID->inTransaction()) {
  1283. if (1 == $this->transTimes) {
  1284. $this->linkID->rollBack();
  1285. } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
  1286. $this->linkID->exec(
  1287. $this->parseSavepointRollBack('trans' . $this->transTimes)
  1288. );
  1289. }
  1290. }
  1291. $this->transTimes = max(0, $this->transTimes - 1);
  1292. }
  1293. /**
  1294. * 是否支持事务嵌套
  1295. * @return bool
  1296. */
  1297. protected function supportSavepoint(): bool
  1298. {
  1299. return false;
  1300. }
  1301. /**
  1302. * 生成定义保存点的SQL
  1303. * @access protected
  1304. * @param string $name 标识
  1305. * @return string
  1306. */
  1307. protected function parseSavepoint(string $name): string
  1308. {
  1309. return 'SAVEPOINT ' . $name;
  1310. }
  1311. /**
  1312. * 生成回滚到保存点的SQL
  1313. * @access protected
  1314. * @param string $name 标识
  1315. * @return string
  1316. */
  1317. protected function parseSavepointRollBack(string $name): string
  1318. {
  1319. return 'ROLLBACK TO SAVEPOINT ' . $name;
  1320. }
  1321. /**
  1322. * 批处理执行SQL语句
  1323. * 批处理的指令都认为是execute操作
  1324. * @access public
  1325. * @param BaseQuery $query 查询对象
  1326. * @param array $sqlArray SQL批处理指令
  1327. * @param array $bind 参数绑定
  1328. * @return bool
  1329. */
  1330. public function batchQuery(BaseQuery $query, array $sqlArray = [], array $bind = []): bool
  1331. {
  1332. // 自动启动事务支持
  1333. $this->startTrans();
  1334. try {
  1335. foreach ($sqlArray as $sql) {
  1336. $this->pdoExecute($query, $sql, $bind);
  1337. }
  1338. // 提交事务
  1339. $this->commit();
  1340. } catch (\Exception $e) {
  1341. $this->rollback();
  1342. throw $e;
  1343. }
  1344. return true;
  1345. }
  1346. /**
  1347. * 关闭数据库(或者重新连接)
  1348. * @access public
  1349. * @return $this
  1350. */
  1351. public function close()
  1352. {
  1353. $this->linkID = null;
  1354. $this->linkWrite = null;
  1355. $this->linkRead = null;
  1356. $this->links = [];
  1357. $this->transTimes = 0;
  1358. $this->free();
  1359. return $this;
  1360. }
  1361. /**
  1362. * 是否断线
  1363. * @access protected
  1364. * @param \PDOException|\Exception $e 异常对象
  1365. * @return bool
  1366. */
  1367. protected function isBreak($e): bool
  1368. {
  1369. if (!$this->config['break_reconnect']) {
  1370. return false;
  1371. }
  1372. $error = $e->getMessage();
  1373. foreach ($this->breakMatchStr as $msg) {
  1374. if (false !== stripos($error, $msg)) {
  1375. return true;
  1376. }
  1377. }
  1378. return false;
  1379. }
  1380. /**
  1381. * 获取最近一次查询的sql语句
  1382. * @access public
  1383. * @return string
  1384. */
  1385. public function getLastSql(): string
  1386. {
  1387. return $this->getRealSql($this->queryStr, $this->bind);
  1388. }
  1389. /**
  1390. * 获取最近插入的ID
  1391. * @access public
  1392. * @param BaseQuery $query 查询对象
  1393. * @param string $sequence 自增序列名
  1394. * @return mixed
  1395. */
  1396. public function getLastInsID(BaseQuery $query, string $sequence = null)
  1397. {
  1398. try {
  1399. $insertId = $this->linkID->lastInsertId($sequence);
  1400. } catch (\Exception $e) {
  1401. $insertId = '';
  1402. }
  1403. return $this->autoInsIDType($query, $insertId);
  1404. }
  1405. /**
  1406. * 获取最近插入的ID
  1407. * @access public
  1408. * @param BaseQuery $query 查询对象
  1409. * @param string $insertId 自增ID
  1410. * @return mixed
  1411. */
  1412. protected function autoInsIDType(BaseQuery $query, string $insertId)
  1413. {
  1414. $pk = $query->getAutoInc();
  1415. if ($pk) {
  1416. $type = $this->getFieldBindType($pk);
  1417. if (PDO::PARAM_INT == $type) {
  1418. $insertId = (int) $insertId;
  1419. } elseif (self::PARAM_FLOAT == $type) {
  1420. $insertId = (float) $insertId;
  1421. }
  1422. }
  1423. return $insertId;
  1424. }
  1425. /**
  1426. * 获取最近的错误信息
  1427. * @access public
  1428. * @return string
  1429. */
  1430. public function getError(): string
  1431. {
  1432. if ($this->PDOStatement) {
  1433. $error = $this->PDOStatement->errorInfo();
  1434. $error = $error[1] . ':' . $error[2];
  1435. } else {
  1436. $error = '';
  1437. }
  1438. if ('' != $this->queryStr) {
  1439. $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
  1440. }
  1441. return $error;
  1442. }
  1443. /**
  1444. * 初始化数据库连接
  1445. * @access protected
  1446. * @param boolean $master 是否主服务器
  1447. * @return void
  1448. */
  1449. protected function initConnect(bool $master = true): void
  1450. {
  1451. if (!empty($this->config['deploy'])) {
  1452. // 采用分布式数据库
  1453. if ($master || $this->transTimes) {
  1454. if (!$this->linkWrite) {
  1455. $this->linkWrite = $this->multiConnect(true);
  1456. }
  1457. $this->linkID = $this->linkWrite;
  1458. } else {
  1459. if (!$this->linkRead) {
  1460. $this->linkRead = $this->multiConnect(false);
  1461. }
  1462. $this->linkID = $this->linkRead;
  1463. }
  1464. } elseif (!$this->linkID) {
  1465. // 默认单数据库
  1466. $this->linkID = $this->connect();
  1467. }
  1468. }
  1469. /**
  1470. * 连接分布式服务器
  1471. * @access protected
  1472. * @param boolean $master 主服务器
  1473. * @return PDO
  1474. */
  1475. protected function multiConnect(bool $master = false): PDO
  1476. {
  1477. $config = [];
  1478. // 分布式数据库配置解析
  1479. foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
  1480. $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
  1481. }
  1482. // 主服务器序号
  1483. $m = floor(mt_rand(0, $this->config['master_num'] - 1));
  1484. if ($this->config['rw_separate']) {
  1485. // 主从式采用读写分离
  1486. if ($master) // 主服务器写入
  1487. {
  1488. $r = $m;
  1489. } elseif (is_numeric($this->config['slave_no'])) {
  1490. // 指定服务器读
  1491. $r = $this->config['slave_no'];
  1492. } else {
  1493. // 读操作连接从服务器 每次随机连接的数据库
  1494. $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
  1495. }
  1496. } else {
  1497. // 读写操作不区分服务器 每次随机连接的数据库
  1498. $r = floor(mt_rand(0, count($config['hostname']) - 1));
  1499. }
  1500. $dbMaster = false;
  1501. if ($m != $r) {
  1502. $dbMaster = [];
  1503. foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
  1504. $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
  1505. }
  1506. }
  1507. $dbConfig = [];
  1508. foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
  1509. $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
  1510. }
  1511. return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
  1512. }
  1513. /**
  1514. * 执行数据库Xa事务
  1515. * @access public
  1516. * @param callable $callback 数据操作方法回调
  1517. * @param array $dbs 多个查询对象或者连接对象
  1518. * @return mixed
  1519. * @throws PDOException
  1520. * @throws \Exception
  1521. * @throws \Throwable
  1522. */
  1523. public function transactionXa(callable $callback, array $dbs = [])
  1524. {
  1525. $xid = uniqid('xa');
  1526. if (empty($dbs)) {
  1527. $dbs[] = $this;
  1528. }
  1529. foreach ($dbs as $key => $db) {
  1530. if ($db instanceof BaseQuery) {
  1531. $db = $db->getConnection();
  1532. $dbs[$key] = $db;
  1533. }
  1534. $db->startTransXa($xid);
  1535. }
  1536. try {
  1537. $result = null;
  1538. if (is_callable($callback)) {
  1539. $result = $callback($this);
  1540. }
  1541. foreach ($dbs as $db) {
  1542. $db->prepareXa($xid);
  1543. }
  1544. foreach ($dbs as $db) {
  1545. $db->commitXa($xid);
  1546. }
  1547. return $result;
  1548. } catch (\Exception | \Throwable $e) {
  1549. foreach ($dbs as $db) {
  1550. $db->rollbackXa($xid);
  1551. }
  1552. throw $e;
  1553. }
  1554. }
  1555. /**
  1556. * 启动XA事务
  1557. * @access public
  1558. * @param string $xid XA事务id
  1559. * @return void
  1560. */
  1561. public function startTransXa(string $xid): void
  1562. {}
  1563. /**
  1564. * 预编译XA事务
  1565. * @access public
  1566. * @param string $xid XA事务id
  1567. * @return void
  1568. */
  1569. public function prepareXa(string $xid): void
  1570. {}
  1571. /**
  1572. * 提交XA事务
  1573. * @access public
  1574. * @param string $xid XA事务id
  1575. * @return void
  1576. */
  1577. public function commitXa(string $xid): void
  1578. {}
  1579. /**
  1580. * 回滚XA事务
  1581. * @access public
  1582. * @param string $xid XA事务id
  1583. * @return void
  1584. */
  1585. public function rollbackXa(string $xid): void
  1586. {}
  1587. }