【Rails】で、結局 params[:id]って何なん

この投稿は、
[DMM WEBCAMP mentor Advent Calendar 2022](https://qiita.com/advent-calendar/2022/infratop_mentor)
の投稿13日目のエントリーです。

12日目も 私で
[【Rails】gemを使わずに 楽天Apiを動かしてみよう：後編【ハンズオン】](https://qiita.com/tomoaki-kimura/items/eb8faf554f8c398954ac)

## 環境と前提

- ruby 3.1.1
- Rails 6.1.7

## Railsでアプリを作る

### 準備

準備は、 `rails new` をやる準備が整っている前提で進めていきます。

```bash:bash
$ rails _6.1.7_ new routes_test
```

でアプリを作成します。

もし、この時点で、

```
can't find gem railties (= 6.1.7) with executable rails (Gem::GemNotFoundException)
```

といったエラーが出るようでしたら、

```bash:bash
$ gem install rails -v 6.1.7
```

を実行して、該当バージョンのRailsをインストールしておきましょう。

コマンドの結果は以下となります。

```bash:bash
 $ rails new routes_test
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /xxxxxx/xxxxxx/xxxxxxx/xxxxxxx/rakuten_api/.git/
      create  package.json
      create  app
.
.
（省略）
.
.
├─ url-parse@1.5.10
├─ utils-merge@1.0.1
├─ uuid@3.4.0
├─ wbuf@1.7.3
├─ webpack-dev-middleware@3.7.3
├─ webpack-dev-server@3.11.3
├─ websocket-driver@0.7.4
├─ websocket-extensions@0.1.4
└─ ws@6.2.2
✨  Done in 6.53s.
Webpacker successfully installed 🎉 🍰
&
```

プロジェクトに移動します。

```bash:bash
$ cd routes_test
```

ここで、Rails6.1系とruby3.1系との組み合わせで必要となるGemがあります。


```ruby:Gemfile
gem "net-smtp"
gem "net-imap"
gem "net-pop"
```

上記３点を追加して、

```bash:bash
$ bundle install
```
を実行しておきましょう。

これで準備は完了です。

## モデル

今回はモデルが必要なコマンドを使う為、ざっくり `scaffold` で作ります。

```bash:bash
$ rails g scaffold User name
$ rails g scaffold Post title
```

これでMVCを作成し、

```bash:bash
% rails db:migrate
Running via Spring preloader in process 11163
== 20221213073921 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0134s
== 20221213073921 CreateUsers: migrated (0.0136s) =============================

== 20221213073934 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0010s
== 20221213073934 CreatePosts: migrated (0.0010s) =============================
```

マイグレーションを通しておきましょう。

## routes.rb

ルーティングでとりあえず、 `resources` を多発させていませんか？
基本的に、 「書けるのなら `resources`」 を使うのは正解です。実際にRailsもそれを推奨しています。

ただし、 `resources` をネストさせて、

```ruby
  resources :users do
    resources :posts do
      resource :comments
    end
  end
```

こんなコードを大量に作っていませんか？
一見、階層化していて判断しやすいようですが、これはあかんやつです。

[Railsガイド](https://railsguides.jp/routing.html#%E3%83%8D%E3%82%B9%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E5%9B%9E%E6%95%B0%E3%81%AE%E4%B8%8A%E9%99%90)にも

`リソースのネスティングは、ぜひとも1回にとどめて下さい。決して2回以上ネストするべきではありません。`

と書かれています。

これがどのくらい面倒な話になるのかを今から検証してみます。

では、ルーティングを書いてみましょう。すでに `scaffold` で、自動で書かれてしまいますが、無視して・・・・

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :users do
    resources :posts
  end
end
```

このようにネストさせて書き換えてみます。

早速ターミナルで、

```bash
rails routes -c users
```

と打ってみましょう。 `-c` オプションはコントローラー名で絞り込みをします。

```bash:bash
$ rails routes -c users
   Prefix Verb   URI Pattern               Controller#Action
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PATCH  /users/:id(.:format)      users#update
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy
```

ここまでは何の問題もありません。

では、ここに出てきたURLヘルパーからパラメーターを確認したいと思います。

コンソールを起動しましょう。

```irb:rails console
 routes_test (main #%)% rails c                    
Running via Spring preloader in process 11558
Loading development environment (Rails 6.1.7)
irb(main):001:0> app.users_path
=> "/users"
irb(main):002:0> app.new_user_path
=> "/users/new"
irb(main):003:0> app.edit_user_path(1)
=> "/users/1/edit"
irb(main):004:0> app.user_path(1)
=> "/users/1"
```

このような感じで、 URLヘルパーから `URL` を作ることが出来ました。

また、この数字の部分はインスタンスで代用できますので、

```irb:rails console
irb(main):005:0> user = User.create(name: "taro")
   (1.1ms)  SELECT sqlite_version(*)
  TRANSACTION (0.0ms)  begin transaction                               
  User Create (0.8ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "taro"], ["created_at", "2022-12-13 07:48:43.838522"], ["updated_at", "2022-12-13 07:48:43.838522"]]       
  TRANSACTION (0.6ms)  commit transaction                              
=>                                                                     
#<User:0x00000001075ca938                                              
...                                                                    
irb(main):006:0> app.user_path(user)
=> "/users/1"
```

このように書く事も出来ます。

また、ここまでで、 `Link_to` の引数が、実は文字列のURLだという事も分かります。

```erb
<%= link_to "詳細", user_path(@user) %>
```

このコードの `@user` の `id` が `1` だとしたら、 

```erb
<%= link_to "詳細", 'users/1' %>
```

という感じです。

ただし、Railsでは、 `routes.rb` の書き換えでURLを切り換える事が出来ますので、 `View` には、 ヘルパーを使った `~_path`(相対パス) `~_url`(絶対パス) と記述し、もし URLを変更する必要があるとすれば、routes.rb 側で出来る限り対応します。 すると変更があっても直す場所が少なくて済みます。

では、今度はパラメーターの方を見ていきます。

```irb:rails console
Loading development environment (Rails 6.1.7)
irb(main):001:0> Rails.application.routes.recognize_path('users/1')
=> {:controller=>"users", :action=>"show", :id=>"1"}
```

URLからパスのパラメーターが取得できます。

これは、各アクションに渡されるパラメーターとなっていますので、

```ruby
params[:controller]
params[:action]
params[:id]
```

で取得出来ます。

コンソールでは直接 `params` は取得できませんので、ここは仮に

```irb:rails
irb(main):004:0> params = Rails.application.routes.recognize_path(app.user_path(user))
=> {:controller=>"users", :action=>"show", :id=>"1"}
irb(main):005:0> params
=> {:controller=>"users", :action=>"show", :id=>"1"}
```

このように変数に入れてしまいます。
それから、ブラケット記法で、

```irb:rails
irb(main):006:0> params[:controller]
=> "users"
irb(main):007:0> params[:action]
=> "show"
irb(main):008:0> params[:id]
=> "1"
```

取得できます。

このURLをパラメーター化させたものが、いつも使っている `params[:id]` の正体ということです。

### ネストしたルーティング

いよいよ次が本命となりますが、 `posts_controlle`のルーティングは、

```bash:bash
 routes_test (main #%)% rails routes -c posts
        Prefix Verb   URI Pattern                              Controller#Action
    user_posts GET    /users/:user_id/posts(.:format)          posts#index
               POST   /users/:user_id/posts(.:format)          posts#create
 new_user_post GET    /users/:user_id/posts/new(.:format)      posts#new
edit_user_post GET    /users/:user_id/posts/:id/edit(.:format) posts#edit
     user_post GET    /users/:user_id/posts/:id(.:format)      posts#show
               PATCH  /users/:user_id/posts/:id(.:format)      posts#update
               PUT    /users/:user_id/posts/:id(.:format)      posts#update
               DELETE /users/:user_id/posts/:id(.:format)      posts#destroy
```

このようになっております。 `:user_id` `:id` と idの記録が２箇所で行われており、見た目にすでにややこしいです。

早速コンソールで見てみましょう。
まずは、必要データーを作ります。

```irb:rails console
irb(main):001:0> Post.create!(id: 3, title: "hello")
   (0.9ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction                        
  Post Create (1.2ms)  INSERT INTO "posts" ("id", "title", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["id", 3], ["title", "hello"], ["created_at", "2022-12-13 08:10:11.350372"], ["updated_at", "2022-12-13 08:10:11.350372"]]
  TRANSACTION (0.5ms)  commit transaction                       
=>                                                              
#<Post:0x00000001074cf6f0                                       
 id: 3,                                                         
 title: "hello",                                                
 created_at: Tue, 13 Dec 2022 08:10:11.350372000 UTC +00:00,    
 updated_at: Tue, 13 Dec 2022 08:10:11.350372000 UTC +00:00>    
```

一旦 `id: 3` の投稿を作ります。

そして、変数 `user` `post`　を作ります。

```irb:rails console
irb(main):002:0> user = User.first
  User Load (1.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>                                                                     
#<User:0x0000000107febf20                                              
...                                                                    
irb(main):003:0> post = Post.first
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>                                                                     
#<Post:0x0000000108158188                                              
...                     
```

では、先程の要領で、パラメーターを表示させてみましょう。

```irb:rails console
irb(main):004:0> Rails.application.routes.recognize_path(app.user_posts_path(user))
=> {:controller=>"posts", :action=>"index", :user_id=>"1"}
irb(main):005:0> Rails.application.routes.recognize_path(app.new_user_post_path(user))
=> {:controller=>"posts", :action=>"new", :user_id=>"1"}
irb(main):006:0> Rails.application.routes.recognize_path(app.edit_user_post_path(user, post))
=> {:controller=>"posts", :action=>"edit", :user_id=>"1", :id=>"3"}
irb(main):007:0> Rails.application.routes.recognize_path(app.user_post_path(user, post))
=> {:controller=>"posts", :action=>"show", :user_id=>"1", :id=>"3"}
```

ここまで書くと、 `params[:user_id]` は必要ないものがある事に気がつくと思います。

そもそも `show` `edit` `destroy` などは、 そのレコードの `id` があれば特定出来るので、 `:user_id` で特定する必要がないのです。

特にログイン後、 `current_user` が使える場合、 Userに紐付いたレコードの `id` は簡単に入れられます。

そうなると、ルーティングの案として、

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :users 
  resources :posts
end
```

でも十分良いですし、 どうしても `:user_id`が欲しいんじゃーという場合、

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :users do
    resources :posts, only: [:new, :create]
  end
  resources :posts, only: [:show, :edit, :update, :destroy]
end
```

こんなルーティングも考えられます。

```
$ rails routes -c posts
       Prefix Verb   URI Pattern                         Controller#Action
   user_posts POST   /users/:user_id/posts(.:format)     posts#create
new_user_post GET    /users/:user_id/posts/new(.:format) posts#new
    edit_post GET    /posts/:id/edit(.:format)           posts#edit
         post GET    /posts/:id(.:format)                posts#show
              PATCH  /posts/:id(.:format)                posts#update
              PUT    /posts/:id(.:format)                posts#update
              DELETE /posts/:id(.:format)                posts#destroy
```

元々、Railsは DBの `id` を一意のキーとして扱うので、複合的に `id` を管理する必要はあまりないようです。

## 小ネタ

### debug

`<%= debug params %>` というコードを `View` に仕込んでおくと、パラメーターで悩む事が減ります。

ただし、Develop環境のみにしたいので、

```
<%= debug params if Rails.env.development? %>
```

という条件式も入れておき、

```erb:app/views/layouts/applicaion.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>RoutesTest</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
    <%= debug params if Rails.env.development? %> ←この辺
  </body>
</html>
```

に書いておきましょう。すると、開発環境のみで、

![](https://gyazo.com/abd92f130b94801a4f87d1068637aa87.png)

View側でパラメーターの確認が出来ます。

## 結論

`params[:id]` は URLを構成している情報の一つ。
`params` は 次のリクエストに値を持ち越すための仕組みなので、 `params` を知ると世界が平和に。

## 終わりに

Railsやり始めた頃は、あれれ？ルーティングネストしてないとパラメーター取れないよね？みたいな感じで自ら自爆していいた事もあったなぁ・・・。

ここまで読んでいただき、ありがとうございます。