19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DMM WEBCAMP mentor Advent Calendar 2022

Day 2

【Rails6】コンソールでアソシエーションを理解しよう①(一対多)

Last updated at Posted at 2022-12-01

この投稿は、
DMM WEBCAMP mentor Advent Calendar 2022
の投稿2日目のエントリーです。

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

はじめに

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

環境と前提

  • ruby 3.1.1
  • Rails 6.1.7
  • yarn 1.22.18

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

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

準備

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

$ rails _6.1.7_ new post_app

でアプリを作成します。

もし、この時点で、

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

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

$ gem install rails -v 6.1.7

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

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

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

 $ 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があります。

Gemfile
gem "net-smtp"
gem "net-imap"
gem "net-pop"

上記3点を追加して、

$ bundle install

を実行しておきましょう。

これで準備は完了です。

モデル作成

モデル構造

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

親モデル・ユーザー(1)

User
  - name

子モデル・投稿(多)

Post
  - user_id
  - message

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

モデル作成

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

$ 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

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

続いて子モデルです。

$ 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 型のカラムは省略して書けますので、

$ rails g model Post user:references message

$ rails g model Post user:references message:string

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

$ rails g model Post user_id:integer message:string

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

マイグレーション

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

$ 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モデルを確認します。

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

すでに

belongs_to :user

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

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

app/models/user.rb
class User < ApplicationRecord
end

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

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

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

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

コンソールでの実験

インスタンス

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

$ rails c

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

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

では、早速

User.new

と打ってみましょう。

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

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

=> #<User: この中身がUserクラス(モデル)のインスタンス >

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

また、

 id: nil, name: nil, created_at: nil, updated_at: nil

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

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

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

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

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

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

save

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

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のコントローラーではこの戻り値を利用して、

if @user.save
else
end

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

first / last

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

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(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(main):006:0> Post.new
=> #<Post:0x0000000108c419c8 id: nil, user_id: nil, message: nil, created_at: nil, updated_at: nil>

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

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.postsbuild を付けてみて下さい。

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(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> 

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

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

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

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

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(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 ではなく オブジェクト自身となっています。

この状態で、

User.last

を使ってみましょう。

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が取り出せていますね。

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

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

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 で書いた記述と同義ですので、

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

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

アソシエーション

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

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 のデーターが6件存在します。

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

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(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>]          

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

当然、

User.last.posts

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

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

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                                         

whereActiveRecord_Relation クラス

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_CollectionProxyhas_manybelongs_to などのメソッドを使う為のクラスでもあります。

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

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

app/models/user.rb
has_many :posts

は、書き換えてみると

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

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

Postモデル側の記述

app/models/post.rb
belongs_to :user

これも書き換えると、

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

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

結論

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

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

has_many

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

belongs_to

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

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

また、こういったメソッドを検索するには、rails api が便利です。

おわりに

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

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

【Rails6】コンソールでアソシエーションを理解しよう②(多対多)

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

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

19
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?