巨人の足元でたじlog

そうして言葉を軽んじるから――― 君は私の言葉を聞き逃す

railsのbelongs_toとhas_manyとreferencesの使い方について整理する

ややこしいので、一気に整理する。

空のrailsプロジェクトを立ち上げて、

$ be rails g model user name:text

※ alias be='bundle exec'

userテーブルができました。

$ be rails g model tweet content:text references:users

tweetテーブルができました。

こいつらモデルのクラスを確認してみると、 app/models/user.rb

class User < ApplicationRecord
end

app/models/tweet.rb

class Tweet < ApplicationRecord
end

dbマイグレートする

$ be rake db:migrate
== 20180717181025 CreateTweets: migrating =====================================
-- create_table(:tweets)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

undefined method `user' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007ff17cfc1488>
.
,

と、エラーになる。

これは、もしかして順番がよろしくなかったか?

もう一度やり直す。

$ be rails g model user name:text
$ be rake db:migrate
$ be rails g model tweet content:text references:user
$ be rake db:migrate
== 20180717181557 CreateTweets: migrating =====================================
-- create_table(:tweets)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

undefined method `user' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007fc20b59a978>
.
.
.

順番なんとか関係なかったです、すみません。
たぶんuserを複数形にしたら行ける気がする。
そのために、一度2つ目のmigrationファイルはなかったことにする。

$ be rails destroy model tweet
$ be rails g model tweet content:text references:users
$ be rake db:migrate
.
.
undefined method `users' for #<ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition:0x00007f8ab5104ea0>

うーむ。先にreferencesを使わない方法を実装して確認する。

もう一度もとに戻します。

$ be rails destroy model tweet

シンプルなやつ

$ be rails g model tweet content:text user_id:integer
$ be rake db:migrate

成功。
しかしこれでは、ただ単にuser_idというたまたまuserテーブルと関係ありそうな名前のカラムがあるだけで、実際には無関係です。
2つのファイルを次のように変更 app/models/user.rb

class User < ApplicationRecord
  has_many :tweets
end

app/models/tweet.rb

class Tweet < ApplicationRecord
  belongs_to :user
end

そして、rails consoleにて試してみる。
まずは一通り。

be rails c -s
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0> u = User.create(name:"hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-17 18:34:37.448049"], ["updated_at", "2018-07-17 18:34:37.448049"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">
irb(main):002:0>
irb(main):003:0>
irb(main):004:0>
irb(main):005:0>
irb(main):006:0> t = Tweet.create(content:"aaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:22.904090"], ["updated_at", "2018-07-17 18:35:22.904090"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">
irb(main):007:0> t2 = Tweet.create(content:"bbbbbaaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "bbbbbaaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:29.800640"], ["updated_at", "2018-07-17 18:35:29.800640"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">
irb(main):008:0>
irb(main):009:0>
irb(main):010:0>
irb(main):011:0>
irb(main):012:0>
irb(main):013:0>
irb(main):014:0> u.tweets
  Tweet Load (0.2ms)  SELECT  "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">, #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">]>
irb(main):015:0>
irb(main):016:0>
irb(main):017:0>
irb(main):018:0>
irb(main):019:0> t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

sqlをちょっと読んでみる。 まずは一つ目。Userの作成。

u = User.create(name:"hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-17 18:34:37.448049"], ["updated_at", "2018-07-17 18:34:37.448049"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

特に難しいことはしていない。ここは至ってシンプル。
ただ、(?, ?, ?)とかの部分はなんかエスケープしてるんだろうくらいの雰囲気で流している。

続いてTweetの作成。

t = Tweet.create(content:"aaaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.2ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaaa"], ["user_id", 1], ["created_at", "2018-07-17 18:35:22.904090"], ["updated_at", "2018-07-17 18:35:22.904090"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">

なるほど、一度Userの情報を探している。
ちなみに、createの際にuser_idを指定しなかったり、usersテーブルのidに存在しない値を指定すると、失敗し、rollbackする。

userに紐づくtweetを取得する

u.tweets
  Tweet Load (0.2ms)  SELECT  "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Tweet id: 1, content: "aaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:22", updated_at: "2018-07-17 18:35:22">, #<Tweet id: 2, content: "bbbbbaaaaaaaa", user_id: 1, created_at: "2018-07-17 18:35:29", updated_at: "2018-07-17 18:35:29">]>

なるほど、かってにuserテーブルのidをtweetテーブルのuser_idに変換してwhereの条件をかけているわけか。

最後に、Tweetに紐付いたuserの情報

t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-17 18:34:37", updated_at: "2018-07-17 18:34:37">

はっ!? SQLは発行されないのか!! これはー多分最初にtを作成したときにuser_idを指定していて、そのときにuserの情報がloadされていたので、それを利用しているぽい。

なるほどね、SQLの中身がわかってちゃんと落ち着いて読めばそんなに対してことやってないってことがわかった。
ただし、userテーブルのidをtweetテーブルのuser_idに変換するとか、名前で制約をつけていることが多くて、そこ離れるまでは大変そうだと思ったよ。

referenceを理解する

これと同じことをreferencesを使ってやってみる。
ちょっと改めて調べたところ、さっき自分がやっていたことはとんでもないウンコマンなことだった。

改めて空のrailsプロジェクトから

$ be rails g model user name:text
$ be rails g model tweet content:text user:references
$ be rake db:migrate

userという名前の型がreferencesというわけだ。
先程までのエラーは、referencesというカラムをuserという型で定義しようとしていたために発生したエラーだったのだった。。。ゴミすぎる。笑
モデルのクラスのファイルを見てみる。

app/models/user.rb

class User < ApplicationRecord
end

app/models/tweet.rb

class Tweet < ApplicationRecord
  belongs_to :user
end

おっと、このままの状態だと、Userクラスに何も書いてないが、果たしてこれで動くのか?
rails consoleで確認。

be rails c -s
Loading development environment in sandbox (Rails 5.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0> u = User.create(name: "hoge")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "hoge"], ["created_at", "2018-07-18 14:01:54.105305"], ["updated_at", "2018-07-18 14:01:54.105305"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):002:0>
irb(main):003:0>
irb(main):004:0>
irb(main):005:0>
irb(main):006:0> t = Tweet.create(content: "aaaaaaa", user_id:1)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Tweet Create (0.3ms)  INSERT INTO "tweets" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "aaaaaaa"], ["user_id", 1], ["created_at", "2018-07-18 14:02:19.665391"], ["updated_at", "2018-07-18 14:02:19.665391"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Tweet id: 1, content: "aaaaaaa", user_id: 1, created_at: "2018-07-18 14:02:19", updated_at: "2018-07-18 14:02:19">
irb(main):007:0>
irb(main):008:0>
irb(main):009:0>
irb(main):010:0>
irb(main):011:0> u
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):012:0> u.tweets
Traceback (most recent call last):
        1: from (irb):12
NoMethodError (undefined method `tweets' for #<User:0x00007f9dc8987f48>)
irb(main):013:0>
irb(main):014:0>
irb(main):015:0>
irb(main):016:0>
irb(main):017:0>
irb(main):018:0> t.user
=> #<User id: 1, name: "hoge", created_at: "2018-07-18 14:01:54", updated_at: "2018-07-18 14:01:54">
irb(main):019:0>

userからtweetを取得するのができない。
やはりuser has_many tweetと定義していないからだ。

しかし、tweet belongs_to userなので、tweetからuserを取得することはできる。

なので、相互からアクセスしたいときには、
app/models/user.rb

class User < ApplicationRecord
  has_many :tweets
end

と、has_manyを追加する必要がある。

ところで、型をreferencesとして定義するメリットは何なんだろう?
確かに、belongs_toは自動で書いてくれたけど、結局has_manyを自分で書かないといけないんだったら、そんなに旨味はないように感じるのだが。。。

外部キーをreferences型カラムで保存する
によると、references型はbelongs_toと同じ意味だと。
references型でテーブル定義をすると、hoge_idというように"_id"をつけなくてもいいらしい。
しかし、これがメリットなのか?

【Rails入門】has_many、belongs_toの使い方まとめ | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

また、referencesを使った場合はapp/model/castle.rb にbelongs_to :ownerが自動で追加されます。 ですので、使えるなら、いちいち手動で設定するよりもこちらのコマンドを使うよにしてください。

この記事のニュアンスだと、別にreferences使わないとだめってことはないみたい。
そうか、別にreferencesじゃなくてもいいのか、ただreferences使ったほうがちょっと便利でラクできるよ!ってことらしい。

そういう理解にしておいて先に進もう。
と、最後にdb/schema.rbだけは確認しておきたい。 referencesを使ってmigrationファイルを作成した場合。

ActiveRecord::Schema.define(version: 2018_07_17_194545) do

  create_table "tweets", force: :cascade do |t|
    t.text "content"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_tweets_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.text "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

なるほど、tweetテーブル側でuser_idにindex的なものがあるなぁ。
しまった、sqlのindexについての知識が乏しい。。。
なんか早くなるっぽいくらいの認識しかない。辛み。

一旦保留にして、referencesじゃなくて、それぞれ別のものとしてuserテーブルとtweetテーブルを作ったものを見てみる。

be rails g model user name:text
be rails g model tweet content:text user_id:integer
be rake db:migrate

db/schema.rb

ActiveRecord::Schema.define(version: 2018_07_18_142813) do

  create_table "tweets", force: :cascade do |t|
    t.text "content"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.text "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

うーむ、tweetsテーブルのuser_idはusersテーブルとは無関係のように見えるな。
これは適当だがreferencesを使ったほうがindex的なものを使うからテーブル感のリレーションを使いたいときには高速。的なことがありそうな気がしてきた。がわからない。
そんなゴリゴリにsqlチューニングする必要ある勢じゃないから、いいや、気にしない。タイムカプセルに入れておこう。

と、やはり気になってteratailにアカウント作って質問の文章を書いている最中にダメ押しでもう少し調べてみたら、多分理解した。

Ruby on Rails - railsのindexとforeign_keyについておしえてください。(63406)|teratail

やっぱり普通にindexを使っているんですね。
そしてindexも概念は理解しました。
sqlのindexの説明で「複製」とかっていう単語を使っていたので、腑に落ちていなかったが、どうやらこういうことらしい。

indexについて理解があいまいなのですが、 --------+--------+---------+

| id | name | shoku |

|1 | taro | ramen |

| 2 | ken | somen |

こんなデータがあったとして、nameフィールドにindexをつけたとすると、よみだすときこのデータ全体を読み込むのではなく、nameフィールドだけを絞って読み込むということですか? だとしたら find_by があるのでindexが不要だと思ったのですが。。。

という質問に対して

たとえば、ユーザーのデータが数百万件を超えるような膨大な数になったとしましょう。 その中から、「メールアドレスがhoge@example.comのデータを探す」となれば、インデックスがない場合だと「全部のデータを当たって、一致するメールアドレスを探す」という処理が必要になります。一方、インデックスでは「B木」のような完全に同一のデータ、あるいはデータ範囲を検索しやすいような特殊な構造でデータを入れてあるので、メールアドレスが一致するデータを、全探索より遥かに高速に引き出すことができます。

Ruby on Rails - railsのindexとforeign_keyについておしえてください。(63406)|teratail

なるほど、普通に複製するだけじゃなくて、B木とかの検索しやすいデータ構造にしておくのか。
いやーCS力足りないなぁ。。。
ということは、結局テーブル同士のリレーションは結構な数の呼び出しが起こりそうな気がしているので、indexを貼ってくれているrefernces型を使うのがbetterという結論でよろしいですかね。