---
# System prepended metadata

title: 2020 年 09 月

---

2020 年 09 月
===

# 蒼時

先從 Rails API 文件裡面找到 [`#includes`](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes) 方法，在 `QueryMethods` 模組裡面也可以看到像是 `#eager_load` 和 `#preload` 兩個方法，這也是 Rails 最常被拿來比較的三個實作。

## #includes 方法

展開的原始碼會轉到 `#spawn` 後的物件處理

```ruby
def includes(*args)
  check_if_method_has_arguments!(:includes, args)
  spawn.includes!(*args)
end
```

繼續在 GitHub 尋找，會發現 `#spawn` 基本上是複製一次物件，因此實際上要找的 `#includes!` 實際上就在 `#includes` 原始碼下方（ https://github.com/rails/rails/blob/070d4afacd3e9721b7e3a4634e4d026b5fa2c32c/activerecord/lib/active_record/relation/query_methods.rb#L151 ）

```ruby
    def includes!(*args) # :nodoc:
      args.reject!(&:blank?)
      args.flatten!

      self.includes_values |= args
      self
    end
```

關鍵點應該是對 `includes_values` 做了交集處理

```ruby
    Relation::VALUE_METHODS.each do |name|
      method_name = \
        case name
        when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
        when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
        when *Relation::CLAUSE_METHODS then "#{name}_clause"
        end
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{method_name}                   # def includes_values
          default = DEFAULT_VALUES[:#{name}] #   default = DEFAULT_VALUES[:includes]
          @values.fetch(:#{name}, default)   #   @values.fetch(:includes, default)
        end                                  # end
        def #{method_name}=(value)           # def includes_values=(value)
          assert_mutability!                 #   assert_mutability!
          @values[:#{name}] = value          #   @values[:includes] = value
        end                                  # end
      CODE
    end
```

不過線索到 `includes_values` 就斷掉了，他是利用 DSL 生成的方法，而這個方法會把資料把存在 `@values` 裡面。

## self.includes_values 相關的程式碼

在 `Relation` 物件上會做一些處理（ https://github.com/rails/rails/blob/d04acc0bc5b5b9f5b512460ebb149ea8ac3012f3/activerecord/lib/active_record/relation.rb ）

> 記憶中 Relation 物件上會被製作成像是 `User_Relation` 的物件，通常是在 Model 被定義的時候

### eager_loading?

判斷是否需要 `eager_load` 的情況，如果有 `eager_load` 或者 `includes_value` 且符合條件

```ruby
    # Returns true if relation needs eager loading.
    def eager_loading?
      @should_eager_load ||=
        eager_load_values.any? ||
        includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
    end
```

> 因為 `||` 優先度比 `&&` 低，所以 `includes_values` 存在時還要符合後面兩個條件

* `joined_includes_values` 的條件是 `joins` 和 `includes` 的資料表有重疊
* `references_eager_loaded_tables?` 的條件複雜很多，大致上就是把 `references` 過的表去掉 `joins` 的表如果沒有其他數值的話就符合（待驗證） 

### preload_associations

另一個情況是處理 `preload` 時要找出關聯的資料

```ruby
    def preload_associations(records) # :nodoc:
      preload = preload_values
      preload += includes_values unless eager_loading?
      preloader = nil
      scope = strict_loading_value ? StrictLoadingScope : nil
      preload.each do |associations|
        preloader ||= build_preloader
        preloader.preload records, associations, scope
      end
    end
```

這邊明顯有一個判定條件就是如果被 `eager_load` 過的畫就不會加入到 `preload` 的條件，因此兩者實際上是互斥的

## eager_load

文件上的說明如下

> Forces eager loading by performing a LEFT OUTER JOIN on args

簡單說如果使用 `eager_load` 的話會強制的做 `LEFT OUTER JOIN` 的處理

## preload

文件上說明如下

> Allows preloading of args, in the same way that includes does

看起來是做跟 `includes` 類似的處理，基本上也就是先把相關的表做 `SELECT` 出來的處理

## 小結

由上面幾點來看，基本上 `includes` 在預設情況下跟 `preload` 是相同的，但是當我們的 `includes` 符合 `joins` 或者 `references` 的處理，因為需要做 `LEFT OUTER JOIN` 的處理，他就會被切換到 `eager_load` 的處理下面，同時 `preload` 的處理則跳過所有 `includes` 相關的處理。

簡單來說只要符合 `eage_load` 的條件，就會因為需要做 JOIN 查詢而無法進行 `preload` 類型的處理，使用 `joins` 或者 `references` 都會造成同樣的現象（即使手動做 `preload` 也不會有反應）

> `preload` 只要不跟 `joins` 或 `references` 重疊都會正常運作，至於當重疊時是如何被排除在 `preload` 之外可能就要另外討論，或者 `preload` 本身也會有 `JOIN` 類型的處理，目前還沒有追到原因。