この投稿は、
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があります。
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モデルを確認します。
class Post < ApplicationRecord
belongs_to :user
end
すでに
belongs_to :user
が書き込まれています。
これは、モデル作成時に references
で作ったカラムに対して自動で書き込まれるようになります。
次に Userモデルを確認します。
class User < ApplicationRecord
end
今度は何も書かれていないようです。
has_many
は自動生成されませんので、書き込んでいきましょう。
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.posts
に build
を付けてみて下さい。
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
where
は ActiveRecord_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_CollectionProxy
は has_many
や belongs_to
などのメソッドを使う為のクラスでもあります。
アソシエーションが実現している事
では、結局 has_many
や belongs_to
は何をやっているのか・・・
Userモデル側の
has_many :posts
は、書き換えてみると
def posts
Post.where(user_id: self.id)
end
と書いている事と同義となり、
Postモデル側の記述
belongs_to :user
これも書き換えると、
def user
User.find_by(id: self.user_id)
end
このような感じになります。
結論
has_many
や belongs_to
は 実は特定のインスタンスメソッドを作成しているだけで、 長くなる記述を1行で書けて、モデルもスッキリさせてくれるイケてるメソッドです。
正体は、こんなメソッドです。
has_many
def has_many(name, scope = nil, **options, &extension)
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Reflection.add_reflection self, name, reflection
end
belongs_to
def belongs_to(name, scope = nil, **options)
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
github のリンクから各ソースコードを辿る事が出来ますので、是非覗いてみて下さい。
また、こういったメソッドを検索するには、rails api が便利です。
おわりに
やはり、理解をすすめる上で、コンソールを積極的に扱っていただきたいので、少し読みにくいかも知れませんが、順番に手を動かしていただけるように書いてみました。
次回 3日目の投稿は、また私で恐縮ですが、本記事の続編で多対多のアソシエーションや、 has_many
のオプション等を突っ込んで書いてみます。
【Rails6】コンソールでアソシエーションを理解しよう②(多対多)
スレッドの購読やいいねもお願いします。
ここまで読んでいただきありがとうございます。