摘要:為關(guān)聯(lián)關(guān)系設(shè)置約束子模型的等于父模型的上面設(shè)置的字段的值子類實(shí)現(xiàn)這個(gè)抽象方法通過上面代碼看到創(chuàng)建實(shí)例時(shí)主要是做了一些配置相關(guān)的操作,設(shè)置了子模型父模型兩個(gè)模型的關(guān)聯(lián)字段和關(guān)聯(lián)的約束。不過當(dāng)查詢父模型時(shí),可以預(yù)加載關(guān)聯(lián)數(shù)據(jù)。
Database 模型關(guān)聯(lián)
上篇文章我們主要講了Eloquent Model關(guān)于基礎(chǔ)的CRUD方法的實(shí)現(xiàn),Eloquent Model中除了基礎(chǔ)的CRUD外還有一個(gè)很重要的部分叫模型關(guān)聯(lián),它通過面向?qū)ο蟮姆绞絻?yōu)雅地把數(shù)據(jù)表之間的關(guān)聯(lián)關(guān)系抽象到了Eloquent Model中讓應(yīng)用依然能用Fluent Api的方式訪問和設(shè)置主體數(shù)據(jù)的關(guān)聯(lián)數(shù)據(jù)。使用模型關(guān)聯(lián)給應(yīng)用開發(fā)帶來的收益我認(rèn)為有以下幾點(diǎn)
主體數(shù)據(jù)和關(guān)聯(lián)數(shù)據(jù)之間的關(guān)系在代碼表現(xiàn)上更明顯易懂讓人一眼就能明白數(shù)據(jù)間的關(guān)系。
模型關(guān)聯(lián)在底層幫我們解決好了數(shù)據(jù)關(guān)聯(lián)和匹配,應(yīng)用程序中不需要再去寫join語句和子查詢,應(yīng)用代碼的可讀性和易維護(hù)性更高。
使用模型關(guān)聯(lián)預(yù)加載后,在效率上高于開發(fā)者自己寫join和子查詢,模型關(guān)聯(lián)底層是通過分別查詢主體和關(guān)聯(lián)數(shù)據(jù)再將它們關(guān)聯(lián)匹配到一起。
按照Laravel設(shè)定好的模式來寫關(guān)聯(lián)模型每個(gè)人都能寫出高效和優(yōu)雅的代碼 (這點(diǎn)我認(rèn)為適用于所有的Laravel特性)。
說了這么多下面我們就通過實(shí)際示例出發(fā)深入到底層看看模型關(guān)聯(lián)是如何解決數(shù)據(jù)關(guān)聯(lián)匹配和加載關(guān)聯(lián)數(shù)據(jù)的。
在開發(fā)中我們經(jīng)常遇到的關(guān)聯(lián)大致有三種:一對(duì)一,一對(duì)多和多對(duì)多,其中一對(duì)一是一種特殊的一對(duì)多關(guān)聯(lián)。我們通過官方文檔里的例子來看一下Laravel是怎么定義這兩種關(guān)聯(lián)的。
一對(duì)多class Post extends Model { /** * 獲得此博客文章的評(píng)論。 */ public function comments() { return $this->hasMany("AppComment"); } } /** * 定義一個(gè)一對(duì)多關(guān)聯(lián)關(guān)系,返回值是一個(gè)HasMany實(shí)例 * * @param string $related * @param string $foreignKey * @param string $localKey * @return IlluminateDatabaseEloquentRelationsHasMany */ public function hasMany($related, $foreignKey = null, $localKey = null) { //創(chuàng)建一個(gè)關(guān)聯(lián)表模型的實(shí)例 $instance = $this->newRelatedInstance($related); //關(guān)聯(lián)表的外鍵名 $foreignKey = $foreignKey ?: $this->getForeignKey(); //主體表的主鍵名 $localKey = $localKey ?: $this->getKeyName(); return new HasMany( $instance->newQuery(), $this, $instance->getTable().".".$foreignKey, $localKey ); } /** * 創(chuàng)建一個(gè)關(guān)聯(lián)表模型的實(shí)例 */ protected function newRelatedInstance($class) { return tap(new $class, function ($instance) { if (! $instance->getConnectionName()) { $instance->setConnection($this->connection); } }); }
在定義一對(duì)多關(guān)聯(lián)時(shí)返回了一個(gè)IlluminateDatabaseEloquentRelationsHasMany 類的實(shí)例,Eloquent封裝了一組類來處理各種關(guān)聯(lián),其中HasMany是繼承自HasOneOrMany抽象類, 這也正印證了上面說的一對(duì)一是一種特殊的一對(duì)多關(guān)聯(lián),Eloquent定義的所有這些關(guān)聯(lián)類又都是繼承自Relation這個(gè)抽象類, Relation里定義里一些模型關(guān)聯(lián)基礎(chǔ)的方法和一些必須讓子類實(shí)現(xiàn)的抽象方法,各種關(guān)聯(lián)根據(jù)自己的需求來實(shí)現(xiàn)這些抽象方法。
為了閱讀方便我們把這幾個(gè)有繼承關(guān)系類的構(gòu)造方法放在一起,看看定義一對(duì)多關(guān)返回的HasMany實(shí)例時(shí)都做了什么。
class HasMany extends HasOneOrMany { ...... } abstract class HasOneOrMany extends Relation { ...... public function __construct(Builder $query, Model $parent, $foreignKey, $localKey) { $this->localKey = $localKey; $this->foreignKey = $foreignKey; parent::__construct($query, $parent); } //為關(guān)聯(lián)關(guān)系設(shè)置約束 子模型的foreign key等于父模型的 上面設(shè)置的$localKey字段的值 public function addConstraints() { if (static::$constraints) { $this->query->where($this->foreignKey, "=", $this->getParentKey()); $this->query->whereNotNull($this->foreignKey); } } public function getParentKey() { return $this->parent->getAttribute($this->localKey); } ...... } abstract class Relation { public function __construct(Builder $query, Model $parent) { $this->query = $query; $this->parent = $parent; $this->related = $query->getModel(); //子類實(shí)現(xiàn)這個(gè)抽象方法 $this->addConstraints(); } }
通過上面代碼看到創(chuàng)建HasMany實(shí)例時(shí)主要是做了一些配置相關(guān)的操作,設(shè)置了子模型、父模型、兩個(gè)模型的關(guān)聯(lián)字段、和關(guān)聯(lián)的約束。
Eloquent里把主體數(shù)據(jù)的Model稱為父模型,關(guān)聯(lián)數(shù)據(jù)的Model稱為子模型,為了方便下面所以下文我們用它們來指代主體和關(guān)聯(lián)模型。
定義完父模型到子模型的關(guān)聯(lián)后我們還需要定義子模型到父模型的反向關(guān)聯(lián)才算完整, 還是之前的例子我們?cè)谧幽P屠锿ㄟ^belongsTo方法定義子模型到父模型的反向關(guān)聯(lián)。
class Comment extends Model { /** * 獲得此評(píng)論所屬的文章。 */ public function post() { return $this->belongsTo("AppPost"); } public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null) { //如果沒有指定$relation參數(shù),這里通過debug backtrace方法獲取調(diào)用者的方法名稱,在我們的例子里是post if (is_null($relation)) { $relation = $this->guessBelongsToRelation(); } $instance = $this->newRelatedInstance($related); //如果沒有指定子模型的外鍵名稱則使用調(diào)用者的方法名加主鍵名的snake命名方式來作為默認(rèn)的外鍵名(post_id) if (is_null($foreignKey)) { $foreignKey = Str::snake($relation)."_".$instance->getKeyName(); } // 設(shè)置父模型的主鍵字段 $ownerKey = $ownerKey ?: $instance->getKeyName(); return new BelongsTo( $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation ); } protected function guessBelongsToRelation() { list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); return $caller["function"]; } } class BelongsTo extends Relation { public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relation) { $this->ownerKey = $ownerKey; $this->relation = $relation; $this->foreignKey = $foreignKey; $this->child = $child; parent::__construct($query, $child); } public function addConstraints() { if (static::$constraints) { $table = $this->related->getTable(); //設(shè)置約束 父模型的主鍵值等于子模型的外鍵值 $this->query->where($table.".".$this->ownerKey, "=", $this->child->{$this->foreignKey}); } } }
定義一對(duì)多的反向關(guān)聯(lián)時(shí)也是一樣設(shè)置了父模型、子模型、兩個(gè)模型的關(guān)聯(lián)字段和約束,此外還設(shè)置了關(guān)聯(lián)名稱,在Model的belongsTo方法里如果未提供后面的參數(shù)會(huì)通過debug_backtrace 獲取調(diào)用者的方法名作為關(guān)聯(lián)名稱進(jìn)而猜測(cè)出子模型的外鍵名稱的,按照約定Eloquent 默認(rèn)使用父級(jí)模型名的「snake case」形式、加上 _id 后綴名作為外鍵字段。
多對(duì)多多對(duì)多關(guān)聯(lián)不同于一對(duì)一和一對(duì)多關(guān)聯(lián)它需要一張中間表來記錄兩端數(shù)據(jù)的關(guān)聯(lián)關(guān)系,官方文檔里以用戶角色為例子闡述了多對(duì)多關(guān)聯(lián)的使用方法,我們也以這個(gè)例子來看一下底層是怎么來定義多對(duì)多關(guān)聯(lián)的。
class User extends Model { /** * 獲得此用戶的角色。 */ public function roles() { return $this->belongsToMany("AppRole"); } } class Role extends Model { /** * 獲得此角色下的用戶。 */ public function users() { return $this->belongsToMany("AppUser"); } } /** * 定義一個(gè)多對(duì)多關(guān)聯(lián), 返回一個(gè)BelongsToMany關(guān)聯(lián)關(guān)系實(shí)例 * * @return IlluminateDatabaseEloquentRelationsBelongsToMany */ public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null) { //沒有提供$relation參數(shù) 則通過debug_backtrace獲取調(diào)用者方法名作為relation name if (is_null($relation)) { $relation = $this->guessBelongsToManyRelation(); } $instance = $this->newRelatedInstance($related); $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); //如果沒有提供中間表的名稱,則會(huì)按照字母順序合并兩個(gè)關(guān)聯(lián)模型的名稱作為中間表名 if (is_null($table)) { $table = $this->joiningTable($related); } return new BelongsToMany( $instance->newQuery(), $this, $table, $foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(), $relatedKey ?: $instance->getKeyName(), $relation ); } /** * 獲取多對(duì)多關(guān)聯(lián)中默認(rèn)的中間表名 */ public function joiningTable($related) { $models = [ Str::snake(class_basename($related)), Str::snake(class_basename($this)), ]; sort($models); return strtolower(implode("_", $models)); } class BelongsToMany extends Relation { public function __construct(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null) { $this->table = $table;//中間表名 $this->parentKey = $parentKey;//父模型User的主鍵 $this->relatedKey = $relatedKey;//關(guān)聯(lián)模型Role的主鍵 $this->relationName = $relationName;//關(guān)聯(lián)名稱 $this->relatedPivotKey = $relatedPivotKey;//關(guān)聯(lián)模型Role的主鍵在中間表中的外鍵role_id $this->foreignPivotKey = $foreignPivotKey;//父模型Role的主鍵在中間表中的外鍵user_id parent::__construct($query, $parent); } public function addConstraints() { $this->performJoin(); if (static::$constraints) { $this->addWhereConstraints(); } } protected function performJoin($query = null) { $query = $query ?: $this->query; $baseTable = $this->related->getTable(); $key = $baseTable.".".$this->relatedKey; //$query->join("role_user", "role.id", "=", "role_user.role_id") $query->join($this->table, $key, "=", $this->getQualifiedRelatedPivotKeyName()); return $this; } /** * Set the where clause for the relation query. * * @return $this */ protected function addWhereConstraints() { //$this->query->where("role_user.user_id", "=", 1) $this->query->where( $this->getQualifiedForeignPivotKeyName(), "=", $this->parent->{$this->parentKey} ); return $this; } }
定義多對(duì)多關(guān)聯(lián)后返回一個(gè)IlluminateDatabaseEloquentRelationsBelongsToMany類的實(shí)例,與定義一對(duì)多關(guān)聯(lián)時(shí)一樣,實(shí)例化BelongsToMany時(shí)定義里與關(guān)聯(lián)相關(guān)的配置:中間表名、關(guān)聯(lián)的模型、父模型在中間表中的外鍵名、關(guān)聯(lián)模型在中間表中的外鍵名、父模型的主鍵、關(guān)聯(lián)模型的主鍵、關(guān)聯(lián)關(guān)系名稱。與此同時(shí)給關(guān)聯(lián)關(guān)系設(shè)置了join和where約束,以User類里的多對(duì)多關(guān)聯(lián)舉例,performJoin方法為其添加的join約束如下:
$query->join("role_user", "roles.id", "=", "role_user.role_id")
然后addWhereConstraints為其添加的where約束為:
//假設(shè)User對(duì)象的id是1 $query->where("role_user.user_id", "=", 1)
這兩個(gè)的約束就是對(duì)應(yīng)的SQL語句就是
SELECT * FROM roles INNER JOIN role_users ON roles.id = role_user.role_id WHERE role_user.user_id = 1遠(yuǎn)層一對(duì)多
Laravel還提供了遠(yuǎn)層一對(duì)多關(guān)聯(lián),提供了方便、簡(jiǎn)短的方式通過中間的關(guān)聯(lián)來獲得遠(yuǎn)層的關(guān)聯(lián)。還是以官方文檔的例子說起,一個(gè) Country 模型可以通過中間的 User 模型獲得多個(gè) Post 模型。在這個(gè)例子中,您可以輕易地收集給定國家的所有博客文章。讓我們來看看定義這種關(guān)聯(lián)所需的數(shù)據(jù)表:
countries id - integer name - string users id - integer country_id - integer name - string posts id - integer user_id - integer title - string
class Country extends Model { public function posts() { return $this->hasManyThrough( "AppPost", "AppUser", "country_id", // 用戶表外鍵... "user_id", // 文章表外鍵... "id", // 國家表本地鍵... "id" // 用戶表本地鍵... ); } } /** * 定義一個(gè)遠(yuǎn)層一對(duì)多關(guān)聯(lián),返回HasManyThrough實(shí)例 * @return IlluminateDatabaseEloquentRelationsHasManyThrough */ public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) { $through = new $through; $firstKey = $firstKey ?: $this->getForeignKey(); $secondKey = $secondKey ?: $through->getForeignKey(); $localKey = $localKey ?: $this->getKeyName(); $secondLocalKey = $secondLocalKey ?: $through->getKeyName(); $instance = $this->newRelatedInstance($related); return new HasManyThrough($instance->newQuery(), $this, $through, $firstKey, $secondKey, $localKey, $secondLocalKey); } class HasManyThrough extends Relation { public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) { $this->localKey = $localKey;//國家表本地鍵id $this->firstKey = $firstKey;//用戶表中的外鍵country_id $this->secondKey = $secondKey;//文章表中的外鍵user_id $this->farParent = $farParent;//Country Model $this->throughParent = $throughParent;//中間 User Model $this->secondLocalKey = $secondLocalKey;//用戶表本地鍵id parent::__construct($query, $throughParent); } public function addConstraints() { //country的id值 $localValue = $this->farParent[$this->localKey]; $this->performJoin(); if (static::$constraints) { //$this->query->where("users.country_id", "=", 1) 假設(shè)country_id是1 $this->query->where($this->getQualifiedFirstKeyName(), "=", $localValue); } } protected function performJoin(Builder $query = null) { $query = $query ?: $this->query; $farKey = $this->getQualifiedFarKeyName(); //query->join("users", "users.id", "=", "posts.user_id") $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), "=", $farKey); if ($this->throughParentSoftDeletes()) { $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn()); } } }
定義遠(yuǎn)層一對(duì)多關(guān)聯(lián)會(huì)返回一個(gè)IlluminateDatabaseEloquentRelationshasManyThrough類的實(shí)例,實(shí)例化hasManyThrough時(shí)的操作跟實(shí)例化BelongsToMany時(shí)做的操作非常類似。
針對(duì)這個(gè)例子performJoin為關(guān)聯(lián)添加的join約束為:
query->join("users", "users.id", "=", "posts.user_id")
添加的where約束為:
$this->query->where("users.country_id", "=", 1) 假設(shè)country_id是1
對(duì)應(yīng)的SQL查詢是:
SELECT * FROM posts INNER JOIN users ON users.id = posts.user_id WHERE users.country_id = 1
從SQL查詢我們也可以看到遠(yuǎn)層一對(duì)多跟多對(duì)多生成的語句非常類似,唯一的區(qū)別就是它的中間表對(duì)應(yīng)的是一個(gè)已定義的模型。
動(dòng)態(tài)屬性加載關(guān)聯(lián)模型上面我們定義了三種使用頻次比較高的模型關(guān)聯(lián),下面我們?cè)賮砜匆幌略谑褂盟鼈儠r(shí)關(guān)聯(lián)模型時(shí)如何加載出來的。我們可以像訪問屬性一樣訪問定義好的關(guān)聯(lián)的模型,例如,我們剛剛的 User 和 Post 模型例子中,我們可以這樣訪問用戶的所有文章:
$user = AppUser::find(1); foreach ($user->posts as $post) { // }
還記得我們上一篇文章里將獲取模型屬性時(shí)提到過的嗎,如果模型的$attributes屬性里沒有這個(gè)字段,那么會(huì)嘗試獲取模型關(guān)聯(lián)的值:
abstract class Model implements ... { public function __get($key) { return $this->getAttribute($key); } public function getAttribute($key) { if (! $key) { return; } //如果attributes數(shù)組的index里有$key或者$key對(duì)應(yīng)一個(gè)屬性訪問器`"get" . $key` 則從這里取出$key對(duì)應(yīng)的值 //否則就嘗試去獲取模型關(guān)聯(lián)的值 if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) { return $this->getAttributeValue($key); } if (method_exists(self::class, $key)) { return; } //獲取模型關(guān)聯(lián)的值 return $this->getRelationValue($key); } public function getRelationValue($key) { //取出已經(jīng)加載的關(guān)聯(lián)中,避免重復(fù)獲取模型關(guān)聯(lián)數(shù)據(jù) if ($this->relationLoaded($key)) { return $this->relations[$key]; } // 調(diào)用我們定義的模型關(guān)聯(lián) $key 為posts if (method_exists($this, $key)) { return $this->getRelationshipFromMethod($key); } } protected function getRelationshipFromMethod($method) { $relation = $this->$method(); if (! $relation instanceof Relation) { throw new LogicException(get_class($this)."::".$method." must return a relationship instance."); } //通過getResults方法獲取數(shù)據(jù),并緩存到$relations數(shù)組中去 return tap($relation->getResults(), function ($results) use ($method) { $this->setRelation($method, $results); }); } }
在通過動(dòng)態(tài)屬性獲取模型關(guān)聯(lián)的值時(shí),會(huì)調(diào)用與屬性名相同的關(guān)聯(lián)方法,拿到關(guān)聯(lián)實(shí)例后會(huì)去調(diào)用關(guān)聯(lián)實(shí)例的getResults方法返回關(guān)聯(lián)的模型數(shù)據(jù)。 getResults也是每個(gè)Relation子類需要實(shí)現(xiàn)的方法,這樣每種關(guān)聯(lián)都可以根據(jù)自己情況去執(zhí)行查詢獲取關(guān)聯(lián)模型,現(xiàn)在這個(gè)例子用的是一對(duì)多關(guān)聯(lián),在hasMany類中我們可以看到這個(gè)方法的定義如下:
class HasMany extends HasOneOrMany { public function getResults() { return $this->query->get(); } } class BelongsToMany extends Relation { public function getResults() { return $this->get(); } public function get($columns = ["*"]) { $columns = $this->query->getQuery()->columns ? [] : $columns; $builder = $this->query->applyScopes(); $models = $builder->addSelect( $this->shouldSelect($columns) )->getModels(); $this->hydratePivotRelation($models); if (count($models) > 0) { $models = $builder->eagerLoadRelations($models); } return $this->related->newCollection($models); } }關(guān)聯(lián)方法
出了用動(dòng)態(tài)屬性加載關(guān)聯(lián)數(shù)據(jù)外還可以在定義關(guān)聯(lián)方法的基礎(chǔ)上再給關(guān)聯(lián)的子模型添加更多的where條件等的約束,比如:
$user->posts()->where("created_at", ">", "2018-01-01");
Relation實(shí)例會(huì)將這些調(diào)用通過__call轉(zhuǎn)發(fā)給子模型的Eloquent Builder去執(zhí)行。
abstract class Relation { /** * Handle dynamic method calls to the relationship. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } $result = $this->query->{$method}(...$parameters); if ($result === $this->query) { return $this; } return $result; } }預(yù)加載關(guān)聯(lián)模型
當(dāng)作為屬性訪問 Eloquent 關(guān)聯(lián)時(shí),關(guān)聯(lián)數(shù)據(jù)是「懶加載」的。意味著在你第一次訪問該屬性時(shí),才會(huì)加載關(guān)聯(lián)數(shù)據(jù)。不過當(dāng)查詢父模型時(shí),Eloquent 可以「預(yù)加載」關(guān)聯(lián)數(shù)據(jù)。預(yù)加載避免了 N + 1 查詢問題??匆幌挛臋n里給出的例子:
class Book extends Model { /** * 獲得此書的作者。 */ public function author() { return $this->belongsTo("AppAuthor"); } } //獲取所有的書和作者信息 $books = AppBook::all(); foreach ($books as $book) { echo $book->author->name; }
上面這樣使用關(guān)聯(lián)在訪問每本書的作者時(shí)都會(huì)執(zhí)行查詢加載關(guān)聯(lián)數(shù)據(jù),這樣顯然會(huì)影響應(yīng)用的性能,那么通過預(yù)加載能夠把查詢降低到兩次:
$books = AppBook::with("author")->get(); foreach ($books as $book) { echo $book->author->name; }
我們來看一下底層時(shí)怎么實(shí)現(xiàn)預(yù)加載關(guān)聯(lián)模型的
abstract class Model implements ArrayAccess, Arrayable,...... { public static function with($relations) { return (new static)->newQuery()->with( is_string($relations) ? func_get_args() : $relations ); } } //Eloquent Builder class Builder { public function with($relations) { $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); return $this; } protected function parseWithRelations(array $relations) { $results = []; foreach ($relations as $name => $constraints) { //如果$name是數(shù)字索引,證明沒有為預(yù)加載關(guān)聯(lián)模型添加約束條件,為了統(tǒng)一把它的約束條件設(shè)置為一個(gè)空的閉包 if (is_numeric($name)) { $name = $constraints; list($name, $constraints) = Str::contains($name, ":") ? $this->createSelectWithConstraint($name) : [$name, function () { // }]; } //設(shè)置這種用Book::with("author.contacts")這種嵌套預(yù)加載的約束條件 $results = $this->addNestedWiths($name, $results); $results[$name] = $constraints; } return $results; } public function get($columns = ["*"]) { $builder = $this->applyScopes(); //獲取模型時(shí)會(huì)去加載要預(yù)加載的關(guān)聯(lián)模型 if (count($models = $builder->getModels($columns)) > 0) { $models = $builder->eagerLoadRelations($models); } return $builder->getModel()->newCollection($models); } public function eagerLoadRelations(array $models) { foreach ($this->eagerLoad as $name => $constraints) { if (strpos($name, ".") === false) { $models = $this->eagerLoadRelation($models, $name, $constraints); } } return $models; } protected function eagerLoadRelation(array $models, $name, Closure $constraints) { //獲取關(guān)聯(lián)實(shí)例 $relation = $this->getRelation($name); $relation->addEagerConstraints($models); $constraints($relation); return $relation->match( $relation->initRelation($models, $name), $relation->getEager(), $name ); } }
上面的代碼可以看到with方法會(huì)把要預(yù)加載的關(guān)聯(lián)模型放到$eagarLoad屬性里,針對(duì)我們這個(gè)例子他的值類似下面這樣:
$eagarLoad = [ "author" => function() {} ]; //如果有約束則會(huì)是 $eagarLoad = [ "author" => function($query) { $query->where(....) } ];
這樣在通過Model 的get方法獲取模型時(shí)會(huì)預(yù)加載的關(guān)聯(lián)模型,在獲取關(guān)聯(lián)模型時(shí)給關(guān)系應(yīng)用約束的addEagerConstraints方法是在具體的關(guān)聯(lián)類中定義的,我們可以看下HasMany類的這個(gè)方法。
*注: 下面的代碼為了閱讀方便我把一些在父類里定義的方法拿到了HasMany中,自己閱讀時(shí)如果找不到請(qǐng)去父類中找一下。
class HasMany extends ... { // where book_id in (...) public function addEagerConstraints(array $models) { $this->query->whereIn( $this->foreignKey, $this->getKeys($models, $this->localKey) ); } }
他給關(guān)聯(lián)應(yīng)用了一個(gè)where book_id in (...)的約束,接下來通過getEager方法獲取所有的關(guān)聯(lián)模型組成的集合,再通過關(guān)聯(lián)類里定義的match方法把外鍵值等于父模型主鍵值的關(guān)聯(lián)模型組織成集合設(shè)置到父模型的$relations屬性中接下來用到了這些預(yù)加載的關(guān)聯(lián)模型時(shí)都是從$relations屬性中取出來的不會(huì)再去做數(shù)據(jù)庫查詢
class HasMany extends ... { //初始化model的relations屬性 public function initRelation(array $models, $relation) { foreach ($models as $model) { $model->setRelation($relation, $this->related->newCollection()); } return $models; } //預(yù)加載出關(guān)聯(lián)模型 public function getEager() { return $this->get(); } public function get($columns = ["*"]) { return $this->query->get($columns); } //在子類HasMany public function match(array $models, Collection $results, $relation) { return $this->matchMany($models, $results, $relation); } protected function matchOneOrMany(array $models, Collection $results, $relation, $type) { //組成[父模型ID => [子模型1, ...]]的字典 $dictionary = $this->buildDictionary($results); //將子模型設(shè)置到父模型的$relations屬性中去 foreach ($models as $model) { if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { $model->setRelation( $relation, $this->getRelationValue($dictionary, $key, $type) ); } } return $models; } }
預(yù)加載關(guān)聯(lián)模型后沒個(gè)Book Model的$relations屬性里都有了以關(guān)聯(lián)名author為key的數(shù)據(jù), 類似下面
$relations = [ "author" => Collection(Author)//Author Model組成的集合 ];
這樣再使用動(dòng)態(tài)屬性引用已經(jīng)預(yù)加載關(guān)聯(lián)模型時(shí)就會(huì)直接從這里取出數(shù)據(jù)而不用再去做數(shù)據(jù)庫查詢了。
模型關(guān)聯(lián)常用的一些功能的底層實(shí)現(xiàn)到這里梳理完了,Laravel把我們平常用的join, where in 和子查詢都隱藏在了底層實(shí)現(xiàn)中并且?guī)臀覀儼严嗷リP(guān)聯(lián)的數(shù)據(jù)做好了匹配。還有一些我認(rèn)為使用場(chǎng)景沒那么多的多態(tài)關(guān)聯(lián)、嵌套預(yù)加載那些我并沒有梳理,并且它們的底層實(shí)現(xiàn)都差不多,區(qū)別就是每個(gè)關(guān)聯(lián)類型有自己的關(guān)聯(lián)約束、匹配規(guī)則,有興趣的讀者自己去看一下吧。
本文已經(jīng)收錄在系列文章Laravel源碼學(xué)習(xí)里,歡迎訪問閱讀。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/30716.html
摘要:過去一年時(shí)間寫了多篇文章來探討了我認(rèn)為的框架最核心部分的設(shè)計(jì)思路代碼實(shí)現(xiàn)。為了大家閱讀方便,我把這些源碼學(xué)習(xí)的文章匯總到這里。數(shù)據(jù)庫算法和數(shù)據(jù)結(jié)構(gòu)這些都是編程的內(nèi)功,只有內(nèi)功深厚了才能解決遇到的復(fù)雜問題。 過去一年時(shí)間寫了20多篇文章來探討了我認(rèn)為的Larave框架最核心部分的設(shè)計(jì)思路、代碼實(shí)現(xiàn)。通過更新文章自己在軟件設(shè)計(jì)、文字表達(dá)方面都有所提高,在剛開始決定寫Laravel源碼分析地...
摘要:第三步注冊(cè)工廠啟動(dòng)數(shù)據(jù)庫服務(wù)數(shù)據(jù)庫服務(wù)的啟動(dòng)主要設(shè)置的連接分析器,讓能夠用服務(wù)連接數(shù)據(jù)庫。 在我們學(xué)習(xí)和使用一個(gè)開發(fā)框架時(shí),無論使用什么框架,如何連接數(shù)據(jù)庫、對(duì)數(shù)據(jù)庫進(jìn)行增刪改查都是學(xué)習(xí)的重點(diǎn),在Laravel中我們可以通過兩種方式與數(shù)據(jù)庫進(jìn)行交互: DB, DB是與PHP底層的PDO直接進(jìn)行交互的,通過查詢構(gòu)建器提供了一個(gè)方便的接口來創(chuàng)建及運(yùn)行數(shù)據(jù)庫查詢語句。 Eloquent...
摘要:請(qǐng)求未通過的驗(yàn)證時(shí)會(huì)拋出此異常。異常處理是非常重要但又容易讓開發(fā)者忽略的功能,這篇文章簡(jiǎn)單解釋了內(nèi)部異常處理的機(jī)制以及擴(kuò)展異常處理的方式方法。 異常處理是編程中十分重要但也最容易被人忽視的語言特性,它為開發(fā)者提供了處理程序運(yùn)行時(shí)錯(cuò)誤的機(jī)制,對(duì)于程序設(shè)計(jì)來說正確的異常處理能夠防止泄露程序自身細(xì)節(jié)給用戶,給開發(fā)者提供完整的錯(cuò)誤回溯堆棧,同時(shí)也能提高程序的健壯性。 這篇文章我們來簡(jiǎn)單梳理一下...
摘要:本節(jié)將實(shí)現(xiàn)文章評(píng)論與用戶關(guān)聯(lián)的功能。關(guān)系定義首先修改與表,增加字段增加全部回滾并重新執(zhí)行遷移添加用戶表與文章表評(píng)論表的一對(duì)多關(guān)系添加文章評(píng)論表與用戶表的多對(duì)一關(guān)系同時(shí),評(píng)論表的字段增加。同時(shí),我們還自定義了返回的錯(cuò)誤信息。 本節(jié)將實(shí)現(xiàn)文章、評(píng)論與用戶關(guān)聯(lián)的功能。 關(guān)系定義 首先修改 posts 與 comments 表,增加 user_id 字段 /database/migratio...
摘要:模式定義觀察者模式定義對(duì)象間的一種一對(duì)多依賴關(guān)系,使得每當(dāng)一個(gè)對(duì)象狀態(tài)發(fā)生改變時(shí),其相關(guān)依賴對(duì)象皆得到通知并被自動(dòng)更新。 觀察者模式 Laravel的Event事件系統(tǒng)提供了一個(gè)簡(jiǎn)單的觀察者模式實(shí)現(xiàn),能夠訂閱和監(jiān)聽?wèi)?yīng)用中發(fā)生的各種事件,在PHP的標(biāo)準(zhǔn)庫(SPL)里甚至提供了三個(gè)接口SplSubject, SplObserver, SplObjectStorage來讓開發(fā)者更容易地實(shí)現(xiàn)觀...
閱讀 3402·2021-11-04 16:10
閱讀 3903·2021-09-29 09:43
閱讀 2728·2021-09-24 10:24
閱讀 3440·2021-09-01 10:46
閱讀 2538·2019-08-30 15:54
閱讀 620·2019-08-30 13:19
閱讀 3271·2019-08-29 17:19
閱讀 1085·2019-08-29 16:40