關(guān)于標(biāo)題產(chǎn)生的兩個原因:一定來源于工作真實案列
- 第一種情況是有一個 mobile 新增入庫成功,編輯時獲取到的mobile 為空斋陪,編輯時數(shù)據(jù)修改了,吧之前的數(shù)據(jù)給覆蓋了舔箭,這種問題已經(jīng)相當(dāng)嚴重了 菱阵?:rage: 【這種是 appends 影響】
- 第二種當(dāng)我們編輯一條數(shù)據(jù)踢俄,發(fā)現(xiàn)傳值了,save() 之后卻發(fā)現(xiàn) 字段還是初始值 未更新晴及, 這種一般會發(fā)生在
json
都办、array
、object
這兩種數(shù)據(jù)類型上 【這種是casts 影響】
以下均是測試案例虑稼,模擬工作中使用場景
下面將從上面兩種情況介紹一下這個位置 我們要如何正確使用
$casts
和$appends
|setAppends()
琳钉,使得我們能夠正確的拿到使用的姿勢。數(shù)據(jù)庫字段(測試表[
wecaht_users
])
CREATE TABLE `wechat_users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`nickname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mobile` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
- 本次測試所用的模型 [
WechatUser
]
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class WechatUser extends Model
{
use CommonTrait;
//
protected $fillable = [
'nickname',
'mobile',
'avatar'
];
public function getTestAttribute($value)
{
return $value;
}
}
- 模型引用的
Trait
塊
<?php
namespace App\Model;
trait CommonTrait
{
public function getMobileAttribute($value)
{
return $value;
}
public function setMobileAttribute($value)
{
$this->attributes['mobile'] = $value;
}
}
產(chǎn)生問題的姿勢:(錯誤姿勢蛛倦,禁止這樣子使用)
一歌懒、setAppends
觸發(fā)的系統(tǒng)bug和注意事項
- 工作中的用法(模擬):我們在 公用
CommonTrait
內(nèi)重寫了mobile 字段的getter
和setter
方法, 實際工作中不一定是這個字段,這個是舉例使用溯壶,為什么會這么做及皂,因為項目中這個trait 是只要你引入,只需要在主表 加上對應(yīng)的字段且改, 字段內(nèi)的邏輯用的是 trait 控制的验烧,因為工作中沒有注意到 trait 中的操作,在用戶編輯數(shù)據(jù)時 有一段代碼如下:
$user = WechatUser::query()->find(1);
$user->setAppends([
'mobile',
'test'
]);
如上操作導(dǎo)致詳情獲取到 mobile
字段為空又跛,用戶編輯打開什么也沒操作碍拆,直接點擊表單提交入庫, 這個時候數(shù)據(jù)庫發(fā)現(xiàn) mobile 空了慨蓝,產(chǎn)生這么大的問題倔监,開發(fā)能不慌嗎,就趕緊查看這個問題菌仁,那么你說為什么會有人 做這個操作浩习, 其實也是沒完全理解 setAppend()
這個函數(shù)做了什么操作
- 我的排查問題思路:
- 因為實際數(shù)據(jù)庫在查詢位置我調(diào)試還有數(shù)據(jù)
- 一開始我以為是字段額外操作了,看了下查詢的邏輯并沒有對字段做處理济丘,但是在最后看到一個操作
$user->setAppends([
'mobile',
'test'
]);
- 我就直接定位這個地方的數(shù)據(jù)處理了谱秽,導(dǎo)致后續(xù)的問題
- 下面是為什么執(zhí)行了
setAppend
之后空了
/**
* 將模型的屬性轉(zhuǎn)成數(shù)組結(jié)構(gòu)(我們在查詢到結(jié)果時,這個地方都會執(zhí)行一步操作)
*
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) array
*/
public function attributesToArray()
{
// 處理需要轉(zhuǎn)換成時間格式的屬性
$attributes = $this->addDateAttributesToArray(
$attributes = $this->getArrayableAttributes()
);
// 這一步就是將變異屬性轉(zhuǎn)成數(shù)組
$attributes = $this->addMutatedAttributesToArray(
$attributes, $mutatedAttributes = $this->getMutatedAttributes()
);
// 將模型的屬性和變異屬性(重寫了get和set 操作)進行參數(shù)類型處理
$attributes = $this->addCastAttributesToArray(
$attributes, $mutatedAttributes
);
// 關(guān)鍵的一步摹迷,也正是我們出問題的地方疟赊,獲取到所有的appends 追加的字段 這個地方包含 模型默認設(shè)置的 $appends 屬性的擴充字段,這個位置是 key 是字段 可以看到value 都是 null , 因為 我們所用的 mobile 是系統(tǒng)字段峡碉, 所以這一步銷毀了我們的value ,導(dǎo)致了我們的后續(xù)問題近哟,那么這個地方應(yīng)該怎么用, 咱們?nèi)シ治鲆幌逻@個地方的調(diào)用
foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}
return $attributes;
}
下面具體分析一下 append 字段該怎么去用鲫寄,以及下面這段實行了什么
foreach ($this->getArrayableAppends() as $key) { $attributes[$key] = $this->mutateAttributeForArray($key, null); } ````
$this->mutateAttributeForArray($key, null)
這個其實將我們append 字段的修改器返回的內(nèi)容給轉(zhuǎn)成array 形式
/**
* 使用其突變體進行陣列轉(zhuǎn)換吉执,獲取屬性值疯淫。
*
* @param string $key
* @param mixed $value
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
*/
protected function mutateAttributeForArray($key, $value)
{
$value = $this->mutateAttribute($key, $value);
return $value instanceof Arrayable ? $value->toArray() : $value;
}
// 這個是獲取我們自定義的變異屬性 默認是我們模型定義了這個 `getMobileAttribute($value)` 的修改器
protected function mutateAttribute($key, $value)
{
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
- 相比到這里都明白了,為什么這個位置 mobile 會返回空了吧
- laravel 其實這個位置是讓我們在模型上追加以外的字段的戳玫,所以給我們默認傳的 null 這個熙掺,所以我們不能修改模型已有的屬性,這樣子會打亂我們的正常數(shù)據(jù)咕宿,也不能這么使用币绩,騷操作雖然好用,但是要慎用府阀,使用不好就是坑
- 模型屬性定義的
$append
原理一樣缆镣,我們一定不要再 appends 里面寫數(shù)據(jù)庫字段,一定不要寫试浙,這個是給別人找麻煩
- 模型屬性定義的
二董瞻、$casts
類型轉(zhuǎn)換引起的bug,常見問題出在 json
等字段類型映射上
- 這個問題引起也是因為 我們的
$casts
屬性轉(zhuǎn)換的類型和我們重寫的修改器之后返回的類型不一致導(dǎo)致的,如下我們模型內(nèi)定義為custom
入庫或者輸出時候 轉(zhuǎn)換成json
類型:
protected $casts = [
'custom' => 'json'
];
這樣子寫本身也沒問題川队,只要數(shù)據(jù)是數(shù)組格式,自動轉(zhuǎn)成json 格式入庫睬澡,這個要個前端約定好固额,否則可能出現(xiàn)想不到的數(shù)據(jù)異常,假設(shè)我們現(xiàn)在沒有在模型重寫 custom
的 getCustomAttribute
和setCustomAttribute
這兩個修改器方法煞聪, 這個位置在laravel 中默認處理的方式如下:
有數(shù)據(jù)入庫時會觸發(fā)模型的 save 方法 【laravel 源碼如下】:
/**
* Save the model to the database.
*
* @param array $options
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
*/
public function save(array $options = [])
{
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
// false, indicating that the save failed. This provides a chance for any
// listeners to cancel save operations if validations fail or whatever.
if ($this->fireModelEvent('saving') === false) {
return false;
}
// If the model already exists in the database we can just update our record
// that is already in this database using the current IDs in this "where"
// clause to only update this model. Otherwise, we'll just insert them.
if ($this->exists) {
$saved = $this->isDirty() ?
$this->performUpdate($query) : true;
}
// If the model is brand new, we'll insert it into our database and set the
// ID attribute on the model to the value of the newly inserted row's ID
// which is typically an auto-increment value managed by the database.
else {
$saved = $this->performInsert($query);
if (! $this->getConnectionName() &&
$connection = $query->getConnection()) {
$this->setConnection($connection->getName());
}
}
// If the model is successfully saved, we need to do a few more things once
// that is done. We will call the "saved" method here to run any actions
// we need to happen after a model gets successfully saved right here.
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
我們這里只看 更新操作 有個核心函數(shù): $this->isDirty()
檢測是否有需要更新的字段斗躏,這個函數(shù)又處理了什么操作呢:
/**
* Determine if the model or any of the given attribute(s) have been modified.
*
* @param array|string|null $attributes
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
*/
public function isDirty($attributes = null)
{
return $this->hasChanges(
$this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
);
}
hasChanges
這個主要是判斷一下是否有變更,我們主要看 $this->getDirty()
這個里面的操作昔脯,為什么我們會深入到這里去查這個問題啄糙,因為數(shù)據(jù)庫記錄能否更新和這個息息相關(guān), getDirty()
方法內(nèi)又是怎么操作呢
/**
* Get the attributes that have been changed since last sync.
*
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) array
*/
public function getDirty()
{
$dirty = [];
foreach ($this->getAttributes() as $key => $value) {
if (! $this->originalIsEquivalent($key, $value)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
// 接下來的處理是調(diào)用 $this->originalIsEquivalent($key, $value)
/**
* Determine if the new and old values for a given key are equivalent.
*
* @param string $key
* @param mixed $current
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
*/
public function originalIsEquivalent($key, $current)
{
if (! array_key_exists($key, $this->original)) {
return false;
}
$original = $this->getOriginal($key);
if ($current === $original) {
return true;
} elseif (is_null($current)) {
return false;
} elseif ($this->isDateAttribute($key)) {
return $this->fromDateTime($current) ===
$this->fromDateTime($original);
} elseif ($this->hasCast($key, ['object', 'collection'])) {
return $this->castAttribute($key, $current) ==
$this->castAttribute($key, $original);
} elseif ($this->hasCast($key, ['real', 'float', 'double'])) {
if (($current === null && $original !== null) || ($current !== null && $original === null)) {
return false;
}
return abs($this->castAttribute($key, $current) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4;
} elseif ($this->hasCast($key)) {
return $this->castAttribute($key, $current) ===
$this->castAttribute($key, $original);
}
return is_numeric($current) && is_numeric($original)
&& strcmp((string) $current, (string) $original) === 0;
}
這個時候我們要排查我們 模型內(nèi)定義的 casts 轉(zhuǎn)換的字段默認會執(zhí)行如下代碼:
elseif ($this->hasCast($key)) {
return $this->castAttribute($key, $current) ===
$this->castAttribute($key, $original);
}
這個地方有個類型處理器 【castAttribute】:
/**
* Cast an attribute to a native PHP type.
*
* @param string $key
* @param mixed $value
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
*/
protected function castAttribute($key, $value)
{
if (is_null($value)) {
return $value;
}
switch ($this->getCastType($key)) {
case 'int':
case 'integer':
return (int) $value;
case 'real':
case 'float':
case 'double':
return $this->fromFloat($value);
case 'decimal':
return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'object':
return $this->fromJson($value, true);
case 'array':
case 'json':
return $this->fromJson($value);
case 'collection':
return new BaseCollection($this->fromJson($value));
case 'date':
return $this->asDate($value);
case 'datetime':
case 'custom_datetime':
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
default:
return $value;
}
}
到這個位置我們大概就知道我們所定義的 casts 類型到底在什么時候幫我們執(zhí)行數(shù)據(jù)轉(zhuǎn)換了云稚, 入庫的前一步操作隧饼,而我們往往不注意開發(fā)的時候,問題也就出在這個地方
出問題原因:
- 我們定義了
custom => json
類型 静陈,本身我們要求前端傳過來的是一個數(shù)組ID燕雁,后端轉(zhuǎn)成 逗號拼接入庫,這個時候由于開發(fā)沒有前后端統(tǒng)一鲸拥,出現(xiàn)了更新不上的問題 拐格,但是這個時候因為我們這個模型繼承的父類模型 又是有個修改器,如getCustomAttribute
返回是一個字符串刑赶, 但是 我們最終在$this->fromJson($value);
時候因為value 的非法捏浊,導(dǎo)致json_encode 失敗,返回了 false
/**
* Decode the given JSON back into an array or object.
*
* @param string $value
* @param bool $asObject
* [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
*/
public function fromJson($value, $asObject = false)
{
return json_decode($value, ! $asObject);
}
而模型內(nèi)的 getCustomAttribute
里面代碼是如下格式:
public function setCustomAttribute($value)
{
if ($value) {
$value = implode(',', $value);
}
$this->attributes['custom'] = $value;
}
這個是否修改器內(nèi)的值已經(jīng)不是數(shù)組了撞叨, 是一個字符串金踪,這個是否 執(zhí)行 fromJson
就會返回 false
下面這個條件就會一直返回 true , 默認相等了 浊洞,然后上面! $this->originalIsEquivalent($key, $value)
的就會認為 這個字段 新值和舊數(shù)據(jù) 相等,不需要更新
$this->castAttribute($key, $current) ===
$this->castAttribute($key, $original)
因為 save 這個位置是只更新變更的數(shù)據(jù)字段热康,沒有變更的默認舍棄沛申,所以就出現(xiàn)我們項目中遇到的一個問題,一直不被更新姐军,排查到這個問題铁材,就趕緊更新了代碼
- 這個位置的注意事項咱們要記一下 【最好是根據(jù)自己的需要寫】
- 如果前端提交的參數(shù) 正好是我們想要的,我們直接定義
$casts
字段類型奕锌,就不用后續(xù)處理轉(zhuǎn)換了著觉。這個時候正常寫custom => json
就行 【推薦】
- 如果前端提交的參數(shù) 正好是我們想要的,我們直接定義
- 如果針對前端傳過來的參數(shù)不滿意,需要特殊處理成我們想要的惊暴, 也就是我們現(xiàn)在所做的操作 重寫了
setCustomAttribute
修改器饼丘, 在這個位置直接處理成我們要入庫的數(shù)據(jù)類型和類型就行 【推薦】
- 如果針對前端傳過來的參數(shù)不滿意,需要特殊處理成我們想要的惊暴, 也就是我們現(xiàn)在所做的操作 重寫了
- 模型已經(jīng)定義了
$casts
針對custom => json
類型的轉(zhuǎn)換 ,這個時候又在模型 重新定義了setCustomAttribute
修改器辽话,也是當(dāng)前我們項目中這么做出現(xiàn)bug 的一個原因肄鸽,不是不能這么寫,而是 這個修改器的值類型必須和我們定義的casts
需要轉(zhuǎn)換的類型保持一致油啤,json
一定要求是對象或者數(shù)組才能序列化典徘,string
不能執(zhí)行這個操作,出現(xiàn)前后不一致的類型益咬,導(dǎo)致數(shù)據(jù)寫入失敗逮诲,這種方式我們需要盡量避免,要么直接用casts
類型轉(zhuǎn)換幽告, 要么直接定義 修改器修改格式梅鹦, 兩者確實需要用了 一定要保持格式正確
- 模型已經(jīng)定義了
正確姿勢:
-
如何正確掌握
$appends
和setAppends($appends)
的使用姿勢- 如何正確使用
- 非模型字段
- 一定要在模型內(nèi)實現(xiàn)變異屬性修改器 如:
getTestAttribute($value)
, 這樣子我們就能在模型里面動態(tài)追加了 - 模型的
$appends
會在全局追加該屬性,只要有查詢模型的地方冗锁,返回之后都會帶上 -
setAppends
只會在調(diào)用的地方返回追加字段齐唆,其他地方觸發(fā)不會主動返回該字段
- 如何正確使用
-
如何正確掌握
$casts
的使用姿勢- 如何正確使用
- 非模型字段, 這個處理只是展示數(shù)據(jù)有影響,不影響我們?nèi)霂鞌?shù)據(jù)
- 如果合理冻河,盡量不要重寫修改器蝶念, 前端傳入的參數(shù)直接就是我們所要的數(shù)據(jù),限制嚴格一點沒有壞處芋绸, 這個時候我們 直接使用系統(tǒng)的類型轉(zhuǎn)換 媒殉,節(jié)約開發(fā)時間
- 第三種是我們?nèi)绻惺褂?修改器調(diào)整數(shù)據(jù)格式,那么
$casts
位置就請刪除掉字段類型轉(zhuǎn)換摔敛,因為多人合作廷蓉,避免不掉類型會對不上,針對這種,建議自己寫修改器桃犬,不要添加字段對應(yīng)的轉(zhuǎn)換器刹悴,也是比較推薦的一種
- 如何正確使用
如果哪位在開發(fā)中也有類似的騷操作, 歡迎評論學(xué)習(xí)攒暇。
文中如果錯誤地方土匀,還望各位大佬指正!:stuck_out_tongue: