就如同昨天結尾所說的,我們應該要跟 Rails 一樣,用 Task.title
的方式來呼叫方法,而不是用 Taks['title']
來呼叫,今天我們就來建立真正的 attr_accessor
在進行實作之前,我們要先聊聊一些 Ruby 的基本觀念,在 Ruby 的世界裡面我們可以動態的來定義一個 method(Defining Methods Dynamically),就像下面程式碼所示
class Amo
define_method :hola do |my_arg|
"Hi! #{my_arg}"
end
end
puts Amo.new.hola('apa')
# 印出Hi! apa
這樣的好處是我們可以在程式執行時,直到最後一刻才決定方法的名稱
,這也是像是 Ruby 這 直譯語言 (Interpreted language) 的優勢,所以我們可以利用這樣的技巧來實作
# just_do/sqlite_test.rb
require 'sqlite3'
require 'mavericks/sqlite_model'
class Task < Mavericks::Model::SQLite
Task.schema.keys.each do |attr|
define_method attr do
self[attr.to_s]
end
define_method "#{attr}=" do |arg|
self[attr.to_s] = arg
end
end
end
task = Task.new('title': '鐵人30', 'content': '每天一篇文章')
task.save!
Task.all.each { |task| puts task.title }
透過之前建立好的方法 schema
,來取得 Table 的欄位,靠著這些欄位列表,利用 define_method
來決定要建立那些 accessor
,不過這樣有個缺點,就是會非常的慢,而且你會發現為了能夠使用 accessor
必須在每個 class 都這樣寫,數量一多會非常麻煩,所以要改善一下實作
在 Ruby 裡面還有另一個這樣的機制,在物件呼叫一個不存在的 method 時,會去呼叫另一個 method,說起來有點饒舌,我們直接來看一個例子
class Amo
def method_missing(method, *args)
puts "You called: #{method}(#{args.join(', ')})"
puts "(You also passed it a block)" if block_given?
end
end
# 沒有定義 hola 直接呼叫
Amo.new.hola('jose', 2) {}
出現這樣的結果
You called: hola(jose, 2)
(You also passed it a block)
從這裡我們可以知道,當找不到這個 method 的時候,Ruby 會呼叫 method_missing 來做事,也因為這樣,你可以透過複寫 method_missing
來定義 attr_accessor
,有點像之前的 const_missing
(還記得他嗎?)
當然每次 mthod_missing
也會有速度的問題,所以為了減少呼叫 的次數,我們還需要在呼叫 mthod_missing
之後,定義那些 attr_accessor
現在就來修改我們的 sqlite_model.rb
# mavericks/lib/mavericks/sqlite_model.rb
module Mavericks
module Model
class SQLite
def method_missing(attr, *args)
attrs = self.class.schema
attr = attr.to_s.gsub('=', '')
if attrs.key?(attr)
self.class.define_attr(attrs)
val = args.empty? ? self.send(attr) : self.send("#{attr}=", args[0])
return val
else
super
end
end
def self.define_attr(attrs)
attrs.keys.each do |attr|
add_method_to_get(attr)
add_method_to_set(attr)
end
end
def self.add_method_to_get(attr)
define_method attr do
self[attr.to_s]
end
end
def self.add_method_to_set(attr)
define_method "#{attr}=" do |arg|
self[attr.to_s] = arg
end
end
# .
# .
# (略)
我們在 SQLite
這個 class 裡面加上 method_missing
,現在當我們第一次執行 task.title
時,就會跳到 method_missing
,因為此時的我們還沒有替 Task
定義任何 attr_accessor
,所以進到 method_missing
的第一件事情就是取得 schema
attrs = self.class.schema
接著我們將送進來的 method 做簡單的處理,讓 title=
變成 title
attr = attr.to_s.gsub('=', '')
然後做簡單的檢查,這個 attibute
是不是 schema
的欄位之一,如果不是的話,我們就呼叫 super
執行 method_missing
原本做的事情,如果有包含欄位,就定義這些欄位的 method
最後,我們用 empty? 來檢查 args 有沒有值,如果沒有的話,我們就假設他是想取值(例如: task.ttile
),如果有給參數的話,代表想要設定新的值(例如 task.title = '鐵人40'
)
val = args.empty? ? self.send(attr) : self.send("#{attr}=", args[0])
最後我們回到 sqlite_test.rb 來測試一下
require 'sqlite3'
require 'mavericks/sqlite_model'
class Task < Mavericks::Model::SQLite
end
# 新增一筆資料 title 為 "鐵人30"
task = Task.new('title': '鐵人30', 'content': '每天一篇文章')
task.save!
# 取得剛剛新增的資料,並且將 title 改為 "鐵人40",然後儲存
task2 = Task.find(1)
task2.title = '鐵人40'
task2.save
# 再取一次,確認 title 是不是改為 "鐵人40"
task3 = Task.find(1)
puts task3.title
當然不是,要知道 Rails 做得永遠比我們想像中還要來得多,但你有沒有發現 SQLite
越來越肥大,是不是該做一下整理?並且再改良的更好些,而且 SQLite 畢竟不適合拿來開發網頁用,應該用 PostgreSQL...
嗯,那明天我們就來推出一個威力加強版的 ORM 吧!