module Sequel class Model attr_reader :values attr_reader :changed_columns # Returns value of attribute. def [](column) @values[column] end # Sets value of attribute and marks the column as changed. def []=(column, value) @values[column] = value @changed_columns << column unless @changed_columns.include?(column) end # Enumerates through all attributes. # # === Example: # Ticket.find(7).each { |k, v| puts "#{k} => #{v}" } def each(&block) @values.each(&block) end # Returns attribute names. def keys @values.keys end # Returns value for :id attribute. def id @values[:id] end # Compares model instances by values. def ==(obj) (obj.class == model) && (obj.values == @values) end # Compares model instances by pkey. def ===(obj) (obj.class == model) && (obj.pk == pk) end # Returns key for primary key. def self.primary_key :id end # Returns primary key attribute hash. def self.primary_key_hash(value) {:id => value} end # Sets primary key, regular and composite are possible. # # == Example: # class Tagging < Sequel::Model # # composite key # set_primary_key :taggable_id, :tag_id # end # # class Person < Sequel::Model # # regular key # set_primary_key :person_id # end # # You can even set it to nil! def self.set_primary_key(*key) # if k is nil, we go to no_primary_key if key.empty? || (key.size == 1 && key.first == nil) return no_primary_key end # backwards compat key = (key.length == 1) ? key[0] : key.flatten # redefine primary_key meta_def(:primary_key) {key} unless key.is_a? Array # regular primary key class_def(:this) do @this ||= dataset.filter(key => @values[key]).limit(1).naked end class_def(:pk) do @pk ||= @values[key] end class_def(:pk_hash) do @pk ||= {key => @values[key]} end class_def(:cache_key) do pk = @values[key] || (raise Error, 'no primary key for this record') @cache_key ||= "#{self.class}:#{pk}" end meta_def(:primary_key_hash) do |v| {key => v} end else # composite key exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"} block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}") class_def(:this, &block) exp_list = key.map {|k| "@values[#{k.inspect}]"} block = eval("proc {@pk ||= [#{exp_list.join(',')}]}") class_def(:pk, &block) exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"} block = eval("proc {@this ||= {#{exp_list.join(',')}}}") class_def(:pk_hash, &block) exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',') block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list) class_def(:cache_key, &block) meta_def(:primary_key_hash) do |v| key.inject({}) {|m, i| m[i] = v.shift; m} end end end def self.no_primary_key #:nodoc: meta_def(:primary_key) {nil} meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"} class_def(:this) {raise Error, "No primary key is associated with this model"} class_def(:pk) {raise Error, "No primary key is associated with this model"} class_def(:pk_hash) {raise Error, "No primary key is associated with this model"} class_def(:cache_key) {raise Error, "No primary key is associated with this model"} end # Creates new instance with values set to passed-in Hash ensuring that # new? returns true. def self.create(values = {}, &block) db.transaction do obj = new(values, true, &block) obj.save obj end end class << self def create_with_params(params) create(params.reject {|k, v| !columns.include?(k.to_sym)}) end alias_method :create_with, :create_with_params end # Returns (naked) dataset bound to current instance. def this @this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked end # Returns a key unique to the underlying record for caching def cache_key pk = @values[:id] || (raise Error, 'no primary key for this record') @cache_key ||= "#{self.class}:#{pk}" end # Returns primary key column(s) for object's Model class. def primary_key @primary_key ||= self.class.primary_key end # Returns the primary key value identifying the model instance. Stock implementation. def pk @pk ||= @values[:id] end # Returns a hash identifying the model instance. Stock implementation. def pk_hash @pk_hash ||= {:id => @values[:id]} end # Creates new instance with values set to passed-in Hash. # # This method guesses whether the record exists when # new_record is set to false. def initialize(values = {}, new_record = false, &block) @values = values || {} @changed_columns = [] @new = new_record unless @new # determine if it's a new record k = self.class.primary_key # if there's no primary key for the model class, or # @values doesn't contain a primary key value, then # we regard this instance as new. @new = (k == nil) || (!(Array === k) && !@values[k]) end block[self] if block after_initialize end # Returns true if the current instance represents a new record. def new? @new end alias :new_record? :new? # Returns true when current instance exists, false otherwise. def exists? this.count > 0 end # Creates or updates the associated record. This method can also # accept a list of specific columns to update. def save(*columns) before_save if @new before_create iid = model.dataset.insert(@values) # if we have a regular primary key and it's not set in @values, # we assume it's the last inserted id if (pk = primary_key) && !(Array === pk) && !@values[pk] @values[pk] = iid end if pk @this = nil # remove memoized this dataset refresh end @new = false after_create else before_update if columns.empty? this.update(@values) @changed_columns = [] else # update only the specified columns this.update(@values.reject {|k, v| !columns.include?(k)}) @changed_columns.reject! {|c| columns.include?(c)} end after_update end after_save self end # Saves only changed columns or does nothing if no columns are marked as # chanaged. def save_changes save(*@changed_columns) unless @changed_columns.empty? end # Updates and saves values to database from the passed-in Hash. def set(values) this.update(values) values.each {|k, v| @values[k] = v} end alias_method :update, :set # Reloads values from database and returns self. def refresh @values = this.first || raise(Error, "Record not found") self end alias_method :reload, :refresh # Like delete but runs hooks before and after delete. def destroy db.transaction do before_destroy delete after_destroy end end # Deletes and returns self. def delete this.delete self end ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze def method_missing(m, *args) #:nodoc: if m.to_s =~ ATTR_RE att = $1.to_sym write = $2 == '=' # check whether the column is legal unless columns.include?(att) # if read accessor and a value exists for the column, we return it if !write && @values.has_key?(att) return @values[att] end # otherwise, raise an error raise Error, "Invalid column (#{att.inspect}) for #{self}" end # define the column accessor Thread.exclusive do if write model.class_def(m) {|v| self[att] = v} else model.class_def(m) {self[att]} end end # call the accessor respond_to?(m) ? send(m, *args) : super(m, *args) else super(m, *args) end end end end