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)
# If it is new, it doesn't have a value yet, so we should
# definitely set the new value.
# If the column isn't in @values, we can't assume it is
# NULL in the database, so assume it has changed.
if new? || !@values.include?(column) || value != @values[column]
@changed_columns << column unless @changed_columns.include?(column)
@values[column] = value
end
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
alias_method :eql?, :"=="
# If pk is not nil, true only if the objects have the same class and pk.
# If pk is nil, false.
def ===(obj)
pk.nil? ? false : (obj.class == model) && (obj.pk == pk)
end
# Unique for objects with the same class and pk (if pk is not nil), or
# the same class and values (if pk is nil).
def hash
[model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
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, saves it
# (running any callbacks), and returns the instance.
def self.create(values = {}, &block)
obj = new(values, &block)
obj.save
obj
end
# Updates the instance with the supplied values with support for virtual
# attributes, ignoring any values for which no setter method is available.
# Does not save the record.
#
# If no columns have been set for this model (very unlikely), assume symbol
# keys are valid column names, and assign the column value based on that.
def set_with_params(hash)
columns_not_set = !model.instance_variable_get(:@columns)
meths = setter_methods
hash.each do |k,v|
m = "#{k}="
if meths.include?(m)
send(m, v)
elsif columns_not_set && (Symbol === k)
self[k] = v
end
end
end
# Runs set_with_params and saves the changes (which runs any callback methods).
def update_with_params(values)
set_with_params(values)
save_changes
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. If the
# model's primary key is changed (using #set_primary_key or #no_primary_key)
# this method is redefined accordingly.
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 = nil, from_db = false, &block)
values ||= {}
@changed_columns = []
if from_db
@new = false
@values = values
else
@values = {}
@new = true
set_with_params(values)
end
@changed_columns.clear
yield self if block
after_initialize
end
# Initializes a model instance as an existing record. This constructor is
# used by Sequel to initialize model instances when fetching records.
def self.load(values)
new(values, true)
end
# Returns true if the current instance represents a new record.
def new?
@new
end
# 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
# Sets the value attributes without saving the record. Returns
# the values changed. Raises an error if the keys are not symbols
# or strings or a string key was passed that was not a valid column.
# This is a low level method that does not respect virtual attributes. It
# should probably be avoided. Look into using set_with_params instead.
def set_values(values)
s = str_columns
vals = values.inject({}) do |m, kv|
k, v = kv
k = case k
when Symbol
k
when String
# Prevent denial of service via memory exhaustion by only
# calling to_sym if the symbol already exists.
raise(::Sequel::Error, "all string keys must be a valid columns") unless s.include?(k)
k.to_sym
else
raise(::Sequel::Error, "Only symbols and strings allows as keys")
end
m[k] = v
m
end
vals.each {|k, v| @values[k] = v}
vals
end
# Sets the values attributes with set_values and then updates
# the record in the database using those values. This is a
# low level method that does not run the usual save callbacks.
# It should probably be avoided. Look into using update_with_params instead.
def update_values(values)
this.update(set_values(values))
end
# Reloads values from database and returns self.
def refresh
@values = this.first || raise(Error, "Record not found")
model.all_association_reflections.each do |r|
instance_variable_set("@#{r[:name]}", nil)
end
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
self
end
# Deletes and returns self. Does not run callbacks.
# Look into using destroy instead.
def delete
this.delete
self
end
private
# Returns all methods that can be used for attribute
# assignment (those that end with =)
def setter_methods
methods.grep(/=\z/)
end
end
end