DEPRECATION WARNING: Class level methods will no longer inherit scoping from `スコープ名` in Rails 6.1. 〜〜 の解説

Rails6.0に上げると、特定の状況がご自身のアプリケーションにある場合以下のようなwarningが出ます。

```
 .DEPRECATION WARNING: Class level methods will no longer inherit scoping from `toplevel` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Topic.unscoped`. (called from block in <class:Topic> at パスと該当行番号
 ```

* SQLの挙動を担保したままどのようにこのwarningをどうやって出さなくするのか

について詳しく解説します。

ただし私個人的にはRailsの対象PRは読みきれていなく、ただのSQL調査による考察なので、100%正しいかどうかはわかりません。でもこの記事のように考えると色々辻褄が合うような気がしています。
何か間違いやさらなる考察などありましたら教えていただけますと幸いです。

少し前提が長くなりますが、ぜひ最後までよろしくお願いします。


# 一応 TL;DR

[結局どうすればいいか](https://qiita.com/KeiSakam/items/7964fd43865c7fea8817#%E7%B5%90%E5%B1%80%E3%81%A9%E3%81%86%E3%81%99%E3%82%8C%E3%81%B0%E3%81%84%E3%81%84%E3%81%8B)

## 今回の問題
問題を把握すること自体がかなりややこしいです。
状況自体がややこしいのと、言葉遣いがふわっとしてることが原因です。

### 問題を読み解くための情報源
以下の情報源を使って話を進めていきます。特に２つ目の解説ブログは必ずご一読ください。意味はわからなくても後に私が解説します。

* kamioさんという方がRailsに対してPRを出して、ご本人が軽くそのPRを出した意図のようなものを解説してくだ去っているブログの記事が以下です。
    * [Railsへの対象PR](https://github.com/rails/rails/pull/35280)
    * [kamipoさんの解説ブログ](https://blog.kamipo.net/entry/2019/05/15/152652)の３つ目の話題

これからの説明に使うため、記事の例にあげてあるコードをそのまま引用させていただきます。

``` minimum_application.rb
#こちらはブログのsample code そのままです。
class Topic < ActiveRecord::Base
  scope :toplevel, -> { where(parent_id: nil) }
  scope :children, -> { where.not(parent_id: nil) }
  scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
end

# Works as expected.
Topic.toplevel.where(id: Topic.children.select(:parent_id))

# Doesn't work due to leaking `toplevel` to `Topic.children`.
Topic.toplevel.has_children
```
とにかくscopeをチェーンすると、クラスグローバルの状態が汚染されると言っています。なので、**汚染**が今回の問題であり、**汚染**が何を意味しているのかが正確に理解することから始まります。

### 問題点を読み解くためのポイント
* **正確に**どんな状況でwarning message が出るのか
* 当たり前ですがこのkamipoさんの例がminimum applicationであるということ。
    * 「scopeのチェーンの話だから例のscopeは2つで十分では？」と考えたくなりますがそうではありません。問題の要件を満たすためには3つ必要です。
* 汚染されているものは正確にどのオブジェクトなのかを見極めること。
* 暗黙的に`where`の前についている`Topic.where`の`Topic`なのか、`Topic.children`なのか、どっちの`Topic`の話をしているのかを常に区別して考えること。ここだけ結論から先に言うと`where`の前に暗黙的に付いている`Topic`は今回の問題と全く関係ありません。ここから`「Topic」`と言ったら`Topic.children`の`Topic`のことだと解釈お願いします。
* kamipoさんの少し紛らわしい言葉遣いに注意すること。私みたいに素人からするとなかなか理解が大変だと思いますが、このあたりも後に解説します。例えば、

    * warnign messageからだと
  
 ```
 .DEPRECATION WARNING: Class level methods will no longer inherit scoping from `toplevel` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Topic.unscoped`. (called from block in <class:Topic> at パスと該当行番号
 ```

 * `Class level methods`、` inherit scoping `、`scope relation`、pass `it` into `the block` directly 、  `instead`、`full set of model` など。 ほぼ全部正確に何を意味しているのか微妙にわかりづらいですね。

    * ブログからだと`leak to` 、`汚染`、`クラスグローバル`、    `as expected` が正確に何を意味しているのかなどです。

    * どれがscopeでどれがscopeじゃないのか。当たり前ですがたまに混同します(笑)。要するに対応関係をはっきりさせるということと同じです。


### 「汚染」の意味
今までは前提で、ここからが解説になります。


「汚染」の意味は、
**`(Topic).where`の`Topic`ではなく、`Topic.children`の`Topic`が、実は`toplevel`にすり替わっていた**ということ。
つまりkamipoさんの言葉を使うなら、**toplevelのscopeが leak into Topic していた**ということです。
この場合`has_children`の中ではクラスグローバルな`Topic.children`ではなく、汚染された`toplevel.children`の挙動になってしまっている、ということです。

もう一度サンプルコードを見てみます

``` minimum_application.rb

#こちらはブログのsample code そのままの再掲です。
class Topic < ActiveRecord::Base
  scope :toplevel, -> { where(parent_id: nil) }
  scope :children, -> { where.not(parent_id: nil) }
  scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
end

# Works as expected.　　　　このコードがwarning message の 'pass it into the block directly' でやれと言われていること。
Topic.toplevel.where(id: Topic.children.select(:parent_id)) #ここのTopic.childrenのTopic はクラスグローバルな状態が保たれている。

# Doesn't work due to leaking `toplevel` to `Topic.children`.  (<- leak to じゃなくて 'into' にした方がよりわかりやすいですね。 toplevel のscopeが落ちちゃうのかと思いました(笑)
Topic.toplevel.has_children #上のコードと同じように見えるが、'has_children'のなかのTopic.childrenのTopic は実は toplevel にすり替わっていた、というのが「汚染」の意味です。

```


### 結局原因は...
#### scopeの`「チェーン」`
warning message 

 ```
 .DEPRECATION WARNING: Class level methods will no longer inherit scoping from `toplevel` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Topic.unscoped`. (called from block in <class:Topic> at パスと該当行番号
 ```

一部である `To continue using the scoped relation, pass it into the block directly.`に注目します。
私が先に解説した通り、

``` minimum_application.rb
# Doesn't work due to leaking `toplevel` to `Topic.children`.
Topic.toplevel.has_children
```

とするとscopeのチェーンになっていて、`has_children`の中に含まれている`children`のレシーバである`Topic`は`toplevel`にすり替わった挙動をします(toplevel が Topicに leak into している)が、

kamipoさんの言う通り、例にあるように

```minimum_application.rb.rb
# Works as expected.
Topic.toplevel.where(id: Topic.children.select(:parent_id))
```
とすれば、 `Topic`は`Topic`のままで、クラスグローバルな状態は保たれています。

つまりwarning messageの`To continue using the scoped relation, pass it into the block directly.`の意味としては

「scopeチェーンをして、かつクラスグローバルな状態を保ちたいなら、もともとscopeを定義するときに書いた
`has_children`のblock 、つまり`{ where(id: Topic.children.select(:parent_id)) }`に `it`( レシーバの`toplevel`、もしくは解釈のしようによっては`has_children`そのもの)を直接渡しなさい」と言う意味です。そうすれば、kamipoさんの一つ目の例が示す通り、クラスグローバルな状態が保たれます。



###  [Railsへの対象PR](https://github.com/rails/rails/pull/35280)では何をしているのか
きちんとPRのコードを読めていないので、**kamipoさんの言説を信じるとするならば**ですが、`toplevel.has_cildren`と言う風にscopeをチェーンさせた時の`has_children`の中身の`Topic.children`の挙動が、`toplevel.children`の挙動から本来のコード通りの`Topic.children`の挙動に変わります。
これがブログ中の「これからは`unscoped`をつけるのを  **忘れないようにすること** を頑張らなくてもいいようになります」の意味だと私は捉えています。


#### 用語の解説
ここで先の[問題点を読み解くためのポイント](https://qiita.com/KeiSakam/items/7964fd43865c7fea8817#%E5%95%8F%E9%A1%8C%E7%82%B9%E3%82%92%E8%AA%AD%E3%81%BF%E8%A7%A3%E3%81%8F%E3%81%9F%E3%82%81%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)で出てきた紛らわしい単語の解説をします。
 
```
 .DEPRECATION WARNING: Class level methods will no longer inherit scoping from `toplevel` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Topic.unscoped`. (called from block in <class:Topic> at パスと該当行番号
 ```
* 上記のwarnign messageにおいて
    * `Class level methods` => 言うならば`children`というscopeのことだと思います。`Topic.children`と書いたはず( Topic というclass level にアクセスしたと思った)が、`toplevel`というscope を inherit してしまっているつまり汚染されている状態、が今後はinherit(汚染)しませんよ。挙動変わりますよと言うこと。
    * ` inherit scoping ` => scopeのチェーンそのもののことを言っているのではなく、いわば今回の汚染された状態のことです。`Topic.children`が`toplevel.children`の挙動に現行でなってしまっていることを、「`toplevel`というscopeを`inherit`している」と表現しています。
    * `scope relation`=>こっちがscopeのチェーンのことです。`toplevel.has_children`のこと。
    * pass `it` into `the block` directly  =>先ほど示した通り。こっちは`into` じゃなくて`to`にすべきな気がしますが、、、。
    *   `instead` => 「汚染された`Topic(toplevel)`にアクセスする代わり」に、なのか、一つ上のように「正しい挙動をさせるための一つの対処法である、ブロックに直接渡す方法の代わりに」なのか、正直英語が微妙でよくわかりませんし、どっちでもいいと思います。
    * `full set of model` => `has_children`の中の`Topic.children`の`Topic`のことです。
* ブログからだと
    * `leak to` =>leak **into** と解釈しましょう。ブログの言葉を使うと、`toplevel が leaks into Topic している`
    * `汚染`=> 先に示した通り。leak into や「別のscopeが注入」と同義です。
    * `クラスグローバル` => `Topic.children`が文字通り`Topic`の`full set of modfel`にアクセスできる挙動の状態。
    * `as expected` => 「クラスグローバル」と同じです。文字通り`Topic`の`full set of modfel`にアクセスしている挙動を期待している(as expected ) していることでしょう。
    * 「意図せず別のscopeが注入されている」=> 別のscopeとは`toplevel`のこと。「注入」はleak into と同じだと捉えています。
  
## waning messageの意味
3文に分けて解説します。「汚染」の意味と先の用語の解説が分かれば読み解くのは比較的簡単です。

* `Class level methods will no longer inherit scoping from `toplevel` in Rails 6.1`
☝️ Rails 6.1では、`toplevel` のscopeがleak into Topic しなくなるよってこと。

* `To continue using the scoped relation, pass it into the block directly.`
☝️正しい挙動のまま、`Topic.children`の `Topic`が文字通りクラスグローバルで汚染されていないまま、scopeのチェーンをしたいなら(`To continue using the scoped relation`)、Rails 6.0まではブロックに直接渡しなさい。つまり今Rails 6.0環境ではscope **チェーン**だと正しい挙動はできないですよ。今のあなたのコードでは意図した挙動になっていませんよ。

* ` To instead access the full set of models, as Rails 6.1 will, use `Topic.unscoped`. (called from block in <class:Topic>`
☝️leak into `Topic`したくなくて、このままの実装で6.1の挙動みたいに`Topic`そのものを使いたいなら、今は`Topic.unscoped.chldren`を使いなさい。ということ。`instead` というのは何の「代わり」かというと、「leak into `Topic` している状態の代わりに」、という意味かもしくは「ブロックを使って正しい挙動をさせる策の代わりに」。


## 結局どうすればいいか

**Rails6.0の段階では`unscoped`をつけて`Topic.unscoped`にして、Rails6.1に上げるときに `unscoped`を除去する**

という方法が最適だと思っています。(以下の①のパターン)
つまり既存の汚染されている実装の挙動を修正しつつwarningを出さなくするということですね。

確かにwarningをなくす「だけ」なら `Topic`を外す、とか`Topic`の代わりに`self`をつけるという選択肢でも実現できます。

以下の②、③ような感じです。またサンプルコードをお借りしますが

``` minimum_application.rb

#こちらはブログのsample code そのままの再掲です。
class Topic < ActiveRecord::Base
  #略
  ①scope :has_children, -> { where(id: Topic.unscoped.children.select(:parent_id)) } #最適な方法は多分これ。
  ②scope :has_children, -> { where(id: children.select(:parent_id)) } #Topicを外しただけ！！とか
  ③scope :has_children, -> { where(id: self.children.select(:parent_id)) } #selfをつけるパターン
end
```

ですが、SQLの挙動を見ればわかりますが、これだと`Topic.children`の時と挙動は全く一緒です。つまり汚染されたままです。でもwarningは出ていません。汚染なんてされててもいいからとにかく挙動は現状と変えたくないんだ、という方であれば、Rails6.0までだったらそれでOKだと思います。ですが...


Rails のPRのコードがきちんと読める方なら問題ないかもしれませんが、とにかくkamipoさんは、Topicを外す`children`だけの時と、`self`をつける`self.children`の時の挙動に関して、Rails6.**1**に上げるとき、変わるとも変わらないとも言っていません。要するに何も言っていないので、Rails6.**1**にあげた時の挙動がどうなるかわからない、ということです。deprecation warning が出ていないということは、変わらないと考えた方が自然ですが、汚染されたまま残すというのも不自然です。よくわかりません。


なので「汚染」まで無くして、warningもでなくして、かつRails6.1にあげた時の挙動が担保されているのが、`unscoped`をつける方法なのではないか、というのが私の考えです。Rails6.1に上げた時、`unscoped`は取り外しても外さなくてもどっちでもいいと思います。挙動は同じになるので。


ご自身のコードを今一度確かめてみて、kamipoさんのブログのコードとの対応関係を**正確に**照らし合わせてみるとわかりやすいと思います。


### おまけ

#### ①scopeチェーンの最初のscopeは汚染されているのか
例えば、以下のようなチェーンの時、`some_scope`は汚染されているのかどうか

```hoge.rb
some_scope.another_scope.theother_scope
```

結論、**されていません**。
自分の前にscopeのレシーバがある時のみ汚染の対象となるからです。もっと今回の話題に沿って言うと、`some_scope`は汚染されているされていないの問題はなく、どっちでもありません。kamipoさんの言葉を使うなら、leak するscopeがレシーバにありません。

#### ②scopeが他のscopeに「含まれること」に関しては何か挙動がおかしくなる訳ではない。

```hogehoge.rb
scope :hogehoge_scope, ->(user) do
  where(hogehoge_id: Topic.some_scope(user).pluck(:id))
end
```

みたいに、`some_scope`が`hogehoge_scope`に含まれていたとしても、その`hogehoge_scope`の`Topic`がなんかにすり替わるという訳でもないですし、`some_scope`の中の`Topic`がhogehoge_scopeにすり替わる、という訳でもありません。詳しくはSQLで確認してください。

[結局原因は](https://qiita.com/KeiSakam/items/7964fd43865c7fea8817#%E7%B5%90%E5%B1%80%E5%8E%9F%E5%9B%A0%E3%81%AF)でスコープの`「チェーン」`の**チェーン**を強調したのは、他のscopeに含まれることではないぞ、ということを意識したためです。