ORM 之 CRUD / Scope / Validates / Callback



  • ORM = Object Relational Mapping

  • Active Record = 把資料做成物件
    Active Record 是一種 ORM 框架
    物件 = 欄位 + 基本操作 + 商業邏輯

  • Model = 依照 Active Record 模式設計的產物
    可以翻譯成宅宅語言的:
    Model = Active_record.new



ORM CRUD


Create

1
2
3
4
5
book = Product.new(name: 'Ruby book', price: 350)
book.save # 到這⼀步才存到資料庫裡

# 建立並同時存到 products 表格裡:
Product.create(name: 'Ruby book', price:350)
  • new 方法會建立一筆資料,但還不會存到資料庫裡。
  • create 失敗會 rollback ,可以用 if 判斷
  • create! 失敗會 error,需要用 begin rescue 抓例外

Read

1
2
3
4
5
6
7
8
9
10
11
12
Product.first
Product.last
Product.find(1) # 只能用 id 找,找不到會例外
Product.find_by(id: 1) # 能用 name 等其他東西找,找不到會 nil
Product.find_by_sql("select \* from products where id = 1")
Product.find_each do | product |

# 預設抓 1000 筆

end
Owner.find_by_name_and_email_and_id # 還有這種神奇的寫法,但效能較差
Owner.find_by(name:'kk', email:'ddd') # 效能比較好
  • 還有許多讀取方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Product.all # 知道查什麼時就不用放 all (不需要每次都放)
Product.select('name')
Product.where(name: 'Ruby') # 找到全部 name 的陣列
Product.find_by(name: 'Ruby') # 只會找到一筆
Product.order('id DESC')
Product.order(id: :desc)
Product.limit(5)
Owner.select(:name).where(name:'o1') # 可以用組合技,省記憶體空間
Product.where('price < 10') # 查詢大於小於只能用字串
Owner.where(name:'ruby').first

# 直接 limit 1 筆,較省記憶體空間

all = Owner.where(name:'ruby')
all.first

# 把全部資料都抓出來存到記憶體,再抓第一個,非常占空間
  • 好用的方法們(不要傻傻的用 ORM 抓出來轉迴圈再來計算)
    • count
    • average
    • sum
    • maximum 與 minimum

Update

1
2
3
4
5
6
7
8
p1 = Product.find_by(id: 1) # 儲存
p1.name = 'Ruby book' p1.save # 更新
p1.update(name: 'Ruby book')
p1.update_attributes(name: 'Ruby book') # update 兩者相同,這行可以不用(太長)

Product.update_all(name: 'Ruby book', price: 250)

# 全部更新(請⼩心使⽤!)
  • 其他方法
1
2
3
4
5
# increment / decrement 不會自己 save 但 increment! 會直接存

my_order = Order.first
my_order.increment(:quantity) # quantity 欄位的值 + 1
my_order.toggle(:is_online) # 把原本的 true/false 值對調

Delete

1
2
3
4
5
p1 = Product.find_by(id: 1)
p1.destroy # 會走 callback 流程,過程可以做檢查
p1.delete # 直接刪除
Product.delete(1) # 刪除編號 1 號的商品
Product.destroy_all("price < 10") # 刪除所有低於 10 元的商品


ORM 其他


Scope

  • 把一群條件整理成一個
  • Scope 簡化使用時的邏輯
  • 減少在 Controller 裡寫一堆 Where 組合
  • 用起來跟類別方法一樣
1
2
3
4
5
6
7
8
9
10
11
12
# model 檔
class Product < ApplicationRecord
def self.cheap
where('price < 10')
end

# 上下兩者相同

scope :cheap, ->{where('price < 15')}
end

# 同名方法的話後面會蓋掉前面 結果為 price < 15
  • default_scope 可幫所有的查詢預設套用
1
2
3
4
5
6
7
8
class Product < ApplicationRecord
default_scope { order('id DESC') }
scope :available, -> { where(is_available: true) }
default_scope { where(deleted: false) }

# 應用場景:假刪除/軟刪除 讓被刪除資料不顯示

end

Q: scope 跟類別方法有什麼不一樣?
A: 一樣
Q: 什麼時候該用什麼寫法?
A: 只有一行時寫 scope,多行就寫類別方法


Validates

  • 方法們:
    • presence
    • format
    • uniqueness
    • numericality
    • length
    • condition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 自訂驗證
class Product < ApplicationRecord
validate :name_validator

private
def name_validator
unless name.starts_with? 'Ruby'
errors[:name] << 'must begin with Ruby'
end
end
end

class BeginWithRubyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.starts_with? 'Ruby'
record.errors[attribute] << 'must begin with Ruby'
end
end
end

class Product < ApplicationRecord
validates :name, presence: true, begin_with_ruby: true
end
  • 自訂驗證寫在 lib 裡更好
  • 資料驗證過程:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    p1 = Product.new # 忘了寫產品名稱,但還沒寫入資料庫

    p1.errors.any? # 看看有沒錯誤...
    => false # 這時候還沒發⽣生錯誤

    p1.validate # 開始進⾏行行驗證(還不需要存檔喔)
    => false # 得到 false 表⽰示沒有驗證成功

    p1.errors.any? # 看看有沒有錯誤...
    => true # 這時候就有錯誤內容了了

    p1.errors.full_messages
    => ["Title can't be blank"] # 使⽤ full_messages 可把資料印出來

Q: 硬是要繞過驗證可以嗎?
A: 可以,繞過驗證的方式如下(但不要這麼做啦)
cc.save(validates: false)
Q: 只要有驗證就可以保證資料正確嗎?
A: 不,驗證是綁在 model ,另一個系統若直接寫進資料表就不行


Callback

  • 資料存檔的流程會經過以下流程,可在這些流程執行的時候對資料做一些事情

    1
    save > valid > before_validation > validate > after_validate > before_save > before_create > create > after_create > after_save > after_commit
  • 在資料存檔前對密碼加密

    1
    2
    3
    4
    5
    6
    7
    8
    class User < ApplicationRecord
    before_save :encrypt_email
    private
    def encrypt_email
    require 'digest'
    self.email = Digest::MD5.hexdigest(email)
    end
    end

Q: 在範例中,如果改用 before_save 會造成什麼問題?
A: 應該用 before_create ,加密只應該在建立時做,不應該每次更新都做
Q: before_save 跟 before_create 有什麼差別?
A: before_save 在新增 / 存檔時都會做,before_create 只有在新增時做