require 'metaid' module Sequel class Model @@db = nil def self.db @db ||= ((superclass != Object) && (superclass.db)) || nil end def self.db=(db); @db = db; end def self.table_name @table_name ||= ((superclass != Model) && (superclass.table_name)) || nil end def self.set_table_name(t); @table_name = t; end def self.dataset return @dataset if @dataset if !table_name raise RuntimeError, "Table name not specified for class #{self}." elsif !db raise RuntimeError, "No database connected." end @dataset = db[table_name] @dataset.record_class = self @dataset end def self.set_dataset(ds) @db = ds.db @dataset = ds @dataset.record_class = self end def self.cache_by(column, expiration) @cache_column = column prefix = "#{name}.#{column}." define_method(:cache_key) do prefix + @values[column].to_s end define_method("find_by_#{column}".to_sym) do |arg| key = cache_key rec = CACHE[key] if !rec rec = find(column => arg) CACHE.set(key, rec, expiration) end rec end alias_method :destroy, :destroy_and_invalidate_cache alias_method :set, :set_and_update_cache end def self.cache_column @cache_column end def self.primary_key; @primary_key ||= :id; end def self.set_primary_key(k); @primary_key = k; end def self.set_schema(name = nil, &block) name ? set_table_name(name) : name = table_name @schema = Schema::Generator.new(name, &block) if @schema.primary_key_name set_primary_key @schema.primary_key_name end end def self.schema @schema || ((superclass != Model) && (superclass.schema)) end def self.table_exists? db.table_exists?(table_name) end def self.create_table db.execute schema.create_sql end def self.drop_table db.execute schema.drop_sql end def self.recreate_table drop_table if table_exists? create_table end ONE_TO_ONE_PROC = "proc {i = @values[:%s]; %s[i] if i}".freeze ID_POSTFIX = "_id".freeze FROM_DATASET = "db[%s]".freeze def self.one_to_one(name, opts) klass = opts[:class] ? opts[:class] : (FROM_DATASET % name.inspect) key = opts[:key] || (name.to_s + ID_POSTFIX) define_method name, &eval(ONE_TO_ONE_PROC % [key, klass]) end ONE_TO_MANY_PROC = "proc {%s.filter(:%s => @pkey)}".freeze ONE_TO_MANY_ORDER_PROC = "proc {%s.filter(:%s => @pkey).order(%s)}".freeze def self.one_to_many(name, opts) klass = opts[:class] ? opts[:class] : (FROM_DATASET % (opts[:table] || name.inspect)) key = opts[:on] order = opts[:order] define_method name, &eval( (order ? ONE_TO_MANY_ORDER_PROC : ONE_TO_MANY_PROC) % [klass, key, order.inspect] ) end def self.get_hooks(key) @hooks ||= {} @hooks[key] ||= [] end def self.has_hooks?(key) !get_hooks(key).empty? end def run_hooks(key) self.class.get_hooks(key).each {|h| instance_eval(&h)} end def self.before_destroy(&block) get_hooks(:before_destroy).unshift(block) end def self.after_create(&block) get_hooks(:after_create) << block end def self.find(cond) dataset[cond.is_a?(Hash) ? cond : {primary_key => cond}] end class << self; alias_method :[], :find; end ############################################################################ attr_reader :values, :pkey def model self.class end def primary_key self.class.primary_key end def initialize(values) @values = values @pkey = values[self.class.primary_key] end def exists? model.filter(primary_key => @pkey).count == 1 end def refresh record = self.class.find(primary_key => @pkey) record ? (@values = record.values) : (raise RuntimeError, "Record not found") self end def self.each(&block); dataset.each(&block); end def self.all; dataset.all; end def self.filter(*arg); dataset.filter(*arg); end def self.exclude(*arg); dataset.exclude(*arg); end def self.order(*arg); dataset.order(*arg); end def self.first(*arg); dataset.first(*arg); end def self.count; dataset.count; end def self.map(*arg, &block); dataset.map(*arg, &block); end def self.hash_column(column); dataset.hash_column(primary_key, column); end def self.join(*args); dataset.join(*args); end def self.lock(mode, &block); dataset.lock(mode, &block); end def self.destroy_all has_hooks?(:before_destroy) ? dataset.destroy : dataset.delete end def self.delete_all; dataset.delete; end def self.create(values = nil) db.transaction do obj = find(primary_key => dataset.insert(values)) obj.run_hooks(:after_create) obj end end def destroy db.transaction do run_hooks(:before_destroy) delete end end def delete model.dataset.filter(primary_key => @pkey).delete end FIND_BY_REGEXP = /^find_by_(.*)/.freeze FILTER_BY_REGEXP = /^filter_by_(.*)/.freeze ALL_BY_REGEXP = /^all_by_(.*)/.freeze def self.method_missing(m, *args) Thread.exclusive do method_name = m.to_s if method_name =~ FIND_BY_REGEXP c = $1 meta_def(method_name) {|arg| find(c => arg)} elsif method_name =~ FILTER_BY_REGEXP c = $1 meta_def(method_name) {|arg| filter(c => arg)} elsif method_name =~ ALL_BY_REGEXP c = $1 meta_def(method_name) {|arg| filter(c => arg).all} end end respond_to?(m) ? send(m, *args) : super(m, *args) end def db; self.class.db; end def reload temp = self.class[@pkey] @values = self.class.dataset.naked[primary_key => @pkey] end def [](field); @values[field]; end def []=(field, value); @values[field] = value; end WRITE_ATTR_REGEXP = /(.*)=$/.freeze def method_missing(m, value = nil) if m.to_s =~ WRITE_ATTR_REGEXP self[$1.to_sym] = value else self[m] end end def id; @values[:id]; end def save if @pkey model.dataset.filter(primary_key => @pkey).update(@values) else @pkey = model.dataset.insert(@values) reload end end def ==(obj) (obj.class == model) && (obj.values == @values) end def set(values) model.dataset.filter(primary_key => @pkey).update(values) @values.merge!(values) end end def self.Model(table) Class.new(Sequel::Model) do meta_def(:inherited) do |c| if table.is_a?(Dataset) c.set_dataset(table) else c.set_table_name(table) end end end end end