## thinkphp5.0數據庫與模型的研究
**last update: 2022-11-03 15:08:22**
----
[TOC=3,8]
----
### getLastInsID 問題
同一個連接插入后如果不馬上執行 `$this->getQuery()->getLastInsID($sequence)`,哪怕接下來執行一條 select 也會影響 `getLastInsID` 的值(變成0了)。
>[tip] **特別是持久連接時要注意**,所以多個持久連接時要使用 `\PDO::ATTR_PERSISTENT = 'string'` 字符串的方式區分不同的持久連接,不然有問題,參見: `app\smartpark\model\v1\DbActLog` 和 `think\log\driver\Socket`
(fpm 下 由于 日志保存是最后執行的,所以沒出現過這個問題,不過如果強制寫入日志時就會有問題了)
>[tip] **插入時如果指定了 id 主鍵的值,那么 getLastInsID 就獲取不到插入的id,即 getLastInsID 的意思是獲取 mysql 最后自動生成的id,而不是應用自主寫入的。**
~~~
mysql> INSERT INTO `pt_parking_log` (`id` , `camera_id`) VALUES (1 , 1 );
Query OK, 1 row affected
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
| 0 |
+------------------+
1 row in set
mysql> INSERT INTO `pt_parking_log` (`camera_id`) VALUES (1);
Query OK, 1 row affected
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
| 2 |
+------------------+
1 row in set
mysql> INSERT INTO `pt_parking_log` (`id` , `camera_id`) VALUES (0 , 1 );
Query OK, 1 row affected
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
| 3 |
+------------------+
1 row in set
~~~
----
### PDO 連接復用 insertId 錯亂 問題
本文記錄解決 PDO 底層共用連接的導致 getLastInsID 錯誤的問題
[PHP: Connections and Connection management - Manual](https://www.php.net/manual/zh/pdo.connections.php
)
>[tip] PDO::ATTR_PERSISTENT選項的值轉換為bool(啟用/禁用持久連接),**除非它是非數字字符串,在這種情況下,它允許使用多個持久連接池。** 如果不同的連接使用不兼容的設置,這很有用
注意:即使 tp 層連接類 不是單例了,但是底層pdo 還是會復用,所以要設置 \PDO::ATTR_PERSISTENT 名稱(字符串)來區分。
```php
<?php
namespace app\smartpark\model\v1;
class DbActLog extends BModel
{
protected $table = 'sp_db_act_log';
public function __construct($data = [])
{
// 避免和主 db 共用連接
$connection = config('database');
$connection['debug'] = false;
$connection['query'] = \think\db\Query::class;
// 不能使用同一個 主持久連接,否則 PDO 底層 會復用連接
// https://www.php.net/manual/zh/pdo.connections.php
$connection['params'][\PDO::ATTR_PERSISTENT] = 'db_act_log';
$this->connection = $connection;
parent::__construct($data);
}
}
```
----
### 源碼分析
~~~php
think\Validate
protected $rule = []
protected $message = []
protected $field = []
protected $scene = []
$this->batch()->
// 定義場景或選擇使用場景
$this->scene($name, $fields = null)->
hasScene($name)
check($data)
getError()
think\Model
// 設置驗證規則
public $this->validate()->
// 自動驗證數據 注意這是受保護的,不能外部主動調用
// 注意,驗證通過的話,模型的驗證規則會被重置位空:$this->validate = null
protected validateData() 只在 save/saveAll 中使用了
public save() // insert/update 操作
public saveAll()
public getError()
// Db::connect() 創建單例的 think\\db\\connector\Mysql [連接器對象]
// think\\db\\connector->connect() 單例的 new PDO 保存在 [連接器對象] $this->links[$linkNum] 中
// \\think\\db\\Query 查詢器對象 保存在 think\\Model::$links(即一個模型類一個查詢器單例對象) 中(new 查詢器對象時 傳入了連接實例 和當前模型對象),連接實例 是單例的 保存在 think\\Db::$instance
// 使用 db('name') 時,先單例 [連接器對象] connector (think\\Db::$instance),然后在 [連接器對象] 上執行 name()方法,實際是在 一個臨時的、新的查詢器上 (new 查詢器對象時 傳入了當前連接實例) 上執行,即 每次 db() 調用都會 new 新的 查詢器對象
,但 [連接器對象] 是單例的。
// 注意 new \\think\\db\\Query 時,如果沒有傳入 連接器對象,則會 Db::connect([], true) 強制創建新 [連接器對象]
public getQuery() // 獲取當前模型的查詢對象
where 等鏈式操作都在查詢器對象上
// 注意 new \\think\\db\\Query 時,如果沒有傳入 連接器對象,則會 Db::connect([], true); 強制創建新 [連接器對象]
查詢器上的操作 最終都是通過調用 其屬性 [連接器對象] 完成的:$this->connection->execute
()
系統獲取 [連接器對象] 都是通過 think\\Db::connect() 連接配置生成key 保持單例 (think\\Db::$instance),數據庫 PDO 連接 是惰性的,只有當 [連接器對象] 上有查詢時才會真正連接,PDO連接 在 [連接器對象] 的 $this->links[$linkNum] 屬性上,所以通常 系統中只有一個 [連接器對象] ,也只有 一個實際的數據庫連接。
pdo sql 最終執行都是在連接器上,連接器上本身也有一些直接使用的sql執行方法 query/execute,如果調用的方法不存在 如 name()方法時,就會創建查詢器 ($this->getQuery())在查詢器上調用這個方法,注意這個創建的查詢器對象是一次性的,沒有在連接器對象上保存起來,所以 think\\Db::name('a') , think\\Db::name('a') 會返回兩個獨立的查詢器對象
。
PDO 對象 在連接器對象上,連接器對象基本是單例的,不管在不同的模型上或是直接使用,所以 結果集的處理時要注意,即使在不同的模型上,因為共用連接,游標等,所以結果集處理時要注意,要防止獲取到連接上上一次的查詢結果集。
模型 每次自動驗證后會清空 $this->validate 所以下次調用時,需要再次設置驗證規則 ->validate(true),傳入 true 則使用定義的 與當前模型名稱同名的驗證器類,tp 這里待優化,只記錄了當前模型名稱,沒有記錄完整類名,如果定位到與當前模塊同名且同模塊的驗證器類。這就導致 如果在 index 模塊 使用 admin 模塊的模型,驗證時使用的還是 index 模塊的 驗證器類,除非 傳入 $this->validate('admin/User') 才行
~~~
----
### 總結
#### 連接器對象: PDO
think\\db\\connector\Mysql extends think\\db\\connector
$this->linkID 屬性:PDO 數據庫連接對象
單例在 `think\\Db::$instance` 上 (key 數據庫配置MD5)
think\\Db::connect() 獲取單例連接器對象(可傳入強制連接,此時會重新實例化連接器對象)
think\\Db 調用不存在靜態方法時,其實是調用 其連接器的方法
連接器上方法不存在時 就調用查詢器上的方法(查詢器對象是臨時 new 的沒有單例保存,創建查詢器對象時傳入了自身 $this)
`$this->links[$linkNum]` = new PDO();
連接器上每個連接序號的pdo對象是單例的,所以 **一個數據庫連接在一個連接器對象上只會有一次,連接序號參數是因為主從多庫的情況,此時一個連接器對象上可能有多個數據庫的連接,但同樣,每個數據庫在一個連接器對象只有一次連接**
----
#### 查詢器對象: Active Record: Data
\\think\\db\\Query
$this->connection 屬性:連接器對象
$this->model 屬性:模型對象
查詢器上的sql執行方法,都是調用 連接器對象上的方法:
`query() => $this->connection->query() => $connection->linkID->execute()`
查詢器主要是實現sql 解析等鏈式方法,如 where inc 等。最終生成sql 給連接器執行。
在返回結果前,如果 有 $model 屬性,則會 返回 $model 對象,也就是 ActiveRecord 化處理。
----
#### 模型: Active Record: Object Item
self::$links[get_called_class()] 靜態屬性:查詢器對象 (注意是 self 即 think\Model)
static::$initialized[get_class($this)] 靜態屬性:查詢器對象 (注意是 static 即 實際調用的 模型類)
self::$event[get_called_class()] 靜態屬性:每個模型上有各自的事件
```php
public function __call($method, $args)
{
$query = $this->db(true, false);
...
}
public static function __callStatic($method, $args)
{
$model = new static();
$query = $model->db();
...
}
protected function initialize()
{
$class = get_class($this);
if (!isset(static::$initialized[$class])) {
static::$initialized[$class] = true;
static::init();
}
}
protected static function init()
{
}
```
當調用不存在的靜態方法時,如 User::find()、Model::with() ,會 自動 new 當前模型類,并調用其 $this->db(true, true) 方法獲取查詢器對象(強制獲取新 查詢器對象),最終調用的是 查詢器上的 find() 方法,并返回。
當調用 User::get() 時,static::parseQuery() -> self::with() : $query->find() (注意這里 static/self 的差別) 獲取查詢器,最終調用查詢器的find() 方法 返回結果。(新模型對象,新查詢器對象)
查詢器對象保存在 `self::$links[$this->class]` 模型類上,**注意是類而不是對象實例。**
所以:如果**調用不存在的靜態方法** 每次會創建新的查詢器對象 $this->db(true, true),如果 **調用不存在的非靜態方法**,則會有單例效果 $this->db(true, false) 即 **一個模型對象上 都是同一個查詢器對象**。
buildQuery() new 創建查詢器對象時,傳入了 Db::connect($connection) 作為連接器對象參數,和 $this 作為模型對象參數。
~~~php
User::get()
$query = static::parseQuery()
self::with()
->find()
這里 self::with() 而不是 static::with() 暫時沒研究清楚有什么區別意義。
~~~
----
#### 分析
1. 平時直接 Db::name('')->find() 執行sql, 多次操作會創建多次查詢器對象,如果使用 new 模型就能避免(注意不是模型方法靜態調用)
2. 不論直接使用連接器對象,還是使用模型,不論多次調用或實例化,數據庫連接對象只有一個,因為單例在 think\\\\Db::$instance 上了。
3. 使用模型必須要定義自己的模型類,沒有驗證器那那樣的 “獨立驗證”
----
### 補充知識:延遲靜態綁定(late static binding)
注意 static:: 和 self:: 的區別:
類的靜態屬性全局只有一個,這二者都是將數據保存在類的靜態屬性上,不同的是 static:: 是實際執行的具體類,self:: 是當前類(字面代碼所在的類)。 $this-> 是訪問實際具體對象實例上的屬性。
----
get_called_class() (獲取靜態方法調用的類名) 和 get_class() 的區別:
知識點:延遲靜態綁定(late static binding),靜態方法和屬性在定義是就綁定了,static:: 關鍵字 可以訪問延遲綁定的靜態屬性和方法。
所以請仔細理解 think\\Model 中 :
self:: 當前類,即 think\\Model
static:: 后期靜態綁定類,如 User::get() 這種 用戶定義的模型類
$this->class = get_called_class()
用來在每個實際具體模型上實現查詢器對象的單例化
self::$links[$this->class] = 查詢器對象(think\\Model::$links[A/B/C/...])
----
initialize() 模型初始化(實例化)
每個模型類 實例化為對象時的 構造方法
每次 new User 實例化 都會調用初始化方法,如 每次 User::get(1) 都會 new 新的實際模型對象
----
static init() 模型初始化
static::$initialized[get_class($this)] = true
> 同樣也可以使用靜態`init`方法,需要注意的是`init`只在第一次實例化的時候執行(多次 new 也只會執行一次),并且方法內需要注意靜態調用的規范。
待研究 這里明明應該 使用 static::$initialized 標記就行了啊?
----
self::$event 每個模型上的事件
self::$event[get_called_class()][$event][] = $callback;
self::$event[stataic] 相當于事件是注冊到 think\Model[模型定義類(A/B/C/...)] 上。
----
[PHP: 后期靜態綁定 - Manual](https://www.php.net/manual/zh/language.oop5.late-static-bindings.php)
```php
<?php
// static,self 分析
class A {
static $a = 'a';
public function setA()
{
self::$a = 'b';
}
public function setA2()
{
static::$a = 'b';
}
public function getA()
{
return self::$a;
}
public function getA2()
{
return static::$a;
}
}
class B extends A {
// 如果沒有子類繼承重寫, static 設置時會有 self 效果
static $a = 'a_';
}
class C extends A {
static $a = 'a__';
}
$A = new A;
$B = new B;
$C = new C;
----
echo C::$a;
// a__
echo $B->getA(); // a
echo $B->getA2(); // a_
$B->setA();
echo $B->getA(); // b
echo $B->getA2(); // a_
----
$B->setA(); // A/B/C 都是一樣的
echo $A->getA(); // b
echo $A->getA2(); // b
echo $B->getA(); // b
echo $B->getA2(); // a_
echo $C->getA(); // b
echo $C->getA2(); // a__
echo A::$a; // b
echo B::$a; // a_
echo C::$a; // a__
----
$B->setA2();
echo $A->getA(); // a
echo $A->getA2(); // a
echo $B->getA(); // a
echo $B->getA2(); // b
echo $C->getA(); // a
echo $C->getA2(); // a__
echo A::$a; // a
echo B::$a; // b
echo C::$a; // a__
```
----
### 相關資料
[數據庫 · ThinkPHP5.0完全開發手冊 · 看云](http://www.hmoore.net/manual/thinkphp5/118058)
[模型 · ThinkPHP5.0完全開發手冊 · 看云](http://www.hmoore.net/manual/thinkphp5/135186)
[掌握ThinkPHP5數據庫和模型 · 看云](http://www.hmoore.net/thinkphp/master-database-and-model)
[ThinkORM開發指南 · 看云](http://www.hmoore.net/manual/think-orm)
[ORM介紹 - 知乎](https://zhuanlan.zhihu.com/p/151373067)
[ORM 實例教程 - 阮一峰的網絡日志](http://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html)
[淺談MyBatis-Plus學習之ActiveRecord - hjjay - 博客園](https://www.cnblogs.com/jayhou/p/9824232.html)
> Active Record(簡稱AR),是一種領域模型模式,特點是一個模型類對應關系型數據庫中的一個表,而模型類的一個實例對應表中的一行記錄。
----
last update: 2022-04-05 18:12:22
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 科目一
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南