ActiveRecord 資料表關係

本文深入探讨ActiveRecord中如何利用主键与外部键实现资料表之间的关联,并通过示例展示一对一、一对多、多对多等不同类型的关联关系及其操作方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ActiveRecord可以用Associations來定義資料表之間的關聯性,這是最被大家眼睛一亮ORM功能。到目前為止我們學會了用ActiveRecord來操作資料庫,但是還沒充分發揮關聯式資料庫的特性,那就是透過primary keyforeign keys將資料表互相關連起來。

Primary Key主鍵是一張資料表可以用來唯一識別的欄位,而Foreign Key外部鍵則是用來指向別張資料表的Primary Key,如此便可以產生資料表之間的關聯關係。了解如何設計正規化關聯式資料庫請參考附錄基礎。

Primary Key這個欄位在Rails中,照慣例叫做id,型別是整數且遞增。而Foreign Key欄位照慣例會叫做{model_name}_id,型別是整數。

一對一關聯one-to-one

has_one diagram

延續Part1Event Model範例,假設一個Event擁有一個Location。來新增一個Location Model,其中的event_id就是外部鍵欄位:

rails g model location name:string event_id:integer

執行bundle exec rake db:migrate產生locations資料表。

分別編輯app/models/event.rbapp/models/location.rb

class Event < ActiveRecord::Base
    has_one :location # 單數
    #...
end

class Location < ActiveRecord::Base
    belongs_to :event # 單數
end

belongs_tohas_one這兩個方法,會分別動態新增一些方法到LocationEvent Model上,讓我們進入rails console實際操作資料庫看看,透過Associations你會發現操作關聯的物件非常直覺:

範例一,建立Location物件並關聯到Event
e = Event.first
l = Location.new( :name => 'Hsinchu', :event => e ) 
# 等同於 l = Location.new( :name => 'Hsinchu', :event_id => e.id )
l.save
e.location
l.event

Event.first會撈出events table的第一筆資料,如果你第一筆還在,那就會是Event.find(1)。同理,Event.last會撈出最後一筆。

範例二,從Event物件中建立一個Location
e = Event.first
l = e.build_location( :name => 'Hsinchu' )
l.save
e.location
l.event
範例三,直接從Event物件中建立一個Location
e = Event.first
l = e.create_location( :name => 'Hsinchu' )
e.location
l.event

一對多關聯one-to-many

has_one diagram

一對多關聯算是最常用的,例如一個Event擁有很多Attendee,來新增Attendee Model

rails g model attendee name:string event_id:integer

執行bundle exec rake db:migrate產生attendees資料表。

分別編輯app/models/event.rbapp/models/attendee.rb

class Event < ActiveRecord::Base
    has_many :attendees # 複數
    #...
end

class Attendee < ActiveRecord::Base
    belongs_to :event # 單數
end

同樣地,belongs_tohas_many這兩個方法,會分別動態新增一些方法到AttendeeEvent Model上,讓我們進入rails console實際操作資料庫看看:

範例一,建立Attendee物件並關聯到Event:
e = Event.first
a = Attendee.new( :name => 'ihower', :event => e ) 
# 或 a = Attendee.new( :name => 'ihower', :event_id => e.id )
a.save
e.attendees # 這是陣列
e.attendees.size
Attendee.first.event
範例二,從Event物件中建立一個Attendee:
e = Event.first
a = e.attendees.build( :name => 'ihower' )
a.save
e.attendees
範例三,直接從Event物件中建立一個Attendee:
e = Event.first
a = e.attendees.create( :name => 'ihower', :event => e )
e.attendees
範例四,先建立Attendee物件再放到Event中:
e = Event.first
a = Attendee.create( :name => 'ihower' )
e.attendees << a
e.attendees
範例五,根據特定的Event查詢Attendee
e = Event.first
e.id # 1
a = e.attendees.find(3)
attendees = e.attendees.where( :name => 'ihower' )

這樣就可以寫出限定在某個Event下的條件查詢,用這種寫法可以避免一些安全性問題,不會讓沒有權限的使用者搜尋到別的EventAttendee

範例六,刪除
Event.attendees.destroy_all # 會一筆筆觸發Attendee的destroy回呼
Event.attendees.delete_all # 不會觸發Attendee的destroy回呼

有個口訣可以記起來:有Foreign KeyModel,就是設定belongs_to的Model。

學到這裡,還記得上一章建立的Category嗎? 它也要跟Event是一對多的關係,讓我們補上程式吧:

class Category < ActiveRecord::Base
    has_many :events
end

class Event < ActiveRecord::Base
  belongs_to :category
  # ...
end

多對多關聯many-to-many

has_one diagram

has_one diagram

另一種常見的關聯模式則是多對多,一筆資料互相擁有多筆資料,例如一個Event有多個Group,一個Group有多個Event。多對多關聯的實作必須多一個額外關聯用的資料表(又做作Join table),讓我們來建立Group Model和關聯用的EventGroupship Model,其中後者定義了兩個Foreign Keys

rails g model group name:string
rails g model event_groupship event_id:integer group_id:integer

執行bundle exec rake db:migrate產生這兩個資料表。

分別編輯app/models/event.rbapp/models/group.rbapp/models/event_groupship.rb

class Event < ActiveRecord::Base
    has_many :event_groupships
    has_many :groups, :through => :event_groupships
end

class EventGroupship < ActiveRecord::Base
    belongs_to :event
    belongs_to :group
end

class Group < ActiveRecord::Base
    has_many :event_groupships
    has_many :events, :through => :event_groupships
end

這個Join table筆者的命名習慣會是ship結尾,用以凸顯它的關聯性質。另外,除了定義Foreign Keys之外,你也可以自由定義一些額外的欄位,例如記錄是哪位使用者建立關聯。

blongs_tohas_many我們見過了,這裡多一種has_many :through方法,可以神奇地把EventGroup關聯起來,讓我們進入rails console實際操作資料庫看看:

範例,建立雙向關聯記錄:
g = Group.create( :name => 'ruby taiwan' )
e1 = Event.first
e2 = Event.create( :name => 'ruby tuesday' )
EventGroupship.create( :event => e1, :group => g )
EventGroupship.create( :event => e2, :group => g )
g.events
e1.groups
e2.groups

Rails還有一種舊式的has_and_belongs_to_many方法也可以建立多對多關係,不過已經很少使用,在此略過不提。

關連的參數

以上的關聯方法blongs_tohas_onehas_many都還有一些可以客製的參數,讓我們來介紹幾個常用的參數,完整的參數請查詢API文件:

class_name

可以變更關聯的類別名稱,例如:

class Event < ActiveRecord::Base
    belongs_to :manager, :class_name => "User" # 外部鍵是user_id
end
foreign_key

可以變更Foreign Key的欄位名稱,例如改成manager_id

class Event < ActiveRecord::Base
    belongs_to :manager, :class_name => "User", :foreign_key => "manager_id"
end
order

has_many可以透過:order參數指定順序:

class Event < ActiveRecord::Base
    has_many :attendees, :order => "id desc"
    #...
end
dependent

可以設定當物件刪除時,也會順便刪除它的has_many物件:

class Event < ActiveRecord::Base
    has_many :attendees, :dependent => :destroy
 end

:dependent可以有三種不同的刪除方式,分別是:

  • :destroy 會執行attendeedestroy回呼
  • :delete 不會執行attendeedestroy回呼
  • :nullify 這是預設值,不會幫忙刪除attendee

要不要執行attendee的刪除回呼效率相差不少,如果需要的話,必須一筆筆把attendee讀取出來變成attendee物件,然後呼叫它的destroy。如果用:delete的話,只需要一個SQL語句就可以刪除全部attendee

joins 和 includes 查詢

針對Model中的belongs_tohas_many關連,可以使用joins,也就是INNER JOIN

Event.joins(:category)
# SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id"

可以一次關連多個:

 Event.joins(:category, :location)

joins主要的用途是來搭配where的條件查詢:

Event.joins(:category).where("categories.name is NOT NULL")
# SELECT "events".* FROM "events" INNER JOIN "categories" ON "categories"."id" = "events"."category_id" WHERE (categories.name is NOT NULL)

透過joins抓出來的event物件是沒有包括其關連物件的。如果需要其關連物件的資料,會使用includesincludes可以預先將關連物件的資料也讀取出來,避免N+1問題(見效能一章)

Event.includes(:category)
# SELECT * FROM events
# SELECT * FROM categories WHERE categories.id IN (1,2,3...)

同理,也可以一次載入多個關連:

Event.includes(:category, :attendees)
# SELECT "events".* FROM "events" 
# SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (1,2,3...)
# SELECT "attendees".* FROM "attendees" WHERE "attendees"."event_id" IN (4, 5, 6, 7, 8...)

includes方法也可以加上條件:

Event.includes(:category).where( :category => { :position => 1 } )

更多線上資源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值