前面 19 天我們寫了很多 code,但你會發現我們 lib
資料夾底下很亂,這是目前裡面所包含的東西
.
├── lib
│ ├── mavericks
│ │ ├── controller.rb
│ │ ├── data_record
│ │ │ ├── base.rb
│ │ │ ├── connection_adapter.rb
│ │ │ ├── method.rb
│ │ │ ├── persistence.rb
│ │ │ └── relation.rb
│ │ ├── data_record.rb
│ │ ├── dependencies.rb
│ │ ├── file_model.rb
│ │ ├── routing.rb
│ │ ├── sqlite_model.rb
│ │ ├── support.rb
│ │ └── version.rb
│ └── mavericks.rb
如果想要做一個框架出來,除了達到框架需要的功能以外,如何寫一個容易維護和擴充的程式碼也是相當重要,所以接下來會邊重構邊增加一些功能,實務上對於重構應該要搭配測試一起進行才對,不過這裡就不針對測試那塊著墨,有興趣的人可以自行搭配測試
我們今天的目標就是將 data_record.rb
改成 active_record.rb
版本
原先的 data_record.rb
# mavericks/lib/mavericks/data_record.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
會改成像是這樣
# mavericks/lib/active_record.rb
module ActiveRecord
autoload :Mavericks, "mavericks/support"
autoload :Base, "active_record/base"
autoload :Persistence, "active_record/persistence"
autoload :ConnectionAdapter, "active_record/connection_adapter"
autoload :Relation, "active_record/relation"
end
會發現我們將 active_record
拉到跟 mavericks
一樣的層級,希望之後 active_record
也能獨立成一個套件,而這裡我們用 autoload
的好處是,如果有用到這個常數,才會把檔案給 load
進來,例如我們有用到 ActiveRecord::Base
,才會載入 active_record/base.rb
而在 active_record
裡面我們會暫時用到 mavericks/support
來處理載入檔案的問題,但其實這做法並不好,因為我們原先的目的就是要把 active_record
獨立出來,不過現在先暫時這樣做,之後我們會在重新整理
接著修改 base.rb
# mavericks/lib/mavericks/data_record/base.rb
module Mavericks
module DataRecord
class Base
include Persistence
extend Method
end
end
end
我們原先的 base.rb
長這樣,用二分法將 class method
和 instance method
做區隔,但這做法並不好,應該用 功能
做為 module 分開才對,這樣才能達到 reuse
的效果
# mavericks/lib/active_record/base.rb
require 'yaml'
module ActiveRecord
class Base
include Persistence
def initialize(attributes = {})
self.class.set_column_to_attribute
@attributes = attributes
@new_record = true
end
def new_record?
@new_record
end
def self.establish_connection
raw = File.read('config/database.yml')
database_config = YAML.safe_load(raw)
case database_config['default']['adapter']
when 'postgresql'
@@connection = ConnectionAdapter::PostgreSQLAdapter.new(database_config['development']['database'])
when 'sqlite'
@@connection = ConnectionAdapter::SQLiteAdapter.new(database_config['development']['database'])
end
end
def self.connection
@@connection
end
def self.set_column_to_attribute
self.connection.schema(self.table_name).each{ |column| self.define_method_attribute(column) }
end
def self.define_method_attribute(name)
class_eval <<-STR
def #{name}
@attributes[:#{name}] || @attributes["#{name}"]
end
def #{name}=(value)
@attributes[:#{name}] = value
end
STR
end
def self.table_name
singular_table_name = Mavericks.to_underscore name
Mavericks.to_plural singular_table_name
end
def self.count
self.connection.execute(<<-SQL)[0]['count']
SELECT COUNT(*) as count FROM #{self.table_name}
SQL
end
def self.to_sql(val)
case val
when Numeric
val.to_s
when String
"'#{val}'"
else
raise "Can't support #{val.class} to SQL!"
end
end
def self.all
Relation.new(self).records
end
def self.last
all.last
end
def self.find(id)
find_by_sql("SELECT * FROM #{self.table_name} WHERE id = #{id.to_i}").first
end
def self.find_by_sql(sql)
connection.execute(sql).map do |attributes|
new(attributes)
end
end
def self.where(query)
sql_syntax = query.map do |key, val|
"#{key.to_s} = #{self.to_sql(val)}"
end
Relation.new(self).where(sql_syntax)
end
end
end
我們將 class method 都搬到 base.rb
底下,並且把 sqlite_model.rb
跟 data_record.rb
的程式碼整理成一起,會發現 self.establish_connection
已經可以同時支援兩種資料庫
在 connection_adapter.rb
我們也實作了兩種資料庫的連線方式
# mavericks/lib/active_record/connection_adapter.rb
module ActiveRecord
module ConnectionAdapter
class PostgreSQLAdapter
def initialize(dbname)
require 'pg'
@db = PG.connect(dbname: dbname)
end
def execute(sql)
@db.exec(sql)
end
def schema(table_name)
self.execute("SELECT column_name FROM information_schema.columns
WHERE table_name= '#{table_name}'").map{|m| m["column_name"]}
end
end
class SQLiteAdapter
def initialize(dbname)
require 'sqlite3'
@db = SQLite3::Database.new dbname
end
def execute(sql)
@db.results_as_hash = true
@db.execute(sql)
end
def schema(table_name)
@db.table_info(table_name).map{ |row| row["name"] }
end
end
end
end
persistence.rb
比較單純,只有處理資料儲存部分
# mavericks/lib/active_record/persistence.rb
module ActiveRecord
module Persistence
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
relation.rb
則保持一樣沒什麼變
# mavericks/lib/active_record/relation.rb
module ActiveRecord
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 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} = #{@klass.to_sql(val)}" }
else
@where_values += [sql_syntax]
end
self
end
def first
records.first
end
def each(&block)
records.each(&block)
end
end
end
最後 active_record 的資料夾結構會長這樣
├── lib
│ ├── active_record
│ │ ├── base.rb
│ │ ├── connection_adapter.rb
│ │ ├── persistence.rb
│ │ └── relation.rb
│ ├── active_record.rb
接著我們要用的時候,只要直接繼承 ActiveRecord::Base
就可以了
# just_do/sqlite_test.rb
require 'active_record'
ActiveRecord::Base.establish_connection
class Task < ActiveRecord::Base
end
Task.new(title: '鐵人30', content: '每天寫一篇').save
puts Task.all.last.title
puts Task.count
Task.where(title: '鐵人30').where(content: '每天寫一篇').each do |task|
puts task.title
end
今天大部分做得事情都是重構,程式的細節在前面幾天都有解說過,這裡就不再花費篇幅
另外因為發現程式碼越來越多,所以從第 20 天開始,會將程式碼更新在 github 上,供大家參考
https://github.com/apayu/mavericks