大家好!
昨天研究到在兩個Model 間加上has_many、belongs_to 等,即可利用Rails 黑魔法為彼此加上associations,亦即為彼此加上了幾個方法:
irb(main):022:0> methods_after.flatten - methods_before.flatten
[
    [0] "autosave_associated_records_for_podcast",
    [1] "build_podcast",
    [2] "create_podcast",
    [3] "create_podcast!",
    [4] "podcast",
    [5] "podcast=",
    [6] "reload_podcast"
]
那被翻爛的書今天想研究的是為Model 加上對應的PK & FK 欄位,但沒加上Model associations 會怎麼樣,讓我們來實驗看看吧:
# 首先檢視目前的podcasts & episodes table 各有哪些欄位:
  create_table "podcasts", force: :cascade do |t|
    t.string "name"
    t.integer "genres"
    t.string "host"
    t.text "introduction"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
 
    create_table "episodes", force: :cascade do |t|
    t.string "name"
    t.integer "duration"
    t.datetime "record_on"
    t.string "host"
    t.string "guest"
    t.text "introduction"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
並根據Model 關聯圖增加migration 檔:
❯ rails g migration add_podcast_id_to_episodes
Running via Spring preloader in process 64826
      invoke  active_record
      create    db/migrate/20220921141701_add_podcast_id_to_episodes.rb
      
# 修改migration
class AddPodcastIdToEpisodes < ActiveRecord::Migration[6.0]
  def change
    add_reference :episodes, :podcast
  end
end
# 能看到多了一個podcast_id 的欄位在episodes 資料表上
create_table "episodes", force: :cascade do |t|
  t.bigint "podcast_id"
  t.index ["podcast_id"], name: "index_episodes_on_podcast_id"
end
接著我們來修改episode 的seeding tasks:
namespace :seeding do desc "Seeding episodes"
  task seed_episodes: :environment do
    Episode.destroy_all
    0.upto(19) do |no|
      Episode.create!([{
        name: "第#{no}話",
        duration: 60,
        record_on: DateTime.now + (no * 7).days,
        host: "COLATIME",
        introduction: "開播啦~~",
         podcast_id: Podcast.pluck(:id).sample #新增這行
      }])
      p "Created #{Episode.count} episodes"
    end
  end
end
❯ rails seeding:seed_episodes
seeding 了20 筆的episodes 後,我們進console 來看一下:
# 可看到是有資料的
irb(main):003:0> Episode.last
  Episode Load (0.6ms)  SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT $1  [["LIMIT", 1]]
#<Episode:0x00007fe52618c078> {
              :id => 60,
            :name => "第19話",
        :duration => 60,
       :record_on => Wed, 01 Feb 2023 14:25:48 UTC +00:00,
            :host => "COLATIME",
           :guest => nil,
    :introduction => "開播啦~~",
      :created_at => Wed, 21 Sep 2022 14:25:48 UTC +00:00,
      :updated_at => Wed, 21 Sep 2022 14:25:48 UTC +00:00,
      :podcast_id => 282
}
# 但我們能隨意指派一個不存在的podcast_id 嗎?我們來試試看 XD
irb(main):004:0> Episode.last.update(podcast_id: 1)
  Episode Load (0.7ms)  SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Episode Update (0.8ms)  UPDATE "episodes" SET "podcast_id" = $1, "updated_at" = $2 WHERE "episodes"."id" = $3  [["podcast_id", 1], ["updated_at", "2022-09-21 14:32:51.324768"], ["id", 80]]
   (1.1ms)  COMMIT
true # => 居然可以!XD
# 那讓我們試著叫出Episode.last 所屬的Podcast
irb(main):005:0> Episode.last.podcast
  Episode Load (0.5ms)  SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT $1  [["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):33
NoMethodError (undefined method `podcast' for #<Episode:0x00007fe524d8a2a0>)
Did you mean?  podcast_id # => Oh, no!
顯然,雖然Episode 有寫入podcast_id,但因為沒在Model 加上belongs_to :podcast的關聯,因此無法藉由關聯方法叫出他所屬的Podcast。
但我還想這麼嘗試:
# 使用ActiveRecord 的joins:
irb(main):001:0> Episode.joins(:podcast)
Traceback (most recent call last):
ActiveRecord::ConfigurationError (Can't join 'Episode' to association named 'podcast'; perhaps you misspelled it?) # => 會被告知沒有關聯
# 但若是混用SQL 語法在內:
irb(main):002:0> Episode.joins("LEFT JOIN podcasts ON episodes.podcast_id = podcasts.id").first
  Episode Load (1.2ms)  SELECT "episodes".* FROM "episodes" LEFT JOIN podcasts ON episodes.podcast_id = podcasts.id ORDER BY "episodes"."id" ASC LIMIT $1  [["LIMIT", 1]]
#<Episode:0x00007fe51e09ea88> {
              :id => 81,
            :name => "第0話",
        :duration => 60,
       :record_on => Wed, 21 Sep 2022 14:46:26 UTC +00:00,
            :host => "COLATIME",
           :guest => nil,
    :introduction => "開播啦~~",
      :created_at => Wed, 21 Sep 2022 14:46:26 UTC +00:00,
      :updated_at => Wed, 21 Sep 2022 14:46:26 UTC +00:00,
      :podcast_id => 289
}
# 卻能看到兩個表被join 了起來,也能叫得出資料!
那我想再做個嘗試:
rollback migration,但這次不用add_reference 的方法,而是自己加上podcast_id 的欄位在Episode 上:
# 再次修改migration
class AddPodcastIdToEpisodes < ActiveRecord::Migration[6.0]
  def change
    # add_reference :episodes, :podcast
    add_column :episodes, :podcast_id, :integer
  end
end
# 可看到目前的schema 跟剛剛有些微不同:
create_table "episodes", force: :cascade do |t|
  t.integer "podcast_id"
end
# 那我們再跑一次seeding episodes 的tasks、進console 確認資料、再試試看上面的query:
irb(main):003:0> Episode.joins("LEFT JOIN podcasts ON episodes.podcast_id = podcasts.id").first
  Episode Load (2.1ms)  SELECT "episodes".* FROM "episodes" LEFT JOIN podcasts ON episodes.podcast_id = podcasts.id ORDER BY "episodes"."id" ASC LIMIT $1  [["LIMIT", 1]]
#<Episode:0x00007fe51fc62b20> {
              :id => 121,
                # 略
              :podcast_id => 281
}
有看出什麼端倪嗎?
回到昨天的第二、第三題:在資料表要加上PK、FK,那麼加了PK、FK 就算加上關聯了嗎?
...
補充:
若Episode 有加上belongs_to :podcast,此時還能隨意指派一個不存在的podcast_id 嗎?
irb(main):005:0> Episode.last.update(podcast_id: 1)
  Episode Load (0.6ms)  SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Podcast Load (0.4ms)  SELECT "podcasts".* FROM "podcasts" WHERE "podcasts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
false # => 但為什麼會false 呢?
# 大概是黑魔法幫我們加上了驗證吧:XD
ActiveRecord::RecordInvalid: Validation failed: Podcast must exist
...
今天的研究先到這,明天再繼續研究;謝謝大家~