Railsのroutesでshallow: trueを利用したときのメリット、デメリットを考察してみた（サンプルコード付き）

Railsのルーティングでは、[shallow: true](https://railsguides.jp/routing.html#%E3%80%8C%E6%B5%85%E3%81%84%E3%80%8D%E3%83%8D%E3%82%B9%E3%83%88)を使用することでURLを"浅く"することができます。使用前後のサンプルコードを用いて、そのメリット・デメリットを考察しました。

# TL;DR
## メリット
* URLから取得できるパラメータに冗長性がなくなる
* ポリモーフィックURL、URLヘルパーのパラメータや文字数が減る

## デメリット
* パーシャルビューとしてnewとeditで共通化したform_withの使い方がトリッキーになる

# 考察用サンプルアプリの仕様

デザインはほとんどscaffoldのままなので、見にくいところがあるかもしれません。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205260/23ac9fcd-5d36-994a-abbe-579f3d25e03a.png)|
|:-:|

* ブログ(Blog)
    * ブログの一覧表示
    * ブログの表示+ブログ内の記事一覧
    * ブログ新規登録
    * ブログ変更
    * ブログ削除

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205260/5eeb4656-57b2-eb1f-9507-b58bc4613254.png)|
|:-:|

* 記事(Entry)
    * ブログ内の記事一覧
    * 記事の表示+コメント表示
    * 記事の新規登録
    * 記事変更
    * 記事削除

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205260/b6aab2a0-5bf9-646b-a1b6-169eafce1d1b.png)|
|:-:|

* コメント(Comment)
    * コメント追加
    * コメント削除

# ソースコードの変化

さて、ここから、`shallow: true`なしとありで、ソースコードがどのように変化したかをdiffで示していきます。

## ルーティング

```diff:routes.rb
 Rails.application.routes.draw do
-  resources :blogs do
+  resources :blogs, shallow: true do
     resources :entries, except: %i(index) do
       resources :comments, only: %i(create destroy)
     end
   end
 end
```

```diff:routes
                    Prefix Verb   URI Pattern                                                                              Controller#Action
-      blog_entry_comments POST   /blogs/:blog_id/entries/:entry_id/comments(.:format)                                     comments#create
-       blog_entry_comment DELETE /blogs/:blog_id/entries/:entry_id/comments/:id(.:format)                                 comments#destroy
+           entry_comments POST   /entries/:entry_id/comments(.:format)                                                    comments#create
+                  comment DELETE /comments/:id(.:format)                                                                  comments#destroy
              blog_entries POST   /blogs/:blog_id/entries(.:format)                                                        entries#create
            new_blog_entry GET    /blogs/:blog_id/entries/new(.:format)                                                    entries#new
-          edit_blog_entry GET    /blogs/:blog_id/entries/:id/edit(.:format)                                               entries#edit
-               blog_entry GET    /blogs/:blog_id/entries/:id(.:format)                                                    entries#show
-                          PATCH  /blogs/:blog_id/entries/:id(.:format)                                                    entries#update
-                          PUT    /blogs/:blog_id/entries/:id(.:format)                                                    entries#update
-                          DELETE /blogs/:blog_id/entries/:id(.:format)                                                    entries#destroy
+               edit_entry GET    /entries/:id/edit(.:format)                                                              entries#edit
+                    entry GET    /entries/:id(.:format)                                                                   entries#show
+                          PATCH  /entries/:id(.:format)                                                                   entries#update
+                          PUT    /entries/:id(.:format)                                                                   entries#update
+                          DELETE /entries/:id(.:format)                                                                   entries#destroy
                     blogs GET    /blogs(.:format)                                                                         blogs#index
                           POST   /blogs(.:format)                                                                         blogs#create
                  new_blog GET    /blogs/new(.:format)                                                                     blogs#new
```

shallow: trueのありなしでルーティングが浅くなります。blogsに対して設定すると、孫にあたるcommentsも影響を受けて浅くなっています。

## コントローラ

set_modelする箇所が必要最低限になり、redirectするときのポリモーフィックURLが簡単になります。

### EntriesController
```diff:entries_controller.rb
 class EntriesController < ApplicationController
-  before_action :set_blog
+  before_action :set_blog, only: %i(new create)
   before_action :set_entry, only: %i(show edit update destroy)

   def show
@@ -16,7 +16,7 @@ class EntriesController < ApplicationController
     @entry = @blog.entries.new(entry_params)

     if @entry.save
-      redirect_to [@blog, @entry], notice: 'Entry was successfully created.'
+      redirect_to @entry, notice: 'Entry was successfully created.'
     else
       render :new
     end
@@ -24,7 +24,7 @@ class EntriesController < ApplicationController

   def update
     if @entry.update(entry_params)
-      redirect_to [@blog, @entry], notice: 'Entry was successfully updated.'
+      redirect_to @entry, notice: 'Entry was successfully updated.'
     else
       render :edit
     end
@@ -32,7 +32,7 @@ class EntriesController < ApplicationController

   def destroy
     @entry.destroy
-    redirect_to @blog, notice: 'Entry was successfully destroyed.'
+    redirect_to @entry.blog, notice: 'Entry was successfully destroyed.'
   end

   private
@@ -41,7 +41,7 @@ class EntriesController < ApplicationController
     end

     def set_entry
-      @entry = @blog.entries.find(params[:id])
+      @entry = Entry.find(params[:id])
     end
```

diffにはありませんが、`set_blog`で`@blog = Blog.find(params[:blog_id]`しています。

shallowなしの場合も`Entry.find`しても良いかもしれませんが、パラメータがある以上Relationshipをチェックしたくなるため、わざわざ`@blog.entries.find`していました。URLに冗長性がなくなったため迷うことがなくなります。

### CommentsController

```diff:comments_controller.rb
@@ -1,34 +1,21 @@
 class CommentsController < ApplicationController
-  before_action :set_blog, :set_entry
-  before_action :set_comment, only: :destroy
-
   def create
+    @entry = Entry.find(params[:entry_id])
     @comment = @entry.comments.new(comment_params)
     @comment.save!

-    redirect_to [@blog, @entry]
+    redirect_to @entry
   end

   def destroy
+    @comment = Comment.find(params[:id])
     @comment.destroy!

-    redirect_to [@blog, @entry]
+    redirect_to @comment.entry
   end

   private

-  def set_blog
-    @blog = Blog.find(params[:blog_id])
-  end
-
-  def set_entry
-    @entry = @blog.entries.find(params[:entry_id])
-  end
-
-  def set_comment
-    @comment = @entry.comments.find(params[:id])
-  end
-
   def comment_params
     params.require(:comment).permit(:body)
   end
```

三段全てset_modelしていたのがスッキリしています。

## ビュー
ビューもポリモーフィックURLやURLヘルパーを使っている箇所は全体的に記述が短くなります。大体同じなのでビューは一部だけ示します。

後述するformのpartial viewは、はまりどころかもしれません。

### 記事一覧

```diff:entries/_index.html.erb
@@ -14,9 +14,9 @@
       <tr>
         <td><%= entry.title %></td>
         <td><%= entry.body %></td>
-        <td><%= link_to 'Show', [blog, entry] %></td>
-        <td><%= link_to 'Edit', edit_blog_entry_path(blog, entry) %></td>
-        <td><%= link_to 'Destroy', [blog, entry], method: :delete, data: { confirm: 'Are you sure?' } %></td>
+        <td><%= link_to 'Show', entry %></td>
+        <td><%= link_to 'Edit', edit_entry_path(entry) %></td>
+        <td><%= link_to 'Destroy', entry, method: :delete, data: { confirm: 'Are you sure?' } %></td>
       </tr>
     <% end %>
   </tbody>
```

若干シンプルになっています。

### 記事の登録、編集

`entries/new.html.erb`は変更なしですが、`entries/edit.html.erb`は変更しました。

```erb:entries/new.html.erb
<h1>New Entry</h1>

<%= render 'form', blog: @blog, entry: @entry %>

<%= link_to 'Back', blog_path(@blog) %>
```

```diff:entries/edit.html.erb
@@ -1,6 +1,6 @@
 <h1>Editing Entry</h1>

-<%= render 'form', blog: @blog, entry: @entry %>
+<%= render 'form', blog: nil, entry: @entry %>

-<%= link_to 'Show', [@blog, @entry] %> |
-<%= link_to 'Back', blog_path(@blog) %>
+<%= link_to 'Show', @entry %> |
+<%= link_to 'Back', blog_path(@entry.blog) %>
```


`render 'form'`のところがちょっとトリッキーで、`blog: nil`とする必要があります。

なぜかというと、
`new`と`edit`のformは以下のようにpartial viewにしています。ところが、`shallow: true`すると、createのときには`model: [blog, entry]`、updateのときには`model: entry`としなければURLが生成できず、partial viewが共通化できなくなってしまいそうですが・・・

```erb:entries/_form.html.erb
<%= form_with(model: [blog, entry], local: true) do |form| %>
  <%# 中略 %>
<% end %>
```

実は、form_withの内部で[Array#compact](https://docs.ruby-lang.org/ja/latest/method/Array/i/compact.html)されるので、nil要素は無視されます。そのため、`blog: nil`として渡してあげると、editの場合は`form_with(model: entry, local: true)`と同じ意味になってくれるので、同じpartial viewが使えるということになります。

ちなみに、_form.html.erbでインスタンス変数を使って`model: [@blog, @entry]`とすると、editｓｗ`blog: nil`のようなことをしなくて良くなります。未定義のインスタンス変数はnilになるためですね。

このあたり、stackoverflowの以下の記事を参考にして作っています。
https://stackoverflow.com/questions/9772588/when-using-shallow-routes-different-routes-require-different-form-for-arguments/9944554#9944554

# サンプルアプリのソースコード

shallowなしの`master`ブランチから、`shallow`ブランチを切っています。

https://github.com/youmts/shallow-sample
https://github.com/youmts/shallow-sample/tree/shallow

# 感想

選択肢の一つとして考慮する価値はあるかなと思いました。URLに無駄がなくなるのでうまく使えると美しく記述できそうです。

`shallow: true`を知ったのは下のQiita記事が最初でした。ありがとうございました。
[resources を nest するときは shallow を使うと幸せになれる](https://qiita.com/kuboon/items/96bbd227f9497ed81f38)