iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 18
0
自我挑戰組

花式PHP系列 第 19

Eloquent:深入with

  • 分享至 

  • xImage
  •  

/images/emoticon/emoticon54.gif

Eloquent\Builder

with() 的用法可以參考文件
with('article:id,name') 這種用法請參考5.5版的文件

$query->with($relation)

<?php

// @see http://php.net/manual/en/function.func-get-args.php
public function with($relations)
{
    $eagerLoad = $this->parseWithRelations(
        is_string($relations) ? func_get_args() : $relations
    );
    $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
    return $this;
}

這一段不長,它的的意思基本上就是:
把收到的所有「關聯」用 parseWithRelations() 處理過之後,
再把結果跟目前的 $this->eagerLoad 合併起來。

處理參數

<?php
is_string($relations) ? func_get_args() : $relations;

至於上面這行的用意在:
雖然 with() 也接受 string 的參數,但 parseWithRelations() 收的是陣列,
所以它會用 func_get_args() 重新把傳入的參數以陣列的形式讀出來,
這樣子就能確保傳遞給 parseWithRelations() 的變數只有可能是陣列了。

parseWithRelations()

<?php
protected function parseWithRelations(array $relations)
{
    $results = [];
    foreach ($relations as $name => $constraints) {
        if (is_numeric($name)) {
            $name = $constraints;
            
            // @see http://php.net/manual/en/function.list.php
            // 請特別注意 list() 在 PHP5 及 PHP7 有行為的差異
            list($name, $constraints) = Str::contains($name, ':')
                ? $this->createSelectWithConstraint($name)
                : [$name, function () {
                    //
                }];
        }
        $results = $this->addNestedWiths($name, $results);
        $results[$name] = $constraints;
    }
    return $results;
}

這一段程式碼主要透過 foreach 解析傳入的所有關聯。

其中 if-else 中間的內容完全是在做參數的處理,
作者會在這段把「裝著關聯的陣列」,
都轉換成「key 是關聯名稱;value 是描述附加條件的 callable」的陣列。

最後在真的把關聯及條件塞進 $result 之前,
透過 addNestedWiths() 處理「巢狀預載入」。

最後再回傳給 with() 的 $eagerLoad,
讓它能與已經設定好的條件($this->eagerLoad)合併。

參數處理

<?php
foreach ($relations as $name => $constraints) {
    /*...*/
}

透過 foreach 我們可以發現到:
作者認為每一陣列元素的 key 是 $name;value 是 $constraints。
這種寫法可以讓我們直接聯想到文件中提到的「預載入條件限制」。

但因為也支援 with('relationOne', 'relationTwo') 這種「只傳關聯名稱」的用法,
(這兩個參數是以一般陣列的方式傳入 parseWithRelations())
所以緊接著的 if-else 會檢查 $name 是不是數字,
是的話 $name = $constraints
接著再給 $constraint 指派一個空的 Callable。

釋義

比如說:

<?php

// 首先是
with('author', 'comments:id');

// 接著會
parseWithRelations([0 => 'author', 1 => 'comments:id']);

// 在 foreach 中的 is_numeric($name) 符合時
$relation = [
    'author' => 'author',
    'comments:id' => 'comments:id'
];

// 然後變成
$relation = [
    'author' => function () { /*...*/ },
    'comments' => $this->createSelectWithConstraint($name)
];

createSelectWithConstraint()

<?php

protected function createSelectWithConstraint($name)
{
    return [explode(':', $name)[0], function ($query) use ($name) {
        $query->select(explode(',', explode(':', $name)[1]));
    }];
}

這是用在處理下面這種只撈特定欄位的行為的。

它這邊的語法比較混雜,
但總之它是回傳一個「包含關聯名稱及附加條件的callback」的陣列回去。
(並透過 list() 把結果分別指派給 $name 及 $constraints)

addNestedWiths()

<?php

protected function addNestedWiths($name, $results)
{
    $progress = [];
    foreach (explode('.', $name) as $segment) {
        $progress[] = $segment;
        if (! isset($results[$last = implode('.', $progress)])) {
            $results[$last] = function () {
                //
            };
        }
    }
    return $results;
}

自動依層載入

有趣的是,在它把關聯名稱用「.」拆成一個一個片段之後,
它會自己去依序設定要載入每一層關聯(同時已經設定過的就會略過)。

比如說如果我是這樣寫的:

<?php

with([
    'author.organization',
    'author.organization.members.article'
]);

那麼當它處理到第二個關聯(author.organization.members.article)的時候,
它會依序檢查下列的關聯有沒有被載入了:
1.author
2.author.organization
3.author.organization.members
4.author.organization.members.article

所以其實當你有一個超巢的關聯(4)要載入,那麼你也不需要設定多餘的關聯了(1,2,3)。

想法:巢狀預載入的優化策略

因為筆者只寫過 5.2 版的 Laravel
所以底下的內容是筆者在看完這段程式碼之後的一些想法,並不代表實際狀況,
如果有什麼想法請留言在底下~

但如果是透過只寫一筆巢狀關聯來作載入的話,每一層關聯都會拉回所有欄位的樣子,
這樣似乎不是很好r。

但看起來目前的 createSelectWithConstraint()
處理不了在巢狀預載入中各自設定載入欄位的狀況。
addNestedWiths() 似乎也沒處理的意思。

所以底下這種寫法應該是不成立的:

<?php

with('author:id.organization:id.members:id.article:id')

但如果手動設定的話應該還是辦得到的:

<?php

with(
    'author:id',
    'author.organization:id',
    'author.organization.members:id',
    'author.organization.members.article:id'
);

這樣就可以降低每一層實際從資料庫拉出的欄位了吧?
不過要小心不要沒拉到能讓 Laravel 繼續往下層抓資料的欄位就是了~
收工~


上一篇
Laravel:Inspiring
下一篇
Eloquent:以withCount優化
系列文
花式PHP31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言