[Rails] Active Record Association (Model)
速查
- Book
belongs_to
Author: Book 會多 author_id - Supplier
has_one
Account: 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
index
unique
column type
integer
primary_key
decimal
float
boolean
binary
string
text
date
time
datetime
timestamp
參考範例
##
# 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
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.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