Model hack for Sequel
Sequel is sweet. Those of us, who do not want a beast like ActiveRecord in their hair pin sized projects, its a godsend. Now, as you hack away at your application you will soon realize that, perhaps ActiveRecord models was also not bad. It gives very good code separation.
So, I wrote a stupid ugly hack that implements a model around Sequel. Now Sequel already provides one and you may ask why reinvent. It was mainly because I was bamboozled by their model implementation and couldn’t understand it mostly.
I have added couple of thingies to ruby core and they are neatly tucked away in a file called “ruby_hacks.rb”. Let me first show that, since its essential for our implementation:
# ruby_hacks.rb
class Hash
def assert_valid_keys(*valid_keys)
unknown_keys = keys - [valid_keys].flatten
unless unknown_keys.empty?
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}")
end
end
end
class Object
def deep_copy
Marshal.load(Marshal.dump(self))
end
def nothing?
if respond_to?(:empty?) && respond_to?(:strip)
empty? or strip.empty?
elsif respond_to?(:empty?)
empty?
elsif respond_to?(:zero?)
zero?
else
!self
end
end
def self.metaclass; class << self; self; end; end
def self.iattr_accessor *args
metaclass.instance_eval do
attr_accessor *args
end
args.each do |attr|
class_eval do
define_method(attr) do
self.class.send(attr)
end
define_method("#{attr}=") do |b_value|
self.class.send("#{attr}=",b_value)
end
end
end
end
end
class Array
def extract_options!
last.is_a?(::Hash) ? pop : {}
end
end
I hope above code is nothing fancy. iattr_accessor implements accessors around class instance variables.
Now lets implement, base class of our model, which will be inherited by all models:
# FIXME: someday it will be good to have method_missing hack, but essentianlly that would
# require some assumptions about database schema.
class DbConnection
iattr_accessor :connection
iattr_accessor :table_name
VALID_FIND_OPTIONS = [:conditions, :include, :joins, :limit, :offset,:filter,:order, :select, :readonly, :group, :from, :lock ]
def self.establish_connection(args = nil)
options = { }
if args.blank?
options[:username] = ServerConfig['database']['username']
options[:password] = ServerConfig['database']['password']
options[:host] = ServerConfig['database']['host']
options[:database] = ServerConfig['database']['database']
options[:port] = ServerConfig['database']['port']
elsif(args.is_a?(Symbol) or args.is_a? String)
yml_string = args.to_s
options[:username] = ServerConfig[yml_string]['username']
options[:password] = ServerConfig[yml_string]['password']
options[:host] = ServerConfig[yml_string]['host']
options[:database] = ServerConfig[yml_string]['database']
options[:port] = ServerConfig[yml_string]['port']
end
unless options[:port]
@connection =
Sequel.open("mysql://#{options[:username]}:#{options[:password]}@#{options[:host]}/#{options[:database]}")
else
@connection =
Sequel.open("mysql://#{options[:username]}:#{options[:password]}@#{options[:host]}:#{options[:port]}/#{options[:database]}")
end
end
establish_connection
# if there is no connection object in current class go for parent class connection object
def self.connection; @connection || self.superclass.connection; end
class << self
def set_table_name p_table_name
@table_name = p_table_name.to_s
end
def execute p_sql_query
connection.execute(p_sql_query)
end
def find(*args)
options = args.extract_options!
validate_find_options(options)
case args.first
when :first: find_first(options)
when :all: find_all(options)
else raise "Invalid find"
end
end
def validate_find_options(options) #:nodoc:
options.assert_valid_keys(VALID_FIND_OPTIONS)
end
def find_first(options)
t_records = connection[table_name].filter(options[:filter]).limit(1)
t_records[1] rescue nil
end
def find_all(options)
connection[table_name].filter(options[:filter])
end
end
end
Now lets see couple of sample model implementations:
# user.rb
class User < DbConnection
set_table_name :users
def self.find_by_auth_id p_auth_id
find(:first,:filter => { :feed_key => p_auth_id})
end
end
Another one, where connection handler is overrideen:
class Ticker < DbConnection
establish_connection :platform_db
set_table_name :ubac_sym
def self.symbol_search p_symbol
find(:first,:filter => { :Symbol => p_symbol })
end
end