跳至主要内容

[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_manybelongs_to)搭配 Rails 的資料表慣例設定主鍵(Primary Key)及外部鍵(Foreign Key),讓這些資料表串在一起。

每一個 association 使用後對應會產生的方法:

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 的資料表和欄位必須透過 migrationmodel 的 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

假設我們有 AuthorBook 兩個 Model,其中每本書都會對應到一個作者(資料表和欄位需要先透過 migration 或 model generator 建立),那麼我們可以用 belongs_to 建立一對一的關連性關係:

# ./app/models/book.rb

class Book < ApplicationRecord
belongs_to :author
end

Book 資料表中會多一個 author_id 的欄位,用來對應到 Authorid ,這個 author_id 被用來做外部對應的欄位,稱為外部鍵(foreign key)

*Book belongs to Author- 表示,要在 migration 時 Book 資料表中會多出 author_id 的欄位。

belongs_to

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

has_one

使用

keywords
  • :association,
  • association=(associate),
  • build_association(attributes = {}),
  • create_association(attributes = {}),
  • create_association!(attributes = {})

buildcreate 的差別在於 build 需要 savecreate 不用。

當在 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 在使用時要用複數。

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 後要用單數

has_many

這時候必需要在 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

參考:Rails 多對多關聯設定與存取

假設一個 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

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_tohas_and_belongs_to_many 可以同時存在:

# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :users
end

這樣就可以同時使用 article.userarticle.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

img has_one

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: trueas:

# ./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)。

img

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)

不要使用到 attributesconnection 等等在 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

參考資料