已經搞了好幾天的 ORM,今天總算要做個結尾啦,這個系列我們從 file_model.rb
用 JSON 格式檔案當作資料庫,在到 sqlite_model.rb
做了初步的 ORM 效果,中間我們也學習了各種動態生成 Attribute 的寫法,今天要來介紹另一個 ORM 很常用的技巧,就是 where
相信大家對於 where
應該不陌生,就像是下面範例一樣
Task.where(id: 1)
不過在進行 where 之前,我們一樣先把 ORM 其他功能都補齊吧!
因為有了前面的例子,基本的操作就比較快速帶過,先來加上 save
因為 save 是屬於 isntance method,所以我會做在 persistence.rb
# mavericks/lib/mavericks/data_record/persistence.rb
module Mavericks
module DataRecord
module Persistence
def initialize(attributes = {})
self.class.set_column_to_attribute
@attributes = attributes
# 用 new_record 來紀錄有沒有重覆儲存
@new_record = true
end
def new_record?
@new_record
end
def save!
return true unless new_record?
vals = @attributes.values.map { |value| self.class.to_sql(value) }
self.class.connection.execute <<-SQL
INSERT INTO #{self.class.table_name} (#{@attributes.keys.join(',')})
VALUES (#{vals.join ","});
SQL
@new_record = false
end
def save
self.save! rescue false
end
end
end
end
會發現其實跟 sqlite_model.rb
很像,但我們多了一些處理,例如同一個物件,我們會用 @new_record
紀錄是不是重複 save,如果是的話就不會再讓他存進資料庫,其他部分都在 sqlite_model.rb
講解過,例如 save
和 save!
另外因為我們也還是沿用 to_sql 這個語法,所以我們還需要在 method 加上 to_sql
# mavericks/lib/mavericks/data_record/method.rb
def to_sql(val)
case val
when Numeric
val.to_s
when String
"'#{val}'"
else
raise "Can't support #{val.class} to SQL!"
end
end
有了 save,要怎麼知道有沒有存成功呢?還記得之前的 count
嗎?這裡原理一樣應該不難,只是要注意 count
也是 class method,要擺在 method
這個檔案裡面
# mavericks/lib/mavericks/data_record/method.rb
def count
self.connection.execute(<<-SQL)[0]['count']
SELECT COUNT(*) FROM #{self.table_name}
SQL
end
回到 sqlite_test.rb 測試一下吧
# just_do/sqlite_test.rb
require 'mavericks/data_record'
Mavericks::DataRecord::Base.establish_connection
class Task < Mavericks::DataRecord::Base
end
task = Task.new(title: '鐵人30', content: '一天一篇文章')
task.save
puts Task.count
一樣每執行一次,count
的數量都會加 1,至於 save! 和 save 也試試看效果吧
# just_do/sqlite_test.rb
require 'mavericks/data_record'
Mavericks::DataRecord::Base.establish_connection
class Task < Mavericks::DataRecord::Base
end
# 故意取一個沒有的 column 名稱
task = Task.new(name: '鐵人30', content: '123')
puts task.save
# false
task.save!
# ERROR: column "name" of relation "tasks" does not exist
我們之前做得 all
,裡面其實包的是 Hash,來假裝有 Attribute 這件事情
row.map do |attr|
data = Hash[schema.keys.zip attr]
self.new data
end
但在 Rails 裡面,裡面包的可是一個個物件,所以你是可以用這樣的方式來取得資料
Task.all.each do |task|
taks.title
# 鐵人30
end
那我們來改良一下 all
吧!改良之前,我們先建立一個 class Relation
,這也是我們今天主題要用到的東西,他其實就是紀錄了所有物件
# mavericks/lib/mavericks/data_record/relation.rb
module Mavericks
class Relation
def initialize(klass)
@klass = klass
end
def to_sql
"SELECT * FROM #{@klass.table_name}"
end
def records
@records ||= @klass.find_by_sql(to_sql)
end
end
end
在 method 加上
# mavericks/lib/mavericks/data_record/method.rb
def all
Relation.new(self).records
end
def last
all.last
end
def find(id)
find_by_sql("SELECT * FROM #{self.table_name} WHERE id = #{id.to_i}").first
end
def find_by_sql(sql)
connection.execute(sql).map do |attributes|
new(attributes)
end
end
這裡我們雖然是呼叫 Task.all
,但其實背地裡是呼叫 Relation
的 records,等於用 Relation
來包裝起來
記得加上 relation.rb
# mavericks/lib/mavericks/data_record/relation.rb
require 'mavericks/support'
require_relative "./data_record/relation"
require_relative "./data_record/connection_adapter.rb"
require_relative "./data_record/persistence"
require_relative "./data_record/method"
require_relative "./data_record/base"
module Mavericks
module DataRecord
end
end
這時候回到 sqlite_test.rb
印出來會是一個一個物件
# just_do/sqlite_test.rb
require 'mavericks/data_record'
Mavericks::DataRecord::Base.establish_connection
class Task < Mavericks::DataRecord::Base
end
puts Task.all
可是為什麼要這樣包呢?多那一層的 Relation 有什麼好處?還記得 Rails 的查詢 where 嗎?
他可以這樣用 Task.where(...).where(...),所以為了達到這樣的效果,我們可以這樣實作
# mavericks/lib/mavericks/data_record/method.rb
def where(query)
sql_syntax = query.map do |key, val|
"#{key.to_s} = #{self.to_sql(val)}"
end
Relation.new(self).where(sql_syntax)
end
我們一樣在寫一個 class method
,讓 Task
可以這樣呼叫 Task.where(...)
,但實際上我們是 new 一個 Relation
的物件,並且將 where 包含的查詢語法傳遞下去,那在 Relation 那邊會怎麼處理呢?
我們來修改一下 relation.rb
的程式碼
# mavericks/lib/mavericks/data_record/relation.rb
module Mavericks
class Relation
def initialize(klass)
@klass = klass
@where_values = []
end
def to_sql
sql = "SELECT * FROM #{@klass.table_name}"
if @where_values.any?
sql += " WHERE " + @where_values.join(' AND ')
end
sql
end
def to_sql_text(val)
case val
when Numeric
val.to_s
when String
"'#{val}'"
else
raise "Can't support #{val.class} to SQL!"
end
end
def records
@records ||= @klass.find_by_sql(to_sql)
end
def where(sql_syntax)
if sql_syntax.class == Hash
@where_values += sql_syntax.map { |key, val| "#{key.to_s} = #{self.to_sql_text(val)}" }
else
@where_values += [sql_syntax]
end
self
end
def first
records.first
end
def each(&block)
records.each(&block)
end
end
end
看一下 where
裡面實作的過程,可以看到我們其實也是用字串吧 where 的查詢語法拼起來,有幾個就拼幾個,這樣就可以一直串下去,一樣用 sqlite_test.rb 來試試看吧
# just_do/sqlite_test.rb
require 'mavericks/data_record'
Mavericks::DataRecord::Base.establish_connection
class Task < Mavericks::DataRecord::Base
end
Task.where(title: '鐵人30').where(content: '每天寫一篇').each do |task|
puts task.title
end