Passed
Push — 5.2 ( da0c84...54339d )
by liu
02:41
created

Connection::getQueryTimes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 <[email protected]>
10
// +----------------------------------------------------------------------
11
declare (strict_types = 1);
12
13
namespace think\db;
14
15
use InvalidArgumentException;
16
use PDO;
17
use PDOStatement;
18
use think\App;
19
use think\Cache;
20
use think\cache\CacheItem;
21
use think\Container;
22
use think\Db;
23
use think\db\exception\BindParamException;
24
use think\Debug;
25
use think\Exception;
26
use think\exception\PDOException;
27
28
abstract class Connection
29
{
30
    const PARAM_FLOAT = 21;
31
32
    /**
33
     * 数据库连接实例
34
     * @var array
35
     */
36
    protected static $instance = [];
37
38
    /**
39
     * PDO操作实例
40
     * @var PDOStatement
41
     */
42
    protected $PDOStatement;
43
44
    /**
45
     * 当前SQL指令
46
     * @var string
47
     */
48
    protected $queryStr = '';
49
50
    /**
51
     * 返回或者影响记录数
52
     * @var int
53
     */
54
    protected $numRows = 0;
55
56
    /**
57
     * 事务指令数
58
     * @var int
59
     */
60
    protected $transTimes = 0;
61
62
    /**
63
     * 错误信息
64
     * @var string
65
     */
66
    protected $error = '';
67
68
    /**
69
     * 查询次数
70
     * @var int
71
     */
72
    protected static $queryTimes = 0;
73
74
    /**
75
     * 数据库连接ID 支持多个连接
76
     * @var PDO[]
77
     */
78
    protected $links = [];
79
80
    /**
81
     * 当前连接ID
82
     * @var PDO
83
     */
84
    protected $linkID;
85
86
    /**
87
     * 当前读连接ID
88
     * @var PDO
89
     */
90
    protected $linkRead;
91
92
    /**
93
     * 当前写连接ID
94
     * @var PDO
95
     */
96
    protected $linkWrite;
97
98
    /**
99
     * 查询结果类型
100
     * @var int
101
     */
102
    protected $fetchType = PDO::FETCH_ASSOC;
103
104
    /**
105
     * 字段属性大小写
106
     * @var int
107
     */
108
    protected $attrCase = PDO::CASE_LOWER;
109
110
    /**
111
     * 监听回调
112
     * @var array
113
     */
114
    protected static $event = [];
115
116
    /**
117
     * 数据表信息
118
     * @var array
119
     */
120
    protected static $info = [];
121
122
    /**
123
     * Builder类名
124
     * @var string
125
     */
126
    protected $builderClassName;
127
128
    /**
129
     * Builder对象
130
     * @var Builder
131
     */
132
    protected $builder;
133
134
    /**
135
     * 数据库连接参数配置
136
     * @var array
137
     */
138
    protected $config = [
139
        // 数据库类型
140
        'type'            => '',
141
        // 服务器地址
142
        'hostname'        => '',
143
        // 数据库名
144
        'database'        => '',
145
        // 用户名
146
        'username'        => '',
147
        // 密码
148
        'password'        => '',
149
        // 端口
150
        'hostport'        => '',
151
        // 连接dsn
152
        'dsn'             => '',
153
        // 数据库连接参数
154
        'params'          => [],
155
        // 数据库编码默认采用utf8
156
        'charset'         => 'utf8',
157
        // 数据库表前缀
158
        'prefix'          => '',
159
        // 数据库调试模式
160
        'debug'           => false,
161
        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
162
        'deploy'          => 0,
163
        // 数据库读写是否分离 主从式有效
164
        'rw_separate'     => false,
165
        // 读写分离后 主服务器数量
166
        'master_num'      => 1,
167
        // 指定从服务器序号
168
        'slave_no'        => '',
169
        // 模型写入后自动读取主服务器
170
        'read_master'     => false,
171
        // 是否严格检查字段是否存在
172
        'fields_strict'   => true,
173
        // 自动写入时间戳字段
174
        'auto_timestamp'  => false,
175
        // 时间字段取出后的默认时间格式
176
        'datetime_format' => 'Y-m-d H:i:s',
177
        // 是否需要进行SQL性能分析
178
        'sql_explain'     => false,
179
        // Builder类
180
        'builder'         => '',
181
        // Query类
182
        'query'           => '\\think\\db\\Query',
183
        // 是否需要断线重连
184
        'break_reconnect' => false,
185
        // 断线标识字符串
186
        'break_match_str' => [],
187
    ];
188
189
    /**
190
     * PDO连接参数
191
     * @var array
192
     */
193
    protected $params = [
194
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
195
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
196
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
197
        PDO::ATTR_STRINGIFY_FETCHES => false,
198
        PDO::ATTR_EMULATE_PREPARES  => false,
199
    ];
200
201
    /**
202
     * 参数绑定类型映射
203
     * @var array
204
     */
205
    protected $bindType = [
206
        'string'  => PDO::PARAM_STR,
207
        'str'     => PDO::PARAM_STR,
208
        'integer' => PDO::PARAM_INT,
209
        'int'     => PDO::PARAM_INT,
210
        'boolean' => PDO::PARAM_BOOL,
211
        'bool'    => PDO::PARAM_BOOL,
212
        'float'   => self::PARAM_FLOAT,
213
    ];
214
215
    /**
216
     * 服务器断线标识字符
217
     * @var array
218
     */
219
    protected $breakMatchStr = [
220
        'server has gone away',
221
        'no connection to the server',
222
        'Lost connection',
223
        'is dead or not enabled',
224
        'Error while sending',
225
        'decryption failed or bad record mac',
226
        'server closed the connection unexpectedly',
227
        'SSL connection has been closed unexpectedly',
228
        'Error writing data to the connection',
229
        'Resource deadlock avoided',
230
        'failed with errno',
231
    ];
232
233
    /**
234
     * 绑定参数
235
     * @var array
236
     */
237
    protected $bind = [];
238
239
    /**
240
     * 缓存对象
241
     * @var Cache
242
     */
243
    protected $cache;
244
245
    /**
246
     * 架构函数 读取数据库配置信息
247
     * @access public
248
     * @param  array $config 数据库配置数组
249
     */
250
    public function __construct(array $config = [])
251
    {
252
        if (!empty($config)) {
253
            $this->config = array_merge($this->config, $config);
254
        }
255
256
        // 创建Builder对象
257
        $class = $this->getBuilderClass();
258
259
        $this->builder = new $class($this);
260
        $this->cache   = Container::pull('cache');
261
262
        // 执行初始化操作
263
        $this->initialize();
264
    }
265
266
    /**
267
     * 初始化
268
     * @access protected
269
     * @return void
270
     */
271
    protected function initialize(): void
272
    {}
273
274
    /**
275
     * 取得数据库连接类实例
276
     * @access public
277
     * @param  array         $config 连接配置
278
     * @param  bool|string   $name 连接标识 true 强制重新连接
279
     * @return Connection
280
     * @throws Exception
281
     */
282
    public static function instance(array $config = [], $name = false): Connection
283
    {
284
        if (false === $name) {
285
            $name = md5(serialize($config));
286
        }
287
288
        if (true === $name || !isset(self::$instance[$name])) {
289
290
            if (empty($config['type'])) {
291
                throw new InvalidArgumentException('Undefined db type');
292
            }
293
294
            // 记录初始化信息
295
            Container::pull('log')->record('[ DB ] INIT ' . $config['type']);
296
297
            if (true === $name) {
298
                $name = md5(serialize($config));
299
            }
300
301
            self::$instance[$name] = App::factory($config['type'], '\\think\\db\\connector\\', $config);
302
        }
303
304
        return self::$instance[$name];
305
    }
306
307
    /**
308
     * 获取当前连接器类对应的Builder类
309
     * @access public
310
     * @return string
311
     */
312
    public function getBuilderClass(): string
313
    {
314
        if (!empty($this->builderClassName)) {
315
            return $this->builderClassName;
316
        }
317
318
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
319
    }
320
321
    /**
322
     * 设置当前的数据库Builder对象
323
     * @access protected
324
     * @param  Builder    $builder
325
     * @return $this
326
     */
327
    protected function setBuilder(Builder $builder)
328
    {
329
        $this->builder = $builder;
330
331
        return $this;
332
    }
333
334
    /**
335
     * 获取当前的builder实例对象
336
     * @access public
337
     * @return Builder
338
     */
339
    public function getBuilder(): Builder
340
    {
341
        return $this->builder;
342
    }
343
344
    /**
345
     * 解析pdo连接的dsn信息
346
     * @access protected
347
     * @param  array $config 连接信息
348
     * @return string
349
     */
350
    abstract protected function parseDsn(array $config);
351
352
    /**
353
     * 取得数据表的字段信息
354
     * @access public
355
     * @param  string $tableName
356
     * @return array
357
     */
358
    abstract public function getFields(string $tableName);
359
360
    /**
361
     * 取得数据库的表信息
362
     * @access public
363
     * @param string $dbName
364
     * @return array
365
     */
366
    abstract public function getTables(string $dbName);
367
368
    /**
369
     * SQL性能分析
370
     * @access protected
371
     * @param  string $sql
372
     * @return array
373
     */
374
    abstract protected function getExplain(string $sql);
375
376
    /**
377
     * 对返数据表字段信息进行大小写转换出来
378
     * @access public
379
     * @param  array $info 字段信息
380
     * @return array
381
     */
382
    public function fieldCase(array $info): array
383
    {
384
        // 字段大小写转换
385
        switch ($this->attrCase) {
386
            case PDO::CASE_LOWER:
387
                $info = array_change_key_case($info);
388
                break;
389
            case PDO::CASE_UPPER:
390
                $info = array_change_key_case($info, CASE_UPPER);
391
                break;
392
            case PDO::CASE_NATURAL:
393
            default:
394
                // 不做转换
395
        }
396
397
        return $info;
398
    }
399
400
    /**
401
     * 获取字段绑定类型
402
     * @access public
403
     * @param  string $type 字段类型
404
     * @return integer
405
     */
406
    public function getFieldBindType(string $type): int
407
    {
408
        if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
409
            $bind = $this->bindType[$type];
410
        } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
411
            $bind = PDO::PARAM_STR;
412
        } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
413
            $bind = self::PARAM_FLOAT;
414
        } elseif (preg_match('/(int|serial|bit)/is', $type)) {
415
            $bind = PDO::PARAM_INT;
416
        } elseif (preg_match('/bool/is', $type)) {
417
            $bind = PDO::PARAM_BOOL;
418
        } else {
419
            $bind = PDO::PARAM_STR;
420
        }
421
422
        return $bind;
423
    }
424
425
    /**
426
     * 获取数据表信息
427
     * @access public
428
     * @param  mixed  $tableName 数据表名 留空自动获取
429
     * @param  string $fetch     获取信息类型 包括 fields type bind pk
430
     * @return mixed
431
     */
432
    public function getTableInfo($tableName, string $fetch = '')
433
    {
434
        if (is_array($tableName)) {
435
            $tableName = key($tableName) ?: current($tableName);
436
        }
437
438
        if (strpos($tableName, ',')) {
439
            // 多表不获取字段信息
440
            return [];
441
        }
442
443
        // 修正子查询作为表名的问题
444
        if (strpos($tableName, ')')) {
445
            return [];
446
        }
447
448
        list($tableName) = explode(' ', $tableName);
449
450
        if (!strpos($tableName, '.')) {
451
            $schema = $this->getConfig('database') . '.' . $tableName;
452
        } else {
453
            $schema = $tableName;
454
        }
455
456
        if (!isset(self::$info[$schema])) {
457
            // 读取缓存
458
            $cacheFile = Container::pull('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php';
459
460
            if (!$this->config['debug'] && is_file($cacheFile)) {
461
                $info = include $cacheFile;
462
            } else {
463
                $info = $this->getFields($tableName);
464
            }
465
466
            $fields = array_keys($info);
467
            $bind   = $type   = [];
468
469
            foreach ($info as $key => $val) {
470
                // 记录字段类型
471
                $type[$key] = $val['type'];
472
                $bind[$key] = $this->getFieldBindType($val['type']);
473
474
                if (!empty($val['primary'])) {
475
                    $pk[] = $key;
476
                }
477
            }
478
479
            if (isset($pk)) {
480
                // 设置主键
481
                $pk = count($pk) > 1 ? $pk : $pk[0];
482
            } else {
483
                $pk = null;
484
            }
485
486
            self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
487
        }
488
489
        return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema];
490
    }
491
492
    /**
493
     * 获取数据表的主键
494
     * @access public
495
     * @param  string $tableName 数据表名
496
     * @return string|array
497
     */
498
    public function getPk($tableName)
499
    {
500
        return $this->getTableInfo($tableName, 'pk');
501
    }
502
503
    /**
504
     * 获取数据表字段信息
505
     * @access public
506
     * @param  string $tableName 数据表名
507
     * @return array
508
     */
509
    public function getTableFields($tableName): array
510
    {
511
        return $this->getTableInfo($tableName, 'fields');
512
    }
513
514
    /**
515
     * 获取数据表字段类型
516
     * @access public
517
     * @param  string $tableName 数据表名
518
     * @param  string $field    字段名
519
     * @return array|string
520
     */
521
    public function getFieldsType($tableName, string $field = null)
522
    {
523
        $result = $this->getTableInfo($tableName, 'type');
524
525
        if ($field && isset($result[$field])) {
526
            return $result[$field];
527
        }
528
529
        return $result;
530
    }
531
532
    /**
533
     * 获取数据表绑定信息
534
     * @access public
535
     * @param  string $tableName 数据表名
536
     * @return array
537
     */
538
    public function getFieldsBind($tableName): array
539
    {
540
        return $this->getTableInfo($tableName, 'bind');
541
    }
542
543
    /**
544
     * 获取数据库的配置参数
545
     * @access public
546
     * @param  string $config 配置名称
547
     * @return mixed
548
     */
549
    public function getConfig(string $config = '')
550
    {
551
        if ('' === $config) {
552
            return $this->config;
553
        }
554
        return $this->config[$config] ?? null;
555
    }
556
557
    /**
558
     * 设置数据库的配置参数
559
     * @access public
560
     * @param  array      $config 配置
561
     * @return void
562
     */
563
    public function setConfig($config): void
564
    {
565
        $this->config = array_merge($this->config, $config);
566
    }
567
568
    /**
569
     * 连接数据库方法
570
     * @access public
571
     * @param  array         $config 连接参数
572
     * @param  integer       $linkNum 连接序号
573
     * @param  array|bool    $autoConnection 是否自动连接主数据库(用于分布式)
574
     * @return PDO
575
     * @throws Exception
576
     */
577
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
578
    {
579
        if (isset($this->links[$linkNum])) {
580
            return $this->links[$linkNum];
581
        }
582
583
        if (empty($config)) {
584
            $config = $this->config;
585
        } else {
586
            $config = array_merge($this->config, $config);
587
        }
588
589
        // 连接参数
590
        if (isset($config['params']) && is_array($config['params'])) {
591
            $params = $config['params'] + $this->params;
592
        } else {
593
            $params = $this->params;
594
        }
595
596
        // 记录当前字段属性大小写设置
597
        $this->attrCase = $params[PDO::ATTR_CASE];
598
599
        if (!empty($config['break_match_str'])) {
600
            $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
601
        }
602
603
        try {
604
            if (empty($config['dsn'])) {
605
                $config['dsn'] = $this->parseDsn($config);
606
            }
607
608
            if ($config['debug']) {
609
                $startTime             = microtime(true);
610
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
611
                // 记录数据库连接信息
612
                $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
613
            } else {
614
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
615
            }
616
617
            return $this->links[$linkNum];
618
        } catch (\PDOException $e) {
619
            if ($autoConnection) {
620
                $this->log($e->getMessage(), 'error');
621
                return $this->connect([], $linkNum);
622
            } else {
623
                throw $e;
624
            }
625
        }
626
    }
627
628
    /**
629
     * 释放查询结果
630
     * @access public
631
     */
632
    public function free(): void
633
    {
634
        $this->PDOStatement = null;
635
    }
636
637
    /**
638
     * 获取PDO对象
639
     * @access public
640
     * @return \PDO|false
641
     */
642
    public function getPdo()
643
    {
644
        if (!$this->linkID) {
645
            return false;
646
        }
647
648
        return $this->linkID;
649
    }
650
651
    /**
652
     * 执行查询 使用生成器返回数据
653
     * @access public
654
     * @param  Query            $query 查询对象
655
     * @param  string           $sql sql指令
656
     * @param  array            $bind 参数绑定
657
     * @param  \think\Model     $model 模型对象实例
658
     * @param  array            $condition 查询条件
659
     * @return \Generator
660
     */
661
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
662
    {
663
        $this->queryPDOStatement($query, $sql, $bind);
664
665
        // 返回结果集
666
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
667
            if ($model) {
668
                yield $model->newInstance($result, true, $condition);
669
            } else {
670
                yield $result;
671
            }
672
        }
673
    }
674
675
    /**
676
     * 执行查询 返回数据集
677
     * @access public
678
     * @param  Query     $query 查询对象
679
     * @param  string    $sql sql指令
680
     * @param  array     $bind 参数绑定
681
     * @param  bool      $cache 是否支持缓存
682
     * @return array
683
     * @throws BindParamException
684
     * @throws \PDOException
685
     * @throws \Exception
686
     * @throws \Throwable
687
     */
688
    public function query(Query $query, string $sql, array $bind = [], bool $cache = false): array
689
    {
690
        // 分析查询表达式
691
        $options = $query->parseOptions();
692
693
        if ($cache && !empty($options['cache'])) {
694
            $cacheItem = $this->parseCache($query, $options['cache']);
695
            $resultSet = $this->cache->get($cacheItem->getKey());
696
697
            if (false !== $resultSet) {
698
                return $resultSet;
699
            }
700
        }
701
702
        $master    = !empty($options['master']) ? true : false;
703
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
704
705
        $this->getPDOStatement($sql, $bind, $master, $procedure);
706
707
        $resultSet = $this->getResult($procedure);
708
709
        if (isset($cacheItem) && $resultSet) {
710
            // 缓存数据集
711
            $cacheItem->set($resultSet);
712
            $this->cacheData($cacheItem);
713
        }
714
715
        return $resultSet;
716
    }
717
718
    /**
719
     * 执行查询但只返回PDOStatement对象
720
     * @access public
721
     * @param  Query         $query 查询对象
722
     * @return \PDOStatement
723
     */
724
    public function pdo(Query $query): PDOStatement
725
    {
726
        $bind = $query->getBind();
727
        // 生成查询SQL
728
        $sql = $this->builder->select($query);
729
730
        return $this->queryPDOStatement($query, $sql, $bind);
731
    }
732
733
    /**
734
     * 执行查询但只返回PDOStatement对象
735
     * @access public
736
     * @param  string    $sql sql指令
737
     * @param  array     $bind 参数绑定
738
     * @param  bool      $master 是否在主服务器读操作
739
     * @param  bool      $procedure 是否为存储过程调用
740
     * @return PDOStatement
741
     * @throws BindParamException
742
     * @throws \PDOException
743
     * @throws \Exception
744
     * @throws \Throwable
745
     */
746
    public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
747
    {
748
        $this->initConnect($master);
749
750
        // 记录SQL语句
751
        $this->queryStr = $sql;
752
753
        $this->bind = $bind;
754
755
        self::$queryTimes++;
756
757
        try {
758
            // 调试开始
759
            $this->debug(true);
760
761
            // 预处理
762
            $this->PDOStatement = $this->linkID->prepare($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->linkID->prepare($sql) can also be of type boolean. However, the property $PDOStatement is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
763
764
            // 参数绑定
765
            if ($procedure) {
766
                $this->bindParam($bind);
767
            } else {
768
                $this->bindValue($bind);
769
            }
770
771
            // 执行查询
772
            $this->PDOStatement->execute();
773
774
            // 调试结束
775
            $this->debug(false, '', $master);
776
777
            return $this->PDOStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->PDOStatement could return the type boolean which is incompatible with the type-hinted return PDOStatement. Consider adding an additional type-check to rule them out.
Loading history...
778
        } catch (\Throwable | \Exception $e) {
779
            if ($this->isBreak($e)) {
780
                return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
781
            }
782
783
            if ($e instanceof \PDOException) {
784
                throw new PDOException($e, $this->config, $this->getLastsql());
785
            } else {
786
                throw $e;
787
            }
788
        }
789
    }
790
791
    /**
792
     * 执行语句
793
     * @access public
794
     * @param  Query         $query 查询对象
795
     * @param  string        $sql sql指令
796
     * @param  array         $bind 参数绑定
797
     * @return int
798
     * @throws BindParamException
799
     * @throws \PDOException
800
     * @throws \Exception
801
     * @throws \Throwable
802
     */
803
    public function execute(Query $query, string $sql, array $bind = []): int
804
    {
805
        $this->queryPDOStatement($query->master(true), $sql, $bind);
806
807
        $this->numRows = $this->PDOStatement->rowCount();
808
809
        return $this->numRows;
810
    }
811
812
    protected function queryPDOStatement(Query $query, string $sql, array $bind = []): PDOStatement
813
    {
814
        $options   = $query->parseOptions();
815
        $master    = !empty($options['master']) ? true : false;
816
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
817
818
        return $this->getPDOStatement($sql, $bind, $master, $procedure);
819
    }
820
821
    /**
822
     * 查找单条记录
823
     * @access public
824
     * @param  Query  $query        查询对象
825
     * @return array
826
     * @throws DbException
827
     * @throws ModelNotFoundException
828
     * @throws DataNotFoundException
829
     */
830
    public function find(Query $query): array
831
    {
832
        // 分析查询表达式
833
        $options = $query->parseOptions();
834
835
        if (!empty($options['cache'])) {
836
            // 判断查询缓存
837
            $cacheItem = $this->parseCache($query, $options['cache']);
838
            $key       = $cacheItem->getKey();
839
        }
840
841
        if (isset($key)) {
842
            $result = $this->cache->get($key);
843
844
            if (false !== $result) {
845
                return $result;
846
            }
847
        }
848
849
        // 生成查询SQL
850
        $sql = $this->builder->select($query, true);
851
852
        // 事件回调
853
        $result = $query->trigger('before_find');
854
855
        if (!$result) {
856
            // 执行查询
857
            $resultSet = $this->query($query, $sql, $query->getBind());
858
859
            $result = $resultSet[0] ?? [];
860
        }
861
862
        if (isset($cacheItem) && $result) {
863
            // 缓存数据
864
            $cacheItem->set($result);
865
            $this->cacheData($cacheItem);
866
        }
867
868
        return $result;
869
    }
870
871
    /**
872
     * 使用游标查询记录
873
     * @access public
874
     * @param  Query   $query        查询对象
875
     * @return \Generator
876
     */
877
    public function cursor(Query $query)
878
    {
879
        // 分析查询表达式
880
        $options = $query->parseOptions();
881
882
        // 生成查询SQL
883
        $sql = $this->builder->select($query);
884
885
        $condition = $options['where']['AND'] ?? null;
886
887
        // 执行查询操作
888
        return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
889
    }
890
891
    /**
892
     * 查找记录
893
     * @access public
894
     * @param  Query   $query        查询对象
895
     * @return array
896
     * @throws DbException
897
     * @throws ModelNotFoundException
898
     * @throws DataNotFoundException
899
     */
900
    public function select(Query $query): array
901
    {
902
        // 分析查询表达式
903
        $options = $query->parseOptions();
904
905
        if (!empty($options['cache'])) {
906
            $cacheItem = $this->parseCache($query, $options['cache']);
907
            $resultSet = $this->getCacheData($cacheItem);
908
909
            if (false !== $resultSet) {
910
                return $resultSet;
911
            }
912
        }
913
914
        // 生成查询SQL
915
        $sql = $this->builder->select($query);
916
917
        $resultSet = $query->trigger('before_select');
918
919
        if (!$resultSet) {
920
            // 执行查询操作
921
            $resultSet = $this->query($query, $sql, $query->getBind());
922
        }
923
924
        if (isset($cacheItem) && false !== $resultSet) {
925
            // 缓存数据集
926
            $cacheItem->set($resultSet);
927
            $this->cacheData($cacheItem);
928
        }
929
930
        return $resultSet;
931
    }
932
933
    /**
934
     * 插入记录
935
     * @access public
936
     * @param  Query   $query        查询对象
937
     * @param  boolean $replace      是否replace
938
     * @param  boolean $getLastInsID 返回自增主键
939
     * @param  string  $sequence     自增序列名
940
     * @return integer
941
     */
942
    public function insert(Query $query, bool $replace = false, bool $getLastInsID = false, string $sequence = null)
943
    {
944
        // 分析查询表达式
945
        $options = $query->parseOptions();
946
947
        // 生成SQL语句
948
        $sql = $this->builder->insert($query, $replace);
949
950
        // 执行操作
951
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
952
953
        if ($result) {
954
            $sequence  = $sequence ?: ($options['sequence'] ?? null);
955
            $lastInsId = $this->getLastInsID($sequence);
956
957
            $data = $options['data'];
958
959
            if ($lastInsId) {
960
                $pk = $query->getPk();
961
                if (is_string($pk)) {
962
                    $data[$pk] = $lastInsId;
963
                }
964
            }
965
966
            $query->setOption('data', $data);
967
968
            $query->trigger('after_insert');
969
970
            if ($getLastInsID) {
971
                return $lastInsId;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $lastInsId returns the type string which is incompatible with the documented return type integer.
Loading history...
972
            }
973
        }
974
975
        return $result;
976
    }
977
978
    /**
979
     * 批量插入记录
980
     * @access public
981
     * @param  Query     $query      查询对象
982
     * @param  mixed     $dataSet    数据集
983
     * @param  bool      $replace    是否replace
984
     * @param  integer   $limit      每次写入数据限制
985
     * @return integer
986
     * @throws \Exception
987
     * @throws \Throwable
988
     */
989
    public function insertAll(Query $query, array $dataSet = [], bool $replace = false, int $limit = 0): int
990
    {
991
        if (!is_array(reset($dataSet))) {
992
            return 0;
993
        }
994
995
        $options = $query->parseOptions();
0 ignored issues
show
Unused Code introduced by
The assignment to $options is dead and can be removed.
Loading history...
996
997
        if ($limit) {
998
            // 分批写入 自动启动事务支持
999
            $this->startTrans();
1000
1001
            try {
1002
                $array = array_chunk($dataSet, $limit, true);
1003
                $count = 0;
1004
1005
                foreach ($array as $item) {
1006
                    $sql = $this->builder->insertAll($query, $item, $replace);
1007
                    $count += $this->execute($query, $sql, $query->getBind());
1008
                }
1009
1010
                // 提交事务
1011
                $this->commit();
1012
            } catch (\Exception | \Throwable $e) {
1013
                $this->rollback();
1014
                throw $e;
1015
            }
1016
1017
            return $count;
1018
        }
1019
1020
        $sql = $this->builder->insertAll($query, $dataSet, $replace);
1021
1022
        return $this->execute($query, $sql, $query->getBind());
1023
    }
1024
1025
    /**
1026
     * 通过Select方式插入记录
1027
     * @access public
1028
     * @param  Query     $query      查询对象
1029
     * @param  array     $fields     要插入的数据表字段名
1030
     * @param  string    $table      要插入的数据表名
1031
     * @return integer
1032
     * @throws PDOException
1033
     */
1034
    public function selectInsert(Query $query, array $fields, string $table): int
1035
    {
1036
        // 分析查询表达式
1037
        $sql = $this->builder->selectInsert($query, $fields, $table);
1038
1039
        return $this->execute($query, $sql, $query->getBind());
1040
    }
1041
1042
    /**
1043
     * 更新记录
1044
     * @access public
1045
     * @param  Query     $query  查询对象
1046
     * @return integer
1047
     * @throws Exception
1048
     * @throws PDOException
1049
     */
1050
    public function update(Query $query): int
1051
    {
1052
        $options = $query->parseOptions();
1053
1054
        if (isset($options['cache'])) {
1055
            $cacheItem = $this->parseCache($query, $options['cache']);
1056
            $key       = $cacheItem->getKey();
1057
        }
1058
1059
        // 生成UPDATE SQL语句
1060
        $sql = $this->builder->update($query);
1061
1062
        // 检测缓存
1063
        if (isset($key) && $this->cache->get($key)) {
1064
            // 删除缓存
1065
            $this->cache->delete($key);
1066
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1067
            $this->cache->clear($cacheItem->getTag());
0 ignored issues
show
Unused Code introduced by
The call to think\Cache::clear() has too many arguments starting with $cacheItem->getTag(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1067
            $this->cache->/** @scrutinizer ignore-call */ 
1068
                          clear($cacheItem->getTag());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1068
        }
1069
1070
        // 执行操作
1071
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
1072
1073
        if ($result) {
1074
            $query->trigger('after_update');
1075
        }
1076
1077
        return $result;
1078
    }
1079
1080
    /**
1081
     * 删除记录
1082
     * @access public
1083
     * @param  Query $query 查询对象
1084
     * @return int
1085
     * @throws Exception
1086
     * @throws PDOException
1087
     */
1088
    public function delete(Query $query): int
1089
    {
1090
        // 分析查询表达式
1091
        $options = $query->parseOptions();
1092
1093
        if (isset($options['cache'])) {
1094
            $cacheItem = $this->parseCache($query, $options['cache']);
1095
            $key       = $cacheItem->getKey();
1096
        }
1097
1098
        // 生成删除SQL语句
1099
        $sql = $this->builder->delete($query);
1100
1101
        // 检测缓存
1102
        if (isset($key) && $this->cache->get($key)) {
1103
            // 删除缓存
1104
            $this->cache->delete($key);
1105
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1106
            $this->cache->clear($cacheItem->getTag());
0 ignored issues
show
Unused Code introduced by
The call to think\Cache::clear() has too many arguments starting with $cacheItem->getTag(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1106
            $this->cache->/** @scrutinizer ignore-call */ 
1107
                          clear($cacheItem->getTag());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1107
        }
1108
1109
        // 执行操作
1110
        $result = $this->execute($query, $sql, $query->getBind());
1111
1112
        if ($result) {
1113
            $query->trigger('after_delete');
1114
        }
1115
1116
        return $result;
1117
    }
1118
1119
    /**
1120
     * 得到某个字段的值
1121
     * @access public
1122
     * @param  Query     $query 查询对象
1123
     * @param  string    $field   字段名
1124
     * @param  mixed     $default   默认值
1125
     * @return mixed
1126
     */
1127
    public function value(Query $query, string $field, $default = null)
1128
    {
1129
        $options = $query->parseOptions();
1130
1131
        if (isset($options['field'])) {
1132
            $query->removeOption('field');
1133
        }
1134
1135
        $query->setOption('field', (array) $field);
1136
1137
        if (!empty($options['cache'])) {
1138
            $cacheItem = $this->parseCache($query, $options['cache']);
1139
            $result    = $this->getCacheData($cacheItem);
1140
1141
            if (false !== $result) {
1142
                return $result;
1143
            }
1144
        }
1145
1146
        // 生成查询SQL
1147
        $sql = $this->builder->select($query, true);
1148
1149
        if (isset($options['field'])) {
1150
            $query->setOption('field', $options['field']);
1151
        } else {
1152
            $query->removeOption('field');
1153
        }
1154
1155
        // 执行查询操作
1156
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1157
1158
        $result = $pdo->fetchColumn();
1159
1160
        if (isset($cacheItem) && false !== $result) {
1161
            // 缓存数据
1162
            $cacheItem->set($result);
1163
            $this->cacheData($cacheItem);
1164
        }
1165
1166
        return false !== $result ? $result : $default;
1167
    }
1168
1169
    /**
1170
     * 得到某个字段的值
1171
     * @access public
1172
     * @param  Query     $query     查询对象
1173
     * @param  string    $aggregate 聚合方法
1174
     * @param  mixed     $field     字段名
1175
     * @param  bool      $force     强制转为数字类型
1176
     * @return mixed
1177
     */
1178
    public function aggregate(Query $query, string $aggregate, $field, bool $force = false)
1179
    {
1180
        if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
1181
            list($distinct, $field) = explode(' ', $field);
1182
        }
1183
1184
        $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
1185
1186
        $result = $this->value($query, $field, 0);
1187
1188
        return $force ? (float) $result : $result;
1189
    }
1190
1191
    /**
1192
     * 得到某个列的数组
1193
     * @access public
1194
     * @param  Query     $query 查询对象
1195
     * @param  string    $field 字段名 多个字段用逗号分隔
1196
     * @param  string    $key   索引
1197
     * @return array
1198
     */
1199
    public function column(Query $query, string $field, string $key = ''): array
1200
    {
1201
        $options = $query->parseOptions();
1202
1203
        if (isset($options['field'])) {
1204
            $query->removeOption('field');
1205
        }
1206
1207
        if ($key && '*' != $field) {
1208
            $field = $key . ',' . $field;
1209
        }
1210
1211
        $field = array_map('trim', explode(',', $field));
1212
1213
        $query->setOption('field', $field);
1214
1215
        if (!empty($options['cache'])) {
1216
            // 判断查询缓存
1217
            $cacheItem = $this->parseCache($query, $options['cache']);
1218
            $result    = $this->getCacheData($cacheItem);
1219
1220
            if (false !== $result) {
1221
                return $result;
1222
            }
1223
        }
1224
1225
        // 生成查询SQL
1226
        $sql = $this->builder->select($query);
1227
1228
        if (isset($options['field'])) {
1229
            $query->setOption('field', $options['field']);
1230
        } else {
1231
            $query->removeOption('field');
1232
        }
1233
1234
        // 执行查询操作
1235
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1236
1237
        if (1 == $pdo->columnCount()) {
1238
            $result = $pdo->fetchAll(PDO::FETCH_COLUMN);
1239
        } else {
1240
            $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
1241
1242
            if ('*' == $field && $key) {
1243
                $result = array_column($resultSet, null, $key);
1244
            } elseif (!empty($resultSet)) {
1245
                $fields = array_keys($resultSet[0]);
1246
                $count  = count($fields);
1247
                $key1   = array_shift($fields);
1248
                $key2   = $fields ? array_shift($fields) : '';
1249
                $key    = $key ?: $key1;
1250
1251
                if (strpos($key, '.')) {
1252
                    list($alias, $key) = explode('.', $key);
1253
                }
1254
1255
                if (2 == $count) {
1256
                    $column = $key2;
1257
                } elseif (1 == $count) {
1258
                    $column = $key1;
1259
                } else {
1260
                    $column = null;
1261
                }
1262
1263
                $result = array_column($resultSet, $column, $key);
1264
            } else {
1265
                $result = [];
1266
            }
1267
        }
1268
1269
        if (isset($cacheItem)) {
1270
            // 缓存数据
1271
            $cacheItem->set($result);
1272
            $this->cacheData($cacheItem);
1273
        }
1274
1275
        return $result;
1276
    }
1277
1278
    /**
1279
     * 根据参数绑定组装最终的SQL语句 便于调试
1280
     * @access public
1281
     * @param  string    $sql 带参数绑定的sql语句
1282
     * @param  array     $bind 参数绑定列表
1283
     * @return string
1284
     */
1285
    public function getRealSql(string $sql, array $bind = []): string
1286
    {
1287
        foreach ($bind as $key => $val) {
1288
            $value = is_array($val) ? $val[0] : $val;
1289
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
1290
1291
            if (self::PARAM_FLOAT == $type) {
1292
                $value = (float) $value;
1293
            } elseif (PDO::PARAM_STR == $type && is_string($value)) {
1294
                $value = '\'' . addslashes($value) . '\'';
1295
            } elseif (PDO::PARAM_INT == $type && '' === $value) {
1296
                $value = 0;
1297
            }
1298
1299
            // 判断占位符
1300
            $sql = is_numeric($key) ?
1301
            substr_replace($sql, $value, strpos($sql, '?'), 1) :
1302
            substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
1303
        }
1304
1305
        return rtrim($sql);
1306
    }
1307
1308
    /**
1309
     * 参数绑定
1310
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
1311
     * 或者 ['value',123] 对应问号占位符
1312
     * @access public
1313
     * @param  array $bind 要绑定的参数列表
1314
     * @return void
1315
     * @throws BindParamException
1316
     */
1317
    protected function bindValue(array $bind = []): void
1318
    {
1319
        foreach ($bind as $key => $val) {
1320
            // 占位符
1321
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1322
1323
            if (is_array($val)) {
1324
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
1325
                    $val[0] = 0;
1326
                } elseif (self::PARAM_FLOAT == $val[1]) {
1327
                    $val[0] = (float) $val[0];
1328
                    $val[1] = PDO::PARAM_STR;
1329
                }
1330
1331
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
1332
            } else {
1333
                $result = $this->PDOStatement->bindValue($param, $val);
1334
            }
1335
1336
            if (!$result) {
1337
                throw new BindParamException(
1338
                    "Error occurred  when binding parameters '{$param}'",
1339
                    $this->config,
1340
                    $this->getLastsql(),
1341
                    $bind
1342
                );
1343
            }
1344
        }
1345
    }
1346
1347
    /**
1348
     * 存储过程的输入输出参数绑定
1349
     * @access public
1350
     * @param  array $bind 要绑定的参数列表
1351
     * @return void
1352
     * @throws BindParamException
1353
     */
1354
    protected function bindParam(array $bind): void
1355
    {
1356
        foreach ($bind as $key => $val) {
1357
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1358
1359
            if (is_array($val)) {
1360
                array_unshift($val, $param);
1361
                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
1362
            } else {
1363
                $result = $this->PDOStatement->bindValue($param, $val);
1364
            }
1365
1366
            if (!$result) {
1367
                $param = array_shift($val);
1368
1369
                throw new BindParamException(
1370
                    "Error occurred  when binding parameters '{$param}'",
1371
                    $this->config,
1372
                    $this->getLastsql(),
1373
                    $bind
1374
                );
1375
            }
1376
        }
1377
    }
1378
1379
    /**
1380
     * 获得数据集数组
1381
     * @access protected
1382
     * @param  bool   $procedure 是否存储过程
1383
     * @return array
1384
     */
1385
    protected function getResult(bool $procedure = false): array
1386
    {
1387
        if ($procedure) {
1388
            // 存储过程返回结果
1389
            return $this->procedure();
1390
        }
1391
1392
        $result = $this->PDOStatement->fetchAll($this->fetchType);
1393
1394
        $this->numRows = count($result);
1395
1396
        return $result;
1397
    }
1398
1399
    /**
1400
     * 获得存储过程数据集
1401
     * @access protected
1402
     * @return array
1403
     */
1404
    protected function procedure(): array
1405
    {
1406
        $item = [];
1407
1408
        do {
1409
            $result = $this->getResult();
1410
            if (!empty($result)) {
1411
                $item[] = $result;
1412
            }
1413
        } while ($this->PDOStatement->nextRowset());
1414
1415
        $this->numRows = count($item);
1416
1417
        return $item;
1418
    }
1419
1420
    /**
1421
     * 执行数据库事务
1422
     * @access public
1423
     * @param  callable $callback 数据操作方法回调
1424
     * @return mixed
1425
     * @throws PDOException
1426
     * @throws \Exception
1427
     * @throws \Throwable
1428
     */
1429
    public function transaction(callable $callback)
1430
    {
1431
        $this->startTrans();
1432
1433
        try {
1434
            $result = null;
1435
            if (is_callable($callback)) {
1436
                $result = call_user_func_array($callback, [$this]);
1437
            }
1438
1439
            $this->commit();
1440
            return $result;
1441
        } catch (\Exception | \Throwable $e) {
1442
            $this->rollback();
1443
            throw $e;
1444
        }
1445
    }
1446
1447
    /**
1448
     * 启动事务
1449
     * @access public
1450
     * @return void
1451
     * @throws \PDOException
1452
     * @throws \Exception
1453
     */
1454
    public function startTrans(): void
1455
    {
1456
        $this->initConnect(true);
1457
1458
        ++$this->transTimes;
1459
1460
        try {
1461
            if (1 == $this->transTimes) {
1462
                $this->linkID->beginTransaction();
1463
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1464
                $this->linkID->exec(
1465
                    $this->parseSavepoint('trans' . $this->transTimes)
1466
                );
1467
            }
1468
        } catch (\Exception $e) {
1469
            if ($this->isBreak($e)) {
1470
                --$this->transTimes;
1471
                $this->close()->startTrans();
1472
            }
1473
            throw $e;
1474
        }
1475
    }
1476
1477
    /**
1478
     * 用于非自动提交状态下面的查询提交
1479
     * @access public
1480
     * @return void
1481
     * @throws PDOException
1482
     */
1483
    public function commit(): void
1484
    {
1485
        $this->initConnect(true);
1486
1487
        if (1 == $this->transTimes) {
1488
            $this->linkID->commit();
1489
        }
1490
1491
        --$this->transTimes;
1492
    }
1493
1494
    /**
1495
     * 事务回滚
1496
     * @access public
1497
     * @return void
1498
     * @throws PDOException
1499
     */
1500
    public function rollback(): void
1501
    {
1502
        $this->initConnect(true);
1503
1504
        if (1 == $this->transTimes) {
1505
            $this->linkID->rollBack();
1506
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1507
            $this->linkID->exec(
1508
                $this->parseSavepointRollBack('trans' . $this->transTimes)
1509
            );
1510
        }
1511
1512
        $this->transTimes = max(0, $this->transTimes - 1);
1513
    }
1514
1515
    /**
1516
     * 是否支持事务嵌套
1517
     * @return bool
1518
     */
1519
    protected function supportSavepoint(): bool
1520
    {
1521
        return false;
1522
    }
1523
1524
    /**
1525
     * 生成定义保存点的SQL
1526
     * @access protected
1527
     * @param  $name
1528
     * @return string
1529
     */
1530
    protected function parseSavepoint(string $name): string
1531
    {
1532
        return 'SAVEPOINT ' . $name;
1533
    }
1534
1535
    /**
1536
     * 生成回滚到保存点的SQL
1537
     * @access protected
1538
     * @param  $name
1539
     * @return string
1540
     */
1541
    protected function parseSavepointRollBack(string $name): string
1542
    {
1543
        return 'ROLLBACK TO SAVEPOINT ' . $name;
1544
    }
1545
1546
    /**
1547
     * 批处理执行SQL语句
1548
     * 批处理的指令都认为是execute操作
1549
     * @access public
1550
     * @param  Query    $query        查询对象
1551
     * @param  array    $sqlArray   SQL批处理指令
1552
     * @param  array    $bind       参数绑定
1553
     * @return bool
1554
     */
1555
    public function batchQuery(Query $query, array $sqlArray = [], array $bind = []): bool
1556
    {
1557
        // 自动启动事务支持
1558
        $this->startTrans();
1559
1560
        try {
1561
            foreach ($sqlArray as $sql) {
1562
                $this->execute($query, $sql, $bind);
1563
            }
1564
            // 提交事务
1565
            $this->commit();
1566
        } catch (\Exception $e) {
1567
            $this->rollback();
1568
            throw $e;
1569
        }
1570
1571
        return true;
1572
    }
1573
1574
    /**
1575
     * 获得查询次数
1576
     * @access public
1577
     * @return integer
1578
     */
1579
    public function getQueryTimes(): int
1580
    {
1581
        return self::$queryTimes;
1582
    }
1583
1584
    /**
1585
     * 关闭数据库(或者重新连接)
1586
     * @access public
1587
     * @return $this
1588
     */
1589
    public function close()
1590
    {
1591
        $this->linkID    = null;
1592
        $this->linkWrite = null;
1593
        $this->linkRead  = null;
1594
        $this->links     = [];
1595
1596
        $this->free();
1597
1598
        return $this;
1599
    }
1600
1601
    /**
1602
     * 是否断线
1603
     * @access protected
1604
     * @param  \PDOException|\Exception  $e 异常对象
1605
     * @return bool
1606
     */
1607
    protected function isBreak($e): bool
1608
    {
1609
        if (!$this->config['break_reconnect']) {
1610
            return false;
1611
        }
1612
1613
        $error = $e->getMessage();
1614
1615
        foreach ($this->breakMatchStr as $msg) {
1616
            if (false !== stripos($error, $msg)) {
1617
                return true;
1618
            }
1619
        }
1620
1621
        return false;
1622
    }
1623
1624
    /**
1625
     * 获取最近一次查询的sql语句
1626
     * @access public
1627
     * @return string
1628
     */
1629
    public function getLastSql(): string
1630
    {
1631
        return $this->getRealSql($this->queryStr, $this->bind);
1632
    }
1633
1634
    /**
1635
     * 获取最近插入的ID
1636
     * @access public
1637
     * @param  string  $sequence     自增序列名
1638
     * @return string
1639
     */
1640
    public function getLastInsID(string $sequence = null): string
1641
    {
1642
        return $this->linkID->lastInsertId($sequence);
1643
    }
1644
1645
    /**
1646
     * 获取返回或者影响的记录数
1647
     * @access public
1648
     * @return integer
1649
     */
1650
    public function getNumRows(): int
1651
    {
1652
        return $this->numRows;
1653
    }
1654
1655
    /**
1656
     * 获取最近的错误信息
1657
     * @access public
1658
     * @return string
1659
     */
1660
    public function getError(): string
1661
    {
1662
        if ($this->PDOStatement) {
1663
            $error = $this->PDOStatement->errorInfo();
1664
            $error = $error[1] . ':' . $error[2];
1665
        } else {
1666
            $error = '';
1667
        }
1668
1669
        if ('' != $this->queryStr) {
1670
            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
1671
        }
1672
1673
        return $error;
1674
    }
1675
1676
    /**
1677
     * 数据库调试 记录当前SQL及分析性能
1678
     * @access protected
1679
     * @param  boolean $start 调试开始标记 true 开始 false 结束
1680
     * @param  string  $sql 执行的SQL语句 留空自动获取
1681
     * @param  bool    $master 主从标记
1682
     * @return void
1683
     */
1684
    protected function debug(bool $start, string $sql = '', bool $master = false): void
1685
    {
1686
        if (!empty($this->config['debug'])) {
1687
            // 开启数据库调试模式
1688
            $debug = Container::pull('debug');
1689
1690
            if ($start) {
1691
                $debug->remark('queryStartTime', 'time');
1692
            } else {
1693
                // 记录操作结束时间
1694
                $debug->remark('queryEndTime', 'time');
1695
                $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime');
1696
                $sql     = $sql ?: $this->getLastsql();
1697
                $result  = [];
1698
1699
                // SQL性能分析
1700
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
1701
                    $result = $this->getExplain($sql);
1702
                }
1703
1704
                // SQL监听
1705
                $this->triggerSql($sql, $runtime, $result, $master);
1706
            }
1707
        }
1708
    }
1709
1710
    /**
1711
     * 监听SQL执行
1712
     * @access public
1713
     * @param  callable $callback 回调方法
1714
     * @return void
1715
     */
1716
    public function listen(callable $callback): void
1717
    {
1718
        self::$event[] = $callback;
1719
    }
1720
1721
    /**
1722
     * 触发SQL事件
1723
     * @access protected
1724
     * @param  string    $sql SQL语句
1725
     * @param  string    $runtime SQL运行时间
1726
     * @param  mixed     $explain SQL分析
1727
     * @param  bool      $master 主从标记
1728
     * @return void
1729
     */
1730
    protected function triggerSql(string $sql, string $runtime, array $explain = [], bool $master = false): void
1731
    {
1732
        if (!empty(self::$event)) {
1733
            foreach (self::$event as $callback) {
1734
                if (is_callable($callback)) {
1735
                    call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
1736
                }
1737
            }
1738
        } else {
1739
            if ($this->config['deploy']) {
1740
                // 分布式记录当前操作的主从
1741
                $master = $master ? 'master|' : 'slave|';
1742
            } else {
1743
                $master = '';
1744
            }
1745
1746
            // 未注册监听则记录到日志中
1747
            $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
1748
1749
            if (!empty($explain)) {
1750
                $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
1751
            }
1752
        }
1753
    }
1754
1755
    public function log($log, string $type = 'sql'): void
1756
    {
1757
        $this->config['debug'] && Container::pull('log')->record($log, $type);
1758
    }
1759
1760
    /**
1761
     * 初始化数据库连接
1762
     * @access protected
1763
     * @param  boolean $master 是否主服务器
1764
     * @return void
1765
     */
1766
    protected function initConnect(bool $master = true): void
1767
    {
1768
        if (!empty($this->config['deploy'])) {
1769
            // 采用分布式数据库
1770
            if ($master || $this->transTimes) {
1771
                if (!$this->linkWrite) {
1772
                    $this->linkWrite = $this->multiConnect(true);
1773
                }
1774
1775
                $this->linkID = $this->linkWrite;
1776
            } else {
1777
                if (!$this->linkRead) {
1778
                    $this->linkRead = $this->multiConnect(false);
1779
                }
1780
1781
                $this->linkID = $this->linkRead;
1782
            }
1783
        } elseif (!$this->linkID) {
1784
            // 默认单数据库
1785
            $this->linkID = $this->connect();
1786
        }
1787
    }
1788
1789
    /**
1790
     * 连接分布式服务器
1791
     * @access protected
1792
     * @param  boolean $master 主服务器
1793
     * @return PDO
1794
     */
1795
    protected function multiConnect(bool $master = false): PDO
1796
    {
1797
        $_config = [];
1798
1799
        // 分布式数据库配置解析
1800
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1801
            $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
1802
        }
1803
1804
        // 主服务器序号
1805
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
1806
1807
        if ($this->config['rw_separate']) {
1808
            // 主从式采用读写分离
1809
            if ($master) // 主服务器写入
1810
            {
1811
                $r = $m;
1812
            } elseif (is_numeric($this->config['slave_no'])) {
1813
                // 指定服务器读
1814
                $r = $this->config['slave_no'];
1815
            } else {
1816
                // 读操作连接从服务器 每次随机连接的数据库
1817
                $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
1818
            }
1819
        } else {
1820
            // 读写操作不区分服务器 每次随机连接的数据库
1821
            $r = floor(mt_rand(0, count($_config['hostname']) - 1));
1822
        }
1823
        $dbMaster = false;
1824
1825
        if ($m != $r) {
1826
            $dbMaster = [];
1827
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1828
                $dbMaster[$name] = $_config[$name][$m] ?? $_config[$name][0];
1829
            }
1830
        }
1831
1832
        $dbConfig = [];
1833
1834
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1835
            $dbConfig[$name] = $_config[$name][$r] ?? $_config[$name][0];
1836
        }
1837
1838
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
1839
    }
1840
1841
    /**
1842
     * 析构方法
1843
     * @access public
1844
     */
1845
    public function __destruct()
1846
    {
1847
        // 释放查询
1848
        $this->free();
1849
1850
        // 关闭连接
1851
        $this->close();
1852
    }
1853
1854
    /**
1855
     * 缓存数据
1856
     * @access protected
1857
     * @param  CacheItem    $cacheItem   缓存Item
1858
     */
1859
    protected function cacheData(CacheItem $cacheItem): void
1860
    {
1861
        if ($cacheItem->getTag()) {
1862
            $this->cache->tag($cacheItem->getTag());
0 ignored issues
show
Bug introduced by
The method tag() does not exist on think\Cache. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1862
            $this->cache->/** @scrutinizer ignore-call */ 
1863
                          tag($cacheItem->getTag());
Loading history...
1863
        }
1864
1865
        $this->cache->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
1866
1867
    }
1868
1869
    /**
1870
     * 获取缓存数据
1871
     * @access protected
1872
     * @param  Query     $query   查询对象
1873
     * @param  mixed     $cache   缓存设置
1874
     * @param  array     $data    缓存数据
1875
     * @param  string    $key     缓存Key
1876
     * @return mixed
1877
     */
1878
    protected function getCacheData(CacheItem $cacheItem)
1879
    {
1880
        // 判断查询缓存
1881
        return $this->cache->get($cacheItem->getKey());
1882
    }
1883
1884
    protected function parseCache(Query $query, array $cache): CacheItem
1885
    {
1886
        list($key, $expire, $tag) = $cache;
1887
1888
        if ($key instanceof CacheItem) {
1889
            $cacheItem = $key;
1890
        } else {
1891
            if (true === $key) {
1892
                if (!empty($query->getOptions('key'))) {
1893
                    $key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
1894
                } else {
1895
                    $key = md5($this->getConfig('database') . serialize($query->getOptions()) . serialize($query->getBind(false)));
1896
                }
1897
            }
1898
1899
            $cacheItem = new CacheItem($key);
1900
            $cacheItem->expire($expire);
1901
            $cacheItem->tag($tag);
1902
        }
1903
1904
        return $cacheItem;
1905
    }
1906
1907
    /**
1908
     * 延时更新检查 返回false表示需要延时
1909
     * 否则返回实际写入的数值
1910
     * @access public
1911
     * @param  string  $type     自增或者自减
1912
     * @param  string  $guid     写入标识
1913
     * @param  integer $step     写入步进值
1914
     * @param  integer $lazyTime 延时时间(s)
1915
     * @return false|integer
1916
     */
1917
    public function lazyWrite(string $type, string $guid, int $step, int $lazyTime)
1918
    {
1919
        if (!$this->cache->has($guid . '_time')) {
1920
            // 计时开始
1921
            $this->cache->set($guid . '_time', time(), 0);
1922
            $this->cache->$type($guid, $step);
1923
        } elseif (time() > $this->cache->get($guid . '_time') + $lazyTime) {
1924
            // 删除缓存
1925
            $value = $this->cache->$type($guid, $step);
1926
            $this->cache->delete($guid);
1927
            $this->cache->delete($guid . '_time');
1928
            return 0 === $value ? false : $value;
1929
        } else {
1930
            // 更新缓存
1931
            $this->cache->$type($guid, $step);
1932
        }
1933
1934
        return false;
1935
    }
1936
}
1937