大家好!
昨天研究到在兩個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
...
今天的研究先到這,明天再繼續研究;謝謝大家~