[Rails] Active Record Association (Model)
速查
- Book
belongs_toAuthor: Book 會多 author_id - Supplier
has_oneAccount: Account 會多 supplier_id
# 會產生 store_id 和 product_id
$ rails g model WareHouse store:references product:references
有 column_id 的 table 表示在比較下階。
觀念
在處理關連性資料庫時,我們經常需要透過 Query 的方式找出另一個關連的資料。但是透過 Active Record Association,我們可以更方便的去操作關連性資料庫。
在 Rails 裡所謂的關係,是指在 Model 層級的關係,主要是透過 Model 的方法(例如 has_many 或 belongs_to)搭配 Rails 的資料表慣例設定主鍵(Primary Key)及外部鍵(Foreign Key),讓這些資料表串在一起。
每一個 association 使用後對應會產生的方法:
- Detailed Association Reference @ RailsGuides
- Active Record Association @ RailsGuides
Generator
# rails g model [ModelClassName <column:type:options>
rails g model WareHouse store:references product:references
rails g model user email:index location_id:integer:index
- 最後加上
--skip-migration將不會產生 migration 檔。 column:type:option,options 可以加入index
這裡的 store:references 寫法其實也可以用前面的 store_id:integer 寫法,但 references 的寫法會多做幾件事:
- 自動加上索引(index),加快查詢速度。
- 自動幫 Model 加上
belongs_to。
column options
indexunique
column type
integerprimary_keydecimalfloatbooleanbinarystringtextdatetimedatetimetimestamp
參考範例
##
# price 要記得加上引號
##
rails g model user pseudo:string{30}
rails g model product 'price:decimal{10,2}'
# uniq, references, index
rails g model user email:index location_id:integer:index
rails g model user pseudo:string:uniq
rails g model user username:string{30}:uniq
rails g model photo album:references
rails g model WareHouse store:references product:references
# polymorphic
rails g model product supplier:references{polymorphic}
rails g model product supplier:references{polymorphic}:index
Advanced Rails model generator @ BY ANDREY KOLESHKO
關連類型
- belongs_to
- has_one
- has_many
- has_many :through
- has_one :through
- has_and_belongs_to_many
在使用上面這些 Active Record Association 的方法時, Database 的資料表和欄位必須透過
migration或model的 generator 先建立好。
一對一對應關係
keywords: belongs_to, has_one
一對一的對應關係在使用時 AAA 會 belongs_to BBB;而 BBB 則會 has_one AAA,這兩者的差別在於要把 foreign_key 放在誰身上。當使用 AAA belongs_to BBB 時,AAA 的資料表中會多出 bbb_id。
但更重要的是透過資料實際的意義來思考,舉例來說應該是 User 擁有(has_one)Account,而非帳號擁有使用者;Account 應該是屬於(belongs_to)User 的,這時候 accounts 資料表中會多出一個 user_id 的 foreign_key 欄位。
過去在建立 migration 檔案時,會直接使用
t.integer :user_id現在則可以使用t.references :user,兩個意思是一樣的。
belongs_to
假設我們有 Author 和 Book 兩個 Model,其中每本書都會對應到一個作者(資料表和欄位需要先透過 migration 或 model generator 建立),那麼我們 可以用 belongs_to 建立一對一的關連性關係:
# ./app/models/book.rb
class Book < ApplicationRecord
belongs_to :author
end
在 Book 資料表中會多一個 author_id 的欄位,用來對應到 Author 的 id ,這個 author_id 被用來做外部對應的欄位,稱為外部鍵(foreign key)。
*Book belongs to Author- 表示,要在 migration 時 Book 資料表中會多出 author_id 的欄位。

belongs_to 在使用時要用單數。
migration 檔內容
相對應的 migration 檔會長這樣:
# ./db/migrate/migration_file.rb
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author, index: true
t.datetime :published_at
t.timestamps
end
end
end
使用 :association, association=
keywords
:association,association=(associate),build_association(attributes = {}),create_association(attributes = {}),create_association!(attributes = {})
association 就是 belongs_to 後面方的 Symbol,因此當加上 belongs_to :author 之後,Book Model 會動態的多了幾個好用的方法,像是 :author, author=:
# rails console
# :author 方法
book = Book.first
book.author # 列出屬於該本書的 author 實例物件
# author= 方法
aaron = Author.find(1)
book.author = aaron # = 後面要放 Model 的實例物件
has_one
Supplier has one Account 表示要在 migration 時,Account 資料表中會多出 supplier_id 的欄位。
# ./app/models/supplier.rb
class Supplier < ApplicationRecord
has_one :account
end
migration 檔案內容
其中相對應的 migration 檔會長這樣,資料表在建立的時候一樣試用 account belongs_to supplier:
# ./db/migrate/migration_file.rb
class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true # 表示 supplier 是 foreign key
t.string :account_number
t.timestamps
end
end
end

使用
keywords
:association,association=(associate),build_association(attributes = {}),create_association(attributes = {}),create_association!(attributes = {})
build和create的差別在於build需要save而create不用。
當在 Supplier 加上 has_one :account 之後,User Model 會動態的多了幾個好用的方法,像是:account, account=, build_account, create_account:
使用 :account, account=
# rails console
# :account
supplier_aaron = Supplier.find(1)
supplier_aaron.account # 取的該 supplier 的 account 實例物件
# account= ,執行時資料會直接存到 database
new_account = Account.create()
supplier_aaron = Supplier.find(1)
supplier_aaron.account = new_account # 把 account 實例物件給 supplier
使用 build_account, create_account
build_account 需要 save;create_account 不需要 save。另外 create_account 只能用在該 supplier 還沒有 account 的情況。
# rails console
# build_account
supplier_pj = Supplier.find(2)
supplier_pj.build_account(account_number: '0323232')
supplier_pj.save
# create_account
supplier_aaron = Supplier.create(name: 'aaron')
supplier_aaron.create_account(account_number: '245434')
在 table 中有包含 _id 欄位的是屬於比較低階層的。 例如 Account table 中有 user_id 的欄位,表示 Account belongs_to User;User has_one Account。
一對多對應關係
keywords: has_many
每個 author 可以有很多本 books:
# ./app/models/author.rb
# 透過 dependent 可以處理有關連資料存在是要如何處理
# destroy: 把關連的資料刪除
# restrict_with_error: 有關連資料時產生錯誤,不能儲存
# restrict_with_exception: 有關連資料時產生錯誤,會直接噴錯,要 catch 處理
# delete_all
# nullify
class Author < ApplicationRecord
has_many :books, dependent: :restrict_with_error
end
has_many 在使用時要用複數。

*Author has_many Books- 表示要 migration 時在 Book 資料表中會多出 author_id 的欄位。
migration 檔案內容
在建立 migration 當時,也是 book belongs to author,表示會在 book 多出 author_id 的欄位:
# ./db/migrate/migration_file.rb
class CreateAuthors < ActiveRecord::Migration[5.0]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author, index: true
t.datetime :published_at
t.timestamps
end
end
end
使用
keywords
collectioncollection<<(object, ...)collection.delete(object, ...)collection.destroy(object, ...)collection=(objects)collection_singular_idscollection_singular_ids=(ids)collection.clearcollection.empty?collection.sizecollection.find(...)collection.where(...)collection.exists?(...)collection.build(attributes = {}, ...)collection.create(attributes = {})collection.create!(attributes = {})
collection 用放在 has_many 後面的 Symbol 名稱替代。
# Rails Console
# :models
author_aaron = Author.find(1)
author_aaron.books # 取得屬於 author_aaron 的 books
author_aaron.book_ids # 取得屬於 author_aaron 的 book ids
# models=
book1 = Book.find(1)
book2 = Book.find(2)
author_aaron.books = [book1, book2] # 將 book1 和 book2 歸於 author_aaron 所寫
author_aaron.book_ids = [1, 2] # 直接根據 books 的 id 歸於 authro_aaron
# models.build
author_aaron.books.build(name: 'Learn JavaScript the hard way') # 建立一本由 author_aaron 所寫的書
多對多對應關係
多對多的對應關係一般可以有兩種作法:
has_many :through: 透過另外建立一個 table,並使用一個獨立的 model 來將兩個 table 間產生關連性。has_and_belongs_to_many: 不會另外建立一個 model(但一樣會建立 table),來直接將兩個 table 關連起來。
在選擇上,比較簡單的作法是直接使用 has_and_belongs_to_many,除非你有需要透過 Model 來操作、驗證、使用回呼(callback)第三個資料表時(join table),再使用 has_many :through。
方法一:使用 has_many :through
keywords: has_many :through
這表示所宣告的 model 可以透過第三個 model 對應另一個 model 的一個或多個物件實例。例如,醫生和病人的約診:
- 醫生(physician)有很多約診單(appointments)
- 醫生可以透過約診單約很多病人
# ./app/models/physician.rb
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
- 約診單(appointment)屬於醫生(physician)和病人(patient)
# ./app/models/appointment.rb
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
- 病人(patient)有很多約診單(appointments)
- 病人可以透過約診單約很多醫生
# ./app/models/patient.rb
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
has_many後要用複數:through後要用複數belongs_to後要用單數

這時候必需要在 migration 檔時中建立 physician_id 和 patient_id 的欄位。
migration 檔案內容
- Appointments belongs_to Physician:會在 Appointments table 中多出 physician_id 欄位
- Appointments belongs_to Patient:會在 Appointments table 中多出 patient_id 欄位
# ./db/migrate/migration_file.rb
class CreateAppointments < ActiveRecord::Migration[5.1]
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician, index: true
t.belongs_to :patient, index: true
t.datetime :appointment_date
t.timestamps
end
end
end
使用
keywords: :models, models=
# rails console
dr_aaron = Physician.find(1)
dr_pj = Physician.find(2)
pt_aaron = Patient.find(1)
pt_pj = Patient.find(2)
pt_ma = Patient.find(3)
# models=
dr_aaron.patients = [pt_aaron, pt_pj] # 此時就已經將資料寫入 appointments 資料表中了
dr_pj.patients = [pt_pj, pt_ma] # 此時就已經將資料寫入 appointments 資料表中了
# :models
dr_aaron.patients # 取得屬於 dr_aaron 的 patients 實例物件
pt_ma.physicians # 取得屬於 pt_ma 的 physicians 時記錄鍵
方法二:使用 HABTM
keywords: has_and_belongs_to_many
假設一個 Author 可以出很多本 Books,一本 Book 也可能有許多 Authors,這時候就可以使用 has_and_belongs_to_many 來建立資料庫:
# ./app/models/book.rb
class Book < ApplicationRecord
has_and_belongs_to_many :authors
end
# ./app/models/author.rb
class Author < ApplicationRecord
has_and_belongs_to_many :books
end
這種情況在 migration 時要另外建立一個儲存關連性資料表,這個第三方資料表的名字是有規定的,預設是「兩個資料表依照英文字母先後順序排序,中間以底線分格」,所以以我們這個例子來說,這個資料表的名字就是「authors_books」。

has_and_belongs_to_many後面要接複數
migration 檔案內容
# ./db/migrate/migration_file.rb
class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly, index: true
t.belongs_to :part, index: true
end
end
end
提醒:belongs_to 和 has_and_belongs_to_many 可以同時存在
belongs_to 和 has_and_belongs_to_many 可以同時存在:
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :users
end
這樣就可以同時使用 article.user 和 article.users 這兩種方法。
嵌套式架構:一對多對多
keywords: has_many :through
透過 has_many :through 我們也可以建立嵌套式的模型,例如,一份文件(document)有很多的章節(sections);每個章節又可以有很多段落(paragraph):
# ./app/models/document.rb
class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
# ./app/models/section.rb
class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
# ./app/models/paragraph.rb
class Paragraph < ApplicationRecord
belongs_to :section
end
一對一對一
keywords: has_one :through
宣告的第一個 model 透過 through 與另外第三個 model 去和第二個 model 產生關連性。例如 Supplier 有一個 Account,而每一個 Account 又可以有一個 AccountHistory:
# ./app/models/supplier.rb
class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
Account belongs to Supplier,所以在 migration 設定檔中, accounts 中會有 supplier_id:
# ./app/models/account.rb
class Account < ApplicationRecord
belongs_to :supplier
has_one :account_history
end
AccountHistory belongs_to Account,所以在 migration 設定檔中,account_histories 中會有 account_id:
# ./app/models/account_history.rb
class AccountHistory < ApplicationRecord
belongs_to :account
end

migration 檔案內容
# ./db/migrate/migration_files.rb
class CreateAccountHistories < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier, index: true
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account, index: true
t.integer :credit_rating
t.timestamps
end
end
end
Polymorphic association
keywords: polymorphic, able
透過 polymorphic associations,一個 model 可以屬於多個 model,例如,我們可以讓一個 pictures model 同時屬於 employee model, product model 和其他更多的 model。
使用關鍵字 polymorphic: true 和 as:
# ./app/models/picture.rb
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
# ./app/models/employee.rb
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
# ./app/models/product.rb
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
其中 在 pictures table 中 imageable_type 指的是對應到的 table 欄位(例如 employees 或 products)而 imageable_id 指的是該欄位的 id(也就是 product_id 或 employees_id)。

migration 檔案內容
# ./db/migrate/migration_files.rb
class CreatePictures < ActiveRecord::Migration[5.1]
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
也可以用 t.references 簡化寫法:
# ./db/migrate/migration_files.rb
class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true, index: true
t.timestamps
end
end
end
使用
你可以想成 belongs_to + polymorphic 設定了一個 interface 讓其他 model 可以使用,因此從 Employee 你可以透過 @employee.pictures 取得屬於該 Employee 的所有 pictures 實例物件。相似的,你也可以取得 @product.pictures。
如果你有 Picture Model 的實例,你也可以透過 @picture.imageable 的方式取得它的父層。要讓它可以辦到,你需要在 model 中同時宣告 foreign key column (:imageable_id) 和 type column (:imageable_type) 來定義 polymorphic interface:
參考資料:Polymorphic association @Active Record Basic
instance method options
:autosave: Boolean:class_name: String:counter_cache: Boolean || Symbol,減少需要的 query 次數,但要記得使用前新增 table 的欄位。:dependent:當相關連的物件被銷毀時要做的處理。:foreign_key:primary_key:polymorphic:touch:validate:optional:conditions:through:as:inverse_of: 用來定義 bi-direction association
Detailed Association Reference @ RailsGuide
注意事項
留意快取(cache)
keywords: reload
Rails 會向 database 要一次資料,接著會把這份資料快取起來,如過要清除快取,可以透過關鍵字 reload:
author.books # retrieves books from the database
author.books.size # uses the cached copy of books
author.books.reload.empty? # discards the cached copy of books
# and goes back to the database
避免命名衝突(naming collision)
不要使用到 attributes 或 connection 等等在 ActiveRecord::Base 中 instance method 有的關鍵字。
記得更新資料庫結構(Schema)
Active Record 主要是 ORM,幫助 Model 和 table 溝通,但是要留意不要只建立的 Model 卻忘記改 table 的架構(Schema)。
- 對於
belongs_to的關連式資料表,記得加上外部鍵(foreign keys),並且建議對加上索引(index)以加速搜尋。 - 對於
has_and_belongs_to_many的關連式資料表,記得建立對應的 join table(兩個資料表的名稱根據字母排序連接起來),並且需要移除 Primary Key。
# ./db/migrate/migration_files.rb
# 透過 id: false 來移除主鍵
create_table :assemblies_parts, id: false do |t|
end
Updating the Schema @ Active Record Association in Rails Guides
參考資料
- Model 關連性 @ 為你自己學 Ruby on Rails
- Active Record Basics @ RailsGuides
- Active Record Association @ RailsGuides