本章的內(nèi)容主要講解了如何給數(shù)據(jù)庫的CURD查詢添加回調(diào)事件,以及如何在最底層的SQL層面進(jìn)行監(jiān)聽和做出性能分析及對查詢性能做出優(yōu)化建議,最后給出了一些安全方面的建議,學(xué)習(xí)內(nèi)容主要從性能分析和優(yōu)化,以及安全三個方面進(jìn)行講解:
- 性能分析
- 數(shù)據(jù)庫調(diào)試模式
- 獲取查詢次數(shù)
- 獲取SQL
- 開啟性能分析
- SQL監(jiān)聽
- 性能優(yōu)化
- SQL優(yōu)化
- 字段緩存
- 數(shù)據(jù)緩存
- 模型緩存
- 查詢事件
- 數(shù)據(jù)安全
- 底層防護(hù)
- 寫入過濾
- 安全建議
- 總結(jié)
性能分析
除了一些糟糕的業(yè)務(wù)邏輯,框架的性能瓶頸一般都是在數(shù)據(jù)庫(其它方面的性能沒什么好糾結(jié)的)。業(yè)務(wù)邏輯的優(yōu)化暫時不在本書的討論范疇,我們首先來學(xué)習(xí)如何進(jìn)行數(shù)據(jù)庫的性能分析。
數(shù)據(jù)庫調(diào)試模式
和應(yīng)用的調(diào)試模式不同,數(shù)據(jù)庫有自己獨立的調(diào)試模式開關(guān),在第一章我們已經(jīng)提過,數(shù)據(jù)庫配置參數(shù)中的debug參數(shù)就是數(shù)據(jù)庫調(diào)試模式的開關(guān)。
// 數(shù)據(jù)庫調(diào)試模式
'debug' => true,
數(shù)據(jù)庫調(diào)試模式開啟后,可以支持下列行為:
- 記錄SQL日志;
- 分析SQL性能;
- 支持SQL監(jiān)聽;
由于上述行為不可避免會產(chǎn)生額外的開銷,因此對性能存在一定的影響,但并不大,因為所有的日志是最終統(tǒng)一一次性寫入,而且可以設(shè)置為某個用戶才寫入日志。
在生產(chǎn)模式下面,必須關(guān)閉應(yīng)用調(diào)試模式(
app_debug),否則會暴露你的服務(wù)器敏感信息。和應(yīng)用調(diào)試模式不同,開啟數(shù)據(jù)庫調(diào)試模式并不會對外暴露任何安全信息,因此是否開啟數(shù)據(jù)庫調(diào)試模式,看自己的需求。
獲取查詢次數(shù)
使用Db::getQueryTimes()方法可以獲取當(dāng)前的數(shù)據(jù)庫查詢次數(shù),如果使用true作為參數(shù)的話可以獲取包括寫操作在內(nèi)的查詢次數(shù)。
// 獲取讀操作次數(shù)
$read = Db::getQueryTimes();
// 獲取所有的查詢次數(shù)
$count = Db::getQueryTimes(true);
如果開啟了頁面Trace顯示的話,可以直觀的看到當(dāng)前請求的查詢信息。

調(diào)用存儲過程會被認(rèn)為是執(zhí)行一次查詢操作而非寫操作,盡管存儲過程內(nèi)部可能會有寫入操作。
獲取SQL
可以用getLastsql方法獲取最后一次執(zhí)行的SQL語句,無論是使用Db類還是模型類,所以下面的方式都是有效的:
Db::name('user')->where('id', '>', 0)->select();
echo Db::getLastSql();
$user = User::get(1);
echo $user->getLastsql();
getLastSql方法即使關(guān)閉數(shù)據(jù)庫調(diào)試模式一樣有效
如果使用了文件類型記錄日志,并且開啟了數(shù)據(jù)庫調(diào)試模式的話,在日志文件中可以看到所有的SQL歷史記錄。
開啟性能分析
框架不但能記錄SQL日志,而且可以對查詢的SQL語句作出性能分析,幫助你快速找出數(shù)據(jù)庫性能瓶頸。
確保在數(shù)據(jù)庫配置文件中開啟下面兩個參數(shù):
// 開啟數(shù)據(jù)庫調(diào)試模式
'debug' => true,
// 開啟SQL性能分析
'sql_explain' => true,
開啟sql_explain參數(shù)后,會對查詢的SQL做EXPLAIN解析(由每個連接器類的getExplain方法完成查詢SQL分析),并把解析結(jié)果合并記錄到SQL日志中(注意:目前僅對Mysql數(shù)據(jù)庫有效)。
下面是一個查詢的分析日志例子:
[ SQL ] SELECT * FROM `user` WHERE `id` IN (2) [ RunTime:0.000703s ]
[ EXPLAIN : array ( 'id' => 1, 'select_type' => 'SIMPLE', 'table' => 'think_user', 'partitions' => NULL, 'type' => 'system', 'possible_keys' => 'PRIMARY', 'key' => NULL, 'key_len' => NULL, 'ref' => NULL, 'rows' => 1, 'filtered' => 100.0, 'extra' => NULL, ) ]
SQL日志中會記錄每個SQL的執(zhí)行時間以及EXPLAIN分析結(jié)果,框架只是記錄分析結(jié)果,至于如何查出問題和解決則需要你具備一定的SQL性能分析和優(yōu)化知識。
當(dāng)
EXPLAIN分析結(jié)果中的extra中使用了filesort或者temporary的話,系統(tǒng)會額外記錄一個警告錯誤告訴我們某條SQL存在性能問題需要處理。
SQL監(jiān)聽
如果覺得內(nèi)置的性能分析不夠全面,完全可以對執(zhí)行的SQL進(jìn)行監(jiān)聽并且對接第三方的SQL分析類庫。使用listen方法注冊SQL監(jiān)聽,例如可以在應(yīng)用公共文件或者某個行為擴(kuò)展中添加如下代碼:
Db::listen(function ($sql, $time, $explain) {
// 記錄SQL
Log::record($sql . ' [' . $time . 's]', 'sql');
// 查看性能分析結(jié)果
dump($explain);
});
如果關(guān)閉了
sql_explain參數(shù),explain參數(shù)就是一個空數(shù)組,你可以在監(jiān)聽方法中自行分析SQL性能問題。
監(jiān)聽的閉包方法支持傳入三個參數(shù),分別是:SQL語句、執(zhí)行時間(秒)和性能分析結(jié)果(數(shù)組),并注意如下事項:
- 如果注冊了多個SQL監(jiān)聽方法,則會依次調(diào)用;
- 一旦注冊了SQL監(jiān)聽,則SQL日志和分析日志自動無效,由監(jiān)聽方法接管;
性能優(yōu)化
現(xiàn)在我們已經(jīng)基本掌握了性能分析的手段,那么如何進(jìn)行性能優(yōu)化(本書中的優(yōu)化范疇主要是數(shù)據(jù)庫操作層面的)就是擺在開發(fā)人員面前的一件棘手大事,如果是一般的應(yīng)用可能主要做好數(shù)據(jù)表的索引就基本上沒什么大的性能問題,對于大流量及高并發(fā)的應(yīng)用,優(yōu)化的手段和空間就比較多,因為這個情況下任何一個細(xì)小的優(yōu)化都能帶來可觀的性能改進(jìn)。
SQL優(yōu)化
這里說的SQL優(yōu)化主要針對數(shù)據(jù)庫層面的優(yōu)化,對于Mysql數(shù)據(jù)庫來說,下面是一些比較常規(guī)的建議:
- 盡量少用SQL函數(shù)(會減少數(shù)據(jù)庫自身查詢緩存的命中率)而是用PHP變量傳入;
- 給常用的查詢字段建立索引或者聯(lián)合索引;
- 對JOIN的條件字段建立索引,并且采用相同的數(shù)據(jù)類型(包括字符集);
- 避免使用
ORDER BY RAND(); - 盡量調(diào)用
field方法顯式列出查詢的字段,即使用field(true); - 養(yǎng)成給數(shù)據(jù)表設(shè)置自增主鍵的習(xí)慣;
- 合理設(shè)計你的數(shù)據(jù)表字段類型;
- 對于大數(shù)據(jù)表使用垂直分表把數(shù)據(jù)表分為固定長度和不定長的兩個表;
更深層次的優(yōu)化可以對Mysql的配置參數(shù)進(jìn)行優(yōu)化配置(沒有一勞永逸的配置優(yōu)化,一定是針對應(yīng)用場景的),相信大部分應(yīng)用暫時還不需要到優(yōu)化配置的地步,首先考慮的還是架構(gòu)設(shè)計的優(yōu)化,數(shù)據(jù)庫配置的優(yōu)化策略對應(yīng)用的部署遷移會造成額外的成本以及不可預(yù)知的問題,如果你不是一個
DBA角色不建議頻繁調(diào)整配置參數(shù)。
字段緩存
說完了數(shù)據(jù)庫層面的優(yōu)化,我們后面著重來說下框架和應(yīng)用層面的優(yōu)化。
為了更安全的進(jìn)行數(shù)據(jù)庫操作,框架底層在查詢數(shù)據(jù)表數(shù)據(jù)的時候,會首先獲取該數(shù)據(jù)表的字段信息,包括字段名稱、字段類型以及主鍵名,對于不在字段列表中的字段則會進(jìn)行忽略處理甚至拋出異常,字段類型則用于進(jìn)行寫入和查詢的自動參數(shù)綁定,雖然說每個數(shù)據(jù)表只會獲取一次字段信息,但每次請求都要重新獲取一次不免覺得有點性能浪費。不過在開發(fā)階段,如果經(jīng)常會涉及到字段信息的變化,還是無所謂,但如果已經(jīng)部署上線了的話,還是建議使用字段緩存,也可以有效提高查詢性能,我們會在頁面Trace的SQL欄中看到類似的信息
[ SQL ] SHOW COLUMNS FROM `user` [ RunTime:0.001582s ]
其實就是查詢數(shù)據(jù)表user的字段信息的SQL語句(不同的數(shù)據(jù)庫查詢字段信息的SQL語句是不同的,由連接器類的getFields方法完成查詢)。
部署上線后,可以在命令行下執(zhí)行以下指令生成字段緩存,在命令行切換到應(yīng)用的根目錄(think文件所在目錄),輸入:
php think optimize:schema
會自動生成當(dāng)前數(shù)據(jù)庫配置文件中定義的數(shù)據(jù)表字段緩存,執(zhí)行后會自動在runtime/schema目錄下面按照數(shù)據(jù)表生成字段緩存文件,緩存文件的命名格式為:
數(shù)據(jù)庫名.數(shù)據(jù)表名.php
如果你的應(yīng)用有多個數(shù)據(jù)庫的操作,也可以指定數(shù)據(jù)庫生成字段緩存(必須有用戶權(quán)限),例如,下面用--db參數(shù)指定生成demo數(shù)據(jù)庫下面的所有數(shù)據(jù)表的字段緩存信息。
php think optimize:schema --db demo
如果你的應(yīng)用不同的模塊使用了不同的數(shù)據(jù)庫連接,還可以根據(jù)模塊來生成,用--module參數(shù)指定模塊如下:
php think optimize:schema --module index
會讀取index模塊的模型來生成數(shù)據(jù)表字段緩存,沒有繼承think\Model類的模型和抽象類不會生成。
每次執(zhí)行指令都會重新生成數(shù)據(jù)表字段緩存文件,如果只是更改了數(shù)據(jù)表的某個字段或者增加了新的字段,重新部署上線的時候,支持單獨更新某個數(shù)據(jù)表的緩存。
使用 --table參數(shù)指定需要更新的數(shù)據(jù)表:
php think optimize:schema --table user
支持指定數(shù)據(jù)庫名稱
php think optimize:schema --table demo.think_user
生成字段緩存后,你會發(fā)現(xiàn)數(shù)據(jù)庫的查詢性能提升明顯,尤其是在請求中操作大量數(shù)據(jù)表的情況下。
數(shù)據(jù)緩存
數(shù)據(jù)庫的優(yōu)化手段有時候比不過架構(gòu)和緩存的設(shè)計優(yōu)化,而架構(gòu)的優(yōu)化是一個綜合的范疇,需要針對具體的邏輯和場景,并且優(yōu)化的手段通常多元化,模型關(guān)聯(lián)的設(shè)計也是底層提供的架構(gòu)設(shè)計的優(yōu)化手段之一(使用預(yù)載入查詢可以有效減少數(shù)據(jù)庫查詢次數(shù)),現(xiàn)在我們要講的是如何利用數(shù)據(jù)緩存策略來減少數(shù)據(jù)庫的查詢開銷,這是一個不依賴數(shù)據(jù)庫的普適優(yōu)化策略。
數(shù)據(jù)庫的數(shù)據(jù)緩存并不是你理解的直接使用Cache類進(jìn)行操作,那樣太麻煩了,每次都要手動設(shè)置及額外讀取,也許像下面這樣:
$user = Cache::get('user_cache');
if (!$user) {
$user = Db::table('user')
->where('id', 10)
->find();
Cache::set('user_cache', $user);
}
查詢類封裝了一個數(shù)據(jù)緩存的鏈?zhǔn)椒椒?code>cache,可以很方便的進(jìn)行查詢數(shù)據(jù)的自動緩存和讀取,以及緩存數(shù)據(jù)的自動更新。數(shù)據(jù)庫的緩存策略主要就是掌握cache鏈?zhǔn)椒椒ǖ氖褂?,下面我們仔?xì)給你講解下用法。
先給出一個最簡單的用法:
Db::table('user')
->cache(600)
->where('id', 10)
->find();
Db::table('user')
->where('status', 1)
->cache(600)
->count();
可以對
find、select、value和column方法及其衍生方法使用數(shù)據(jù)緩存功能,不支持原生查詢query方法。
cache方法如果傳入數(shù)字,表示查詢數(shù)據(jù)的緩存時間(秒),所以上面的查詢在10分鐘以內(nèi)多次調(diào)用的話不會重復(fù)查詢數(shù)據(jù)庫,而是直接讀取緩存數(shù)據(jù)(使用當(dāng)前配置的緩存類型和緩存參數(shù))。
如果需要在外部調(diào)用緩存數(shù)據(jù)(盡管并不常見,但在跨模塊的時候可能會需要),可以指定緩存標(biāo)識,例如:
Db::table('user')
->cache('user_cache_key', 600)
->where('id', 10)
->find();
cache方法的第一個參數(shù)使用字符串表示緩存標(biāo)識,這個時候第二個參數(shù)就表示緩存有效期,然后可以在外部調(diào)用緩存的用戶數(shù)據(jù):
// 緩存數(shù)據(jù)有效期為10分鐘
$userData = Cache::get('user_cache_key');
內(nèi)置的數(shù)據(jù)緩存策略對原生查詢不起作用(只能單獨使用緩存方法來進(jìn)行緩存),相比緩存的優(yōu)勢用原生查詢的那點性能優(yōu)越感這個時候已經(jīng)蕩然無存了,查詢構(gòu)造器的優(yōu)勢就很明顯了。
數(shù)據(jù)緩存策略的關(guān)鍵是如何及時更新緩存數(shù)據(jù),我們來看下如何做到自動更新緩存,下面的內(nèi)容才是數(shù)據(jù)緩存要講的關(guān)鍵。
只需要在調(diào)用更新或者刪除方法之前調(diào)用cache方法(見證奇跡的時刻到了):
Db::table('user')
->cache('user_data')
->select([1, 3, 5]);
Db::table('user')
->cache('user_data')
->update(['id' => 1, 'name' => 'thinkphp']);
Db::table('user')
->cache('user_data')
->select([1, 3, 5]);
在更新數(shù)據(jù)的時候調(diào)用cache手動清除緩存,所以最后查詢的數(shù)據(jù)不會受第一條查詢緩存的影響,查詢出來的數(shù)據(jù)依然是同步更新后的數(shù)據(jù)。
同樣,如果進(jìn)行了刪除操作,也會自動清除緩存數(shù)據(jù)。
Db::table('user')
->cache('user_data')
->select([1, 3, 5]);
Db::table('user')
->cache('user_data')
->delete(1);
Db::table('user')
->cache('user_data')
->select([1, 3, 5]);
確保查詢和更新或者刪除使用相同的緩存標(biāo)識才能自動清除緩存。
比較常用的數(shù)據(jù)緩存是以主鍵為查詢條件的單個數(shù)據(jù)的緩存,所以如果使用find方法并且使用主鍵查詢的情況,緩存更新更智能。update或者delete方法可以不需要調(diào)用cache方法,也會自動清理緩存,例如:
Db::table('user')
->cache(true)
->find(1);
Db::table('user')
->update(['id' => 1, 'name' => 'topthink']);
Db::table('user')
->cache(true)
->find(1);
根據(jù)主鍵查詢的話,緩存更新是自動的,因此上面的例子最后查詢的數(shù)據(jù)會是更新后的數(shù)據(jù)。
使用where方法查詢主鍵條件的話,效果一樣:
Db::table('user')
->cache(true)
->where('id', 1)
->find();
Db::table('user')
->where('id', 1)
->update(['name' => 'topthink']);
Db::table('user')
->cache(true)
->where('id', 1)
->find();
模型緩存
除了使用Db類,模型類還提供了更方便的方法進(jìn)行數(shù)據(jù)緩存。如果是緩存讀取單個數(shù)據(jù),可以使用:
// 查詢數(shù)據(jù)并緩存讀取
$user = User::get(1, [], true);
// 設(shè)置緩存有效期
$user = User::get(1, [], 600);
由于第二個參數(shù)是預(yù)載入查詢,所以查詢緩存屈居二線了_,不過如果你的版本在5.0.6以上的話,可以直接寫成:
// 查詢數(shù)據(jù)并緩存讀取
$user = User::get(1, true);
// 設(shè)置緩存有效期
$user = User::get(1, 600);
當(dāng)使用主鍵查詢、更新和刪除模型數(shù)據(jù)的時候,會自動更新模型數(shù)據(jù)緩存。如果你的查詢條件不是主鍵,可以指定緩存標(biāo)識,并在刪除的時候帶上緩存標(biāo)識,例如:
// 查詢name為thinkphp的用戶數(shù)據(jù)并緩存讀取
$user = User::cache('user_key_thinkphp')
->getByName('thinkphp');
// 刪除數(shù)據(jù)并更新緩存數(shù)據(jù)
$user->cache('user_key_thinkphp')
->delete();
模型數(shù)據(jù)緩存標(biāo)識不能直接在外部讀取,因為緩存的數(shù)據(jù)都是數(shù)組而不是對象,所以下面才是正確的姿勢。
// 查詢name為thinkphp的用戶數(shù)據(jù)并緩存讀取
$user = User::cache('user_key_thinkphp')
->getByName('thinkphp');
// 外部讀取模型數(shù)據(jù)緩存
$data = new User(Cache::get('user_key_thinkphp'));
同樣的用法,如果要緩存讀取多個數(shù)據(jù),使用下面的方式:
// 查詢多個數(shù)據(jù)并緩存讀取
$users = User::all([1, 2, 3], [], true);
// 設(shè)置緩存有效期
$users = User::all([1, 2, 3], [], 3600);
5.0.6版本以上同樣可以使用
// 查詢多個數(shù)據(jù)并緩存讀取
$users = User::all([1, 2, 3], true);
// 設(shè)置緩存有效期
$users = User::all([1, 2, 3], 3600);
模型的數(shù)據(jù)緩存配合關(guān)聯(lián)預(yù)載入查詢的話效果更佳,關(guān)于如何使用關(guān)聯(lián)預(yù)載入查詢請參考上一章的內(nèi)容。
查詢事件
使用查詢事件可以在不改變原有數(shù)據(jù)查詢代碼的前提下制定獨立的緩存策略,先來了解下什么是查詢事件。
查詢事件是針對數(shù)據(jù)庫的CURD操作而設(shè)計的回調(diào)方法,主要包括:
| 事件 | 描述 |
|---|---|
| before_select |
select查詢前回調(diào) |
| before_find |
find查詢前回調(diào) |
| after_insert |
insert操作成功后回調(diào) |
| after_update |
update操作成功后回調(diào) |
| after_delete |
delete操作成功后回調(diào) |
使用下面的方式注冊一個查詢事件
Db::event('before_select', function ($options, $query) {
// 事件處理
});
如果before_select或者before_find回調(diào)方法有返回數(shù)據(jù),則表示提前返回查詢結(jié)果,不會繼續(xù)執(zhí)行查詢操作。
Db::event('before_find', function ($options, $query) {
// 事件處理
if ('user' == $options['table']) {
$result = ['id' => 1, 'name' => 'thinkphp'];
return $result;
}
});
$user = Db::table('user')->find();
user變量最終的結(jié)果是['id'=>1,'name'=>'thinkphp']。
下面的例子我們沒有使用cache方法進(jìn)行數(shù)據(jù)緩存,而是利用查詢事件來定制自己的數(shù)據(jù)緩存策略。
// after_insert回調(diào)方法
Db::event('after_insert', function ($options, $query) {
$pk = $query->getPk($options);
$guid = $options['table'] . '_' . $options['data'][$pk];
Cache::set($guid, $options['data'], 0);
});
// after_update回調(diào)方法
Db::event('after_update', function ($options, $query) {
$pk = $query->getPk($options);
$guid = $options['table'] . '_' . $options['data'][$pk];
$data = Cache::get($guid);
$data = array_merge($data, $options['data']);
Cache::set($guid, $data, 0);
});
// after_delete回調(diào)方法
Db::event('after_delete', function ($options, $query) {
$pk = $query->getPk($options);
$guid = $options['table'] . '_' . $options['data'][$pk];
Cache::set($guid, null, 0);
});
// before_find回調(diào)方法
Db::event('before_find', function ($options, $query) {
$pk = $query->getPk($options);
$guid = $options['table'] . '_' . $options['data'][$pk];
$data = Cache::get($guid);
if ($data) {
return $data;
}
});
注冊完查詢回調(diào)方法后,下面的查詢除了寫操作會執(zhí)行數(shù)據(jù)庫操作,其它的查詢方法都直接讀取緩存數(shù)據(jù),而且始終保持最新的數(shù)據(jù)。
$id = Db::table('user')
->insert(['name'=>'thinkphp']);
Db::table('user')->find($id);
Db::table('user')
->where('id',$id)
->update(['name'=>'topthink']);
Db::table('user')->find($id);
Db::table('user')
->delete($id);
Db::table('user')->find($id);
數(shù)據(jù)安全
安全和優(yōu)化就如同魚和熊掌一般,很難兼得。從某種程度上說,數(shù)據(jù)安全比性能優(yōu)化更重要,因此為了更加安全和穩(wěn)健運行,犧牲一定的性能都是值得的,下面我們來學(xué)習(xí)下基本的安全策略。
底層防護(hù)
5.0版本提供了更高的底層安全策略,雖然不至于因此而高枕無憂,但也完全不必杞人憂天,主要體現(xiàn)在:
- WEB訪問目錄和應(yīng)用目錄隔離;
- 內(nèi)置使用PDO預(yù)處理和自動參數(shù)綁定機(jī)制;
- 默認(rèn)用戶提交數(shù)據(jù)不支持?jǐn)?shù)組;
- 支持?jǐn)?shù)據(jù)自動過濾機(jī)制;
只要善于運用系統(tǒng)提供的安全手段和做好一些配置,可確保你的應(yīng)用安全無虞,聽我給你細(xì)細(xì)道來。
寫入過濾
由于系統(tǒng)的安全機(jī)制,任何非數(shù)據(jù)表的字段如果要寫入數(shù)據(jù)庫都會導(dǎo)致異常,如果你不希望非數(shù)據(jù)表字段寫入數(shù)據(jù)庫的時候拋出異常,而只是忽略就行,那么可以使用下面兩種方式。
如果是僅僅當(dāng)前操作忽略,則可以使用strict方法,例如:
Db::table('user')
->strict(false)
->insert([
'name' => 'thinkphp',
'nickname' => '流年',
'test' => '測試數(shù)據(jù)',
]);
由于user表中并不存在test字段,因此test數(shù)據(jù)會被直接忽略,但由于使用了strict(false)方法,而不會拋出異常。
如果希望全局不拋出異常,可以在數(shù)據(jù)庫配置文件中設(shè)置
// 是否嚴(yán)格檢查字段是否存在
'fields_strict' => false,
但有些時候我們還需要限制寫入數(shù)據(jù)庫的字段,避免被用戶提交更新一些敏感數(shù)據(jù),并非只有查詢的時候可以使用field方法指定字段列表,我們還可以在寫入數(shù)據(jù)的時候使用field方法限制字段寫入。
Db::table('user')
->field('name,nickname')
->where('id', 1)
->update([
'name' => 'thinkphp',
'nickname' => '流年',
'email' => 'thinkphp@qq.com',
]);
上面的例子中,由于我們用field方法限制了寫入的字段列表,因此email數(shù)據(jù)不會被更新,而是直接忽略。
同樣,field方法也支持排除某些字段
Db::table('user')
->field('email,score', true)
->where('id', 1)
->update([
'name' => 'thinkphp',
'nickname' => '流年',
'email' => 'thinkphp@qq.com',
]);
如果使用模型操作的話,我們還可以使用allowField方法提前對數(shù)據(jù)進(jìn)行字段過濾
$user = User::get(1);
$user->name = 'thinkphp';
$user->nickname = '流年';
$user->email = 'thinkphp@qq.com';
$user->allowField('name,nickname')
->save();
allowField過濾數(shù)據(jù)并不會導(dǎo)致異常,和field方法不同,allowField方法并不支持字段排除,如果調(diào)用allowField(true)表示過濾數(shù)據(jù)表字段之外的數(shù)據(jù)
模型還額外提供了一個只讀字段的功能,針對某些字段只提供寫入功能而不提供更新功能,具體可以參考模型高級用法一章的內(nèi)容。
安全建議
為了讓你的應(yīng)用更安全,綜合之前提到的各種安全因素,在數(shù)據(jù)庫的層面我們給出如下安全建議:
- 對用戶輸入的數(shù)據(jù)做盡可能的驗證;
- 對寫入的數(shù)據(jù)做好過濾,避免異常;
- 避免直接使用用戶提交數(shù)據(jù)作為查詢條件;
- 查詢字段名不應(yīng)該由表單或者用戶決定;
- 對于
get和find方法的參數(shù)建議做好Null判斷; - 數(shù)據(jù)輸出的時候注意做好
XSS安全過濾; - 對于模型數(shù)據(jù)盡量隱藏敏感數(shù)據(jù)后輸出;
- 對于業(yè)務(wù)數(shù)據(jù)的寫入操作應(yīng)當(dāng)做好權(quán)限檢查;
- 寫入數(shù)據(jù)嚴(yán)格使用
field方法限制寫入字段;
舉個例子,如果你開放查詢字段名給用戶提交而未作判斷直接作為查詢條件,例如下面的代碼:
$where = request()->param();
// 查詢用戶是否存在
$user = Db::table('user')
->where($where)
->find();
假設(shè)你的表單里面有一個name字段,那么,用戶就可以在瀏覽器構(gòu)造一個name|email字段完成OR查詢,查詢的結(jié)果可能完全不同了,極有可能造成邏輯漏洞。
正確的查詢方式應(yīng)該是:
// 查詢用戶是否存在
$user = Db::table('user')
->where('name',request()->param('name'))
->find();
總結(jié)
到目前為止,我們已經(jīng)完成了5.0的數(shù)據(jù)庫和模型的學(xué)習(xí),最好的老師是實踐并把掌握的知識點融會貫通,在后面的附錄中我們會給大家匯總整理一些常見問題,并保持不斷更新。
感謝你堅持看完了本書的內(nèi)容,您的建議是我們努力完善的動力,希望不吝賜教并隨時在本書的評論區(qū)或者
github上給我們留言,最后祝你在新的開發(fā)征程中所向披靡,因為我們的愿景就是讓開發(fā)變得更簡單!
上一篇:第八章:模型關(guān)聯(lián)
下一篇:附錄A:常見問題