成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Laravel核心解讀--Database(四) 模型關(guān)聯(lián)

gekylin / 1597人閱讀

摘要:為關(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

相關(guān)文章

  • Laravel核心解讀--完結(jié)篇

    摘要:過去一年時(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源碼分析地...

    laoLiueizo 評(píng)論0 收藏0
  • Laravel核心解讀--Database(一)基礎(chǔ)介紹

    摘要:第三步注冊(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...

    reclay 評(píng)論0 收藏0
  • Laravel核心解讀--異常處理

    摘要:請(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)單梳理一下...

    includecmath 評(píng)論0 收藏0
  • Laravel 5.4 入門系列 9. 注冊(cè)與登錄,用戶關(guān)聯(liá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...

    smallStone 評(píng)論0 收藏0
  • Laravel核心解讀--觀察者模式

    摘要:模式定義觀察者模式定義對(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)觀...

    lei___ 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<