【Rails６】コンソールでアソシエーションを理解しよう①（一対多）

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

１日目は @Keichan_15 さんで
[Rails I18n(日本語化)を使用する場合の躓きポイント徹底攻略](https://qiita.com/Keichan_15/items/eae011dccea44abda768)
です。「翻訳ファイルあたらね〜」となる事は多いハズなので、初学者の方には嬉しい記事ですね。

## はじめに

最近DMMでメンターをやらせていただいております。 @tomoaki-kimura です。
RailsはRailsが隠しているコードを理解する事で、ぐっと扱いが上手になります。
今回は、今更のアソシエーションをRailsコンソールを使ってRailsのアソシエーションを理解していきましょう。

## 環境と前提

- ruby 3.1.1
- Rails 6.1.7
- yarn 1.22.18

(今回モデルしか扱わないので、バージョンの違いでそこまでの差分は出ないと考えます。)

前提条件として、 `rails new` の経験者となります。

## 準備

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

```bash
$ rails _6.1.7_ new post_app
```

でアプリを作成します。

もし、この時点で、

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

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

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

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

ただ、今回はモデル操作のみですので、多少バージョンが違っていても構いません。判断が出来る方は、それなりに進めてください。

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

```bash
 $ rails new post_app
      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 /Users/kimuratomoaki/Desktop/my_work/post_app/.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 🎉 🍰
 my_work % cd post_app 
```

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


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

上記３点を追加して、

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

これで準備は完了です。


## モデル作成

### モデル構造

今回一対多のモデルとして、

親モデル・ユーザー（１）
```
User
  - name
```
子モデル・投稿（多）
```
Post
  - user_id
  - message
```

といったモデルを用意します。

### モデル作成

では、親モデルを作りましょう。

```bash
$ rails g model User name
```

を実行します。

```
$ rails g model User name
Running via Spring preloader in process xxxxx
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
```

このようなレスポンスが返ってきて、マイグレーションファイルとモデルが作成されている事が分かります。

続いて子モデルです。

```bash
$ rails g model Post user:references message
```

```
$ rails g model Post user:references message
Running via Spring preloader in process xxxxx
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
```

ここで、上記コマンドの書き方についてですが、モデル作成のコマンドでは、 `string` 型のカラムは省略して書けますので、

```bash
$ rails g model Post user:references message
```

は

```bash
$ rails g model Post user:references message:string
```

の省略した書き方となっています。 また、 `references` 型は `integer` 型としても書けますが、

```bash
$ rails g model Post user_id:integer message:string
```

と、このように `user` ではなく、 `user_id` として書く必要があります。
ただし、最終的に生成されるカラムはどちらも `user_id` となります。

### マイグレーション

では、マイグレーションを実行しておきましょう。

```bash
$ rails db:migrate
```

と実行すると、

```
$ rails db:migrate
Running via Spring preloader in process xxxxx
== xxxxxxxxxxxxxx CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0008s
== xxxxxxxxxxxxxx CreateUsers: migrated (0.0008s) =============================

== xxxxxxxxxxxxxx CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0016s
== xxxxxxxxxxxxxx CreatePosts: migrated (0.0017s) =============================
```

これでOKです。

## アソシエーション

ここからがアソシエーションの設定となります。

### モデル

Postモデルを確認します。

```ruby:app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end
```
すでに

```ruby
belongs_to :user
```

が書き込まれています。
これは、モデル作成時に `references` で作ったカラムに対して自動で書き込まれるようになります。

次に Userモデルを確認します。

```ruby:app/models/user.rb
class User < ApplicationRecord
end
```

今度は何も書かれていないようです。
`has_many` は自動生成されませんので、書き込んでいきましょう。

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end
```

と、 `Post` の複数形で指定しておきましょう。

アソシエーションの設定はここまでです。

## コンソールでの実験

### インスタンス

いよいよ、ここからが本番です。

```bash
$ rails c
```

でコンソールを開きましょう。

```irb
Loading development environment (Rails 6.1.7)
irb(main):001:0> 
```

では、早速

```ruby
User.new
```

と打ってみましょう。

```irb
irb(main):001:0> User.new
=> #<User:0x0000000108070338 id: nil, name: nil, created_at: nil, updated_at: nil>
```

このように戻り値が返ってきます。ここで、注目しておく事は、

```irb
=> #<User: この中身がUserクラス（モデル）のインスタンス >
```

という事で、 `User` モデルに記述されたメソッドが使えるという事です。

また、

```ruby
 id: nil, name: nil, created_at: nil, updated_at: nil
```

この部分がデーター（カラム）となります。

この中で `id` `created_at` `updated_at` は登録時に自動で入力されますので、 `name` だけに値を入力すれば良い事になります。

では、実際にデーターを入力してみましょう。

ここから先は、一旦入力した内容を呼び出しながら進めますので、カーソルの⬆キーを利用して、前のコードを呼び出しながら、足して行きます。

具体的には、 `User.new` を⬆で呼び出して、さらに `(name: "taro")`という引数を入れます。

```irb
irb(main):002:0> User.new(name: "taro")
=> #<User:0x0000000108188608 id: nil, name: "taro", created_at: nil, updated_at: nil>
```

### save

これで値の入ったインスタンスが作成出来ましたので、 `save` を追加して


```irb
irb(main):003:0> User.new(name: "taro").save
  TRANSACTION (0.3ms)  begin transaction
  User Create (1.2ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "taro"], ["created_at", "2022-12-01 17:37:46.597540"], ["updated_at", "2022-12-01 17:37:46.597540"]]      
  TRANSACTION (1.4ms)  commit transaction                                        
=> true        
```

登録が出来ました。戻り値は `true` となっていますね。

Railsのコントローラーではこの戻り値を利用して、

```ruby
if @user.save
else
end
```

のような条件式の書き方をしていました。

### first / last

先程登録したデーターを呼び出す方法ですが、簡単に行うのであれば、 `last` を使います。

```irb
irb(main):004:0> User.last
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=>                                                                               
#<User:0x00000001089b2518                                                        
 id: 1,                                                                          
 name: "taro",                                                                   
 created_at: Thu, 01 Dec 2022 17:37:46.597540000 UTC +00:00,                     
 updated_at: Thu, 01 Dec 2022 17:37:46.597540000 UTC +00:00> 
```

このように書けば、 `find` と違ってレコードが見つからないという事がなく、登録した最後のレコードを取得できます。
SQL上でも `DESC` の結果に `["LIMIT", 1]` とされているのが分かりますね。

逆に登録されたレコードの内一番 `id` が若いものを取得するには `first` を使えば良いでしょう。 

### build

では、次に `last` で見つけた `User` のインスタンスのアソシエーション を使ってみましょう。

```irb
irb(main):005:0> User.last.posts
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
=> []       
```

SQLが親子それぞれのテーブルを`SELECT` した後、戻り値に空の配列 `[]` を見つけています。
これは、
「id: 1 の Userが 投稿した Post はありませんよ。」
という事になります。

続いて、アソシエーションで関連付けられたモデル `Post` を作成しますが、通常であれば、

```irb
irb(main):006:0> Post.new
=> #<Post:0x0000000108c419c8 id: nil, user_id: nil, message: nil, created_at: nil, updated_at: nil>
```

このようにインスタンスを作成し、

```irb
irb(main):007:0> Post.new(user_id: User.last.id, message: "hello")
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Post:0x0000000107522120 id: nil, user_id: 1, message: "hello", created_at: nil, updated_at: nil> 
```

値を入力するには、 `user_id` `message` に対して値を入れます。

しかし、アソシエーションを用いるともっと簡単に分かりやすく記述が可能です。

`User.last.posts` に `build` を付けてみて下さい。

```irb
irb(main):008:0> User.last.posts.build
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Post:0x0000000107553158 id: nil, user_id: 1, message: nil, created_at: nil, updated_at: nil>  
```

このように、 `user_id: 1` がすでに入った状態でインスタンスが生成されています。

ちなみに、 

```
User.last.new
``` 

のようにでも代用出来ますが、英文的には、

```
User.last.build
```

のほうがスッキリしますね。（ 逆に `Post.build` はエラーとなり、使えません.
  ）

残りは `message` カラムに内容を入れたいので、

```irb
irb(main):009:0> User.last.posts.build(message: "hello")
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Post:0x0000000107dffc20 id: nil, user_id: 1, message: "hello", created_at: nil, updated_at: nil> 
```

このように引数を渡せば、先程の

```ruby
Post.new(user_id: User.last.id, message: "hello")
```

と同じ結果を生むことが出来ます。

では、`save` してみましょう。

```irb
irb(main):010:0> User.last.posts.build(message: "hello").save
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction                                                              
  Post Create (1.1ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 1], ["message", "hello"], ["created_at", "2022-12-01 18:14:49.627136"], ["updated_at", "2022-12-01 18:14:49.627136"]]
  TRANSACTION (1.1ms)  commit transaction                                                             
=> true       
```

### create

では、もう一人ユーザーを作ってみましょう。
今度は `create` で作ってみます。

```irb
irb(main):011:0> User.create(name: "jiro")
  TRANSACTION (0.2ms)  begin transaction
  User Create (0.9ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "jiro"], ["created_at", "2022-12-01 18:20:44.926812"], ["updated_at", "2022-12-01 18:20:44.926812"]]          
  TRANSACTION (1.0ms)  commit transaction                                            
=>                                                                                   
#<User:0x0000000109476e18                                                        
 id: 2,                                                                          
 name: "jiro",                                                                   
 created_at: Thu, 01 Dec 2022 18:20:44.926812000 UTC +00:00,                     
 updated_at: Thu, 01 Dec 2022 18:20:44.926812000 UTC +00:00>    
```

今度は戻り値は `true` ではなく オブジェクト自身となっています。

この状態で、

```ruby
User.last
```

を使ってみましょう。

```irb
irb(main):012:0> User.last
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=>                                                                                        
#<User:0x0000000109607c78                                                                 
 id: 2,                                                                                   
 name: "jiro",                                                                            
 created_at: Thu, 01 Dec 2022 18:20:44.926812000 UTC +00:00,                              
 updated_at: Thu, 01 Dec 2022 18:20:44.926812000 UTC +00:00>   
```

`last` で 今登録した id: 2 のUserが取り出せていますね。

では、このユーザーにも投稿して貰いましょう。
今度は５件投稿します。

`(1..5)` でrenge に対して `each` でループを回し、 `hello1` `hello2` `hello3` `hello4` `hello5` というメッセージを作成します。

```irb
irb(main):013:0> (1..5).each { |n| User.last.posts.create(message: "hello#{n}") }
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction                                                                    
  Post Create (0.6ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 2], ["message", "hello1"], ["created_at", "2022-12-01 18:29:02.889050"], ["updated_at", "2022-12-01 18:29:02.889050"]]
  TRANSACTION (0.9ms)  commit transaction                                                                   
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]       
  TRANSACTION (0.0ms)  begin transaction                                                                    
  Post Create (0.5ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 2], ["message", "hello2"], ["created_at", "2022-12-01 18:29:02.892872"], ["updated_at", "2022-12-01 18:29:02.892872"]]
  TRANSACTION (0.8ms)  commit transaction                                                                   
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]       
  TRANSACTION (0.0ms)  begin transaction                                                                    
  Post Create (0.3ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 2], ["message", "hello3"], ["created_at", "2022-12-01 18:29:02.896376"], ["updated_at", "2022-12-01 18:29:02.896376"]]
  TRANSACTION (0.4ms)  commit transaction                                                                   
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.0ms)  begin transaction
  Post Create (2.1ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 2], ["message", "hello4"], ["created_at", "2022-12-01 18:29:02.898502"], ["updated_at", "2022-12-01 18:29:02.898502"]]
  TRANSACTION (0.5ms)  commit transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.0ms)  begin transaction
  Post Create (0.3ms)  INSERT INTO "posts" ("user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 2], ["message", "hello5"], ["created_at", "2022-12-01 18:29:02.902544"], ["updated_at", "2022-12-01 18:29:02.902544"]]
  TRANSACTION (0.4ms)  commit transaction
=> 1..5
```

`each{}` の `{}` は `do ~ end` で書いた記述と同義ですので、

```ruby
(1..5).each do |n|
  User.last.posts.create(message: "hello#{n}")
end
```

このように記述することも出来ます。複数行に渡る場合は、 `do ~ end` を使いましょう。


## アソシエーション

これで概ねデーターが用意できましたので、データーを見ていきましょう。

```irb
irb(main):014:0> Post.all
  Post Load (0.3ms)  SELECT "posts".* FROM "posts"
=>                                                              
[#<Post:0x0000000107fd2cf0                                      
  id: 1,                                                        
  user_id: 1,                                                   
  message: "hello",                                             
  created_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00,   
  updated_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00>,  
 #<Post:0x0000000107fd2bd8                                      
  id: 2,                                                        
  user_id: 2,                                                   
  message: "hello1",                                            
  created_at: Thu, 01 Dec 2022 18:29:02.889050000 UTC +00:00,   
  updated_at: Thu, 01 Dec 2022 18:29:02.889050000 UTC +00:00>,  
 #<Post:0x0000000107fd2b10                                      
  id: 3,
  user_id: 2,
  message: "hello2",
  created_at: Thu, 01 Dec 2022 18:29:02.892872000 UTC +00:00,
  updated_at: Thu, 01 Dec 2022 18:29:02.892872000 UTC +00:00>,
 #<Post:0x0000000107fd2a48
  id: 4,
  user_id: 2,
  message: "hello3",
  created_at: Thu, 01 Dec 2022 18:29:02.896376000 UTC +00:00,
  updated_at: Thu, 01 Dec 2022 18:29:02.896376000 UTC +00:00>,
 #<Post:0x0000000107fd2980
  id: 5,
  user_id: 2,
  message: "hello4",
  created_at: Thu, 01 Dec 2022 18:29:02.898502000 UTC +00:00,
  updated_at: Thu, 01 Dec 2022 18:29:02.898502000 UTC +00:00>,
 #<Post:0x0000000107fd28b8
  id: 6,
  user_id: 2,
  message: "hello5",
  created_at: Thu, 01 Dec 2022 18:29:02.902544000 UTC +00:00,
  updated_at: Thu, 01 Dec 2022 18:29:02.902544000 UTC +00:00>]
```

現在 `Post` のデーターが６件存在します。

これをまず、 `where` を使って呼び出ししてみましょう。

```irb
irb(main):015:0> Post.where(user_id: User.first.id)
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]         
=>                                                                                                       
[#<Post:0x0000000109677af0                                                                               
  id: 1,                                                                                                 
  user_id: 1,                                                                                            
  message: "hello",                                                                                      
  created_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00,                                            
  updated_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00>]  
```

配列 `[]` の中にPostモデルのインスタンスが入っています。

アソシエーションも使ってみましょう。実は先程も一度やってはいますが、

```irb
irb(main):016:0> User.first.posts
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]         
=>                                                                                                       
[#<Post:0x00000001098fec60                                                                               
  id: 1,                                                                                                 
  user_id: 1,                                                                        
  message: "hello",                                                                  
  created_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00,                        
  updated_at: Thu, 01 Dec 2022 18:14:49.627136000 UTC +00:00>]          
```

このように同じ結果となります。

当然、

```ruby
User.last.posts
```

を実行すれば、後で入れた５件のレコードが取り出せます。（是非やってみて下さい。）

しかし、これらの配列に見えるクラスはそれぞれ別物となります。

```irb
irb(main):017:0> Post.where(user_id: User.first.id).class
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> Post::ActiveRecord_Relation                                         
```

`where` は `ActiveRecord_Relation` クラス

```irb
irb(main):018:0> User.first.posts.class
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> Post::ActiveRecord_Associations_CollectionProxy     
```

`has_many` で定義されたメソッドを使った場合、 `ActiveRecord_Associations_CollectionProxy` クラスのオブジェクトとなります。

ただ、これらは大きく変わるものではありませんので、 `ActiveRecord_Associations_CollectionProxy` は `has_many` や `belongs_to` などのメソッドを使う為のクラスでもあります。

## アソシエーションが実現している事

では、結局 `has_many` や `belongs_to` は何をやっているのか・・・                     
Userモデル側の

```ruby:app/models/user.rb
has_many :posts
```
は、書き換えてみると

```ruby
def posts
  Post.where(user_id: self.id)
end
```

と書いている事と同義となり、

Postモデル側の記述

```ruby:app/models/post.rb
belongs_to :user
```

これも書き換えると、

```ruby
def user
  User.find_by(id: self.user_id)
end
```

このような感じになります。

## 結論

`has_many` や `belongs_to` は 実は特定のインスタンスメソッドを作成しているだけで、 長くなる記述を１行で書けて、モデルもスッキリさせてくれるイケてるメソッドです。

正体は、こんなメソッドです。

### has_many

```ruby:rails/activerecord/lib/active_record/associations.rb, line 1466
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end
```

[Github](https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activerecord/lib/active_record/associations.rb#L1466)

### belongs_to

```ruby:rails/activerecord/lib/active_record/associations.rb, line 1790
def belongs_to(name, scope = nil, **options)
  reflection = Builder::BelongsTo.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end
```

[Github](https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activerecord/lib/active_record/associations.rb#L1790)

github のリンクから各ソースコードを辿る事が出来ますので、是非覗いてみて下さい。

また、こういったメソッドを検索するには、[rails api](https://api.rubyonrails.org/) が便利です。

## おわりに

やはり、理解をすすめる上で、コンソールを積極的に扱っていただきたいので、少し読みにくいかも知れませんが、順番に手を動かしていただけるように書いてみました。

次回 ３日目の投稿は、また私で恐縮ですが、本記事の続編で多対多のアソシエーションや、 `has_many` のオプション等を突っ込んで書いてみます。

[【Rails６】コンソールでアソシエーションを理解しよう②（多対多）](https://qiita.com/tomoaki-kimura/items/e6d8df24f978c6039098)

スレッドの購読やいいねもお願いします。

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