sequel 1.3 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +127 -0
- data/COPYING +1 -0
- data/README +5 -4
- data/Rakefile +78 -25
- data/lib/sequel.rb +1 -2
- data/lib/sequel_model.rb +324 -0
- data/lib/sequel_model/associations.rb +351 -0
- data/lib/sequel_model/base.rb +120 -0
- data/lib/sequel_model/caching.rb +42 -0
- data/lib/sequel_model/eager_loading.rb +169 -0
- data/lib/sequel_model/hooks.rb +55 -0
- data/lib/sequel_model/plugins.rb +47 -0
- data/lib/sequel_model/pretty_table.rb +73 -0
- data/lib/sequel_model/record.rb +336 -0
- data/lib/sequel_model/schema.rb +48 -0
- data/lib/sequel_model/validations.rb +15 -0
- data/spec/associations_spec.rb +712 -0
- data/spec/base_spec.rb +239 -0
- data/spec/caching_spec.rb +150 -0
- data/spec/deprecated_relations_spec.rb +153 -0
- data/spec/eager_loading_spec.rb +260 -0
- data/spec/hooks_spec.rb +269 -0
- data/spec/model_spec.rb +543 -0
- data/spec/plugins_spec.rb +74 -0
- data/spec/rcov.opts +4 -0
- data/spec/record_spec.rb +593 -0
- data/spec/schema_spec.rb +69 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/validations_spec.rb +246 -0
- metadata +90 -56
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
def self.set_cache(store, opts = {})
|
4
|
+
@cache_store = store
|
5
|
+
if (ttl = opts[:ttl])
|
6
|
+
set_cache_ttl(ttl)
|
7
|
+
end
|
8
|
+
|
9
|
+
meta_def(:[]) do |*args|
|
10
|
+
if (args.size == 1) && (Hash === (h = args.first))
|
11
|
+
return dataset[h]
|
12
|
+
end
|
13
|
+
|
14
|
+
unless obj = @cache_store.get(cache_key_from_values(args))
|
15
|
+
obj = dataset[primary_key_hash((args.size == 1) ? args.first : args)]
|
16
|
+
@cache_store.set(cache_key_from_values(args), obj, cache_ttl)
|
17
|
+
end
|
18
|
+
obj
|
19
|
+
end
|
20
|
+
|
21
|
+
class_def(:set) {|v| store.delete(cache_key); super}
|
22
|
+
class_def(:save) {store.delete(cache_key); super}
|
23
|
+
class_def(:delete) {store.delete(cache_key); super}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.set_cache_ttl(ttl)
|
27
|
+
@cache_ttl = ttl
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.cache_store
|
31
|
+
@cache_store
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.cache_ttl
|
35
|
+
@cache_ttl ||= 3600
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cache_key_from_values(values)
|
39
|
+
"#{self}:#{values.join(',')}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Eager loading makes it so that you can load all associated records for a
|
2
|
+
# set of objects in a single query, instead of a separate query for each object.
|
3
|
+
#
|
4
|
+
# The basic idea for how it works is that the dataset is first loaded normally.
|
5
|
+
# Then it goes through all associations that have been specified via .eager.
|
6
|
+
# It loads each of those associations separately, then associates them back
|
7
|
+
# to the original dataset via primary/foreign keys. Due to the necessity of
|
8
|
+
# all objects being present, you need to use .all to use eager loading, as it
|
9
|
+
# can't work with .each.
|
10
|
+
#
|
11
|
+
# This implementation avoids the complexity of extracting an object graph out
|
12
|
+
# of a single dataset, by building the object graph out of multiple datasets,
|
13
|
+
# one for each association. By using a separate dataset for each association,
|
14
|
+
# it avoids problems such as aliasing conflicts and creating cartesian product
|
15
|
+
# result sets if multiple *_to_many eager associations are requested.
|
16
|
+
#
|
17
|
+
# One limitation of using this method is that you cannot filter the dataset
|
18
|
+
# based on values of columns in an associated table, since the associations are loaded
|
19
|
+
# in separate queries. To do that you need to load all associations in the
|
20
|
+
# same query, and extract an object graph from the results of that query.
|
21
|
+
#
|
22
|
+
# You can cascade the eager loading (loading associations' associations)
|
23
|
+
# with no limit to the depth of the cascades. You do this by passing a hash to .eager
|
24
|
+
# with the keys being associations of the current model and values being
|
25
|
+
# associations of the model associated with the current model via the key.
|
26
|
+
#
|
27
|
+
# The associations' order, if defined, is respected. You cannot eagerly load
|
28
|
+
# an association with a block argument, as the block argument is evaluated in
|
29
|
+
# terms of a specific instance of the model, and no specific instance exists
|
30
|
+
# when eagerly loading.
|
31
|
+
module Sequel::Model::Associations::EagerLoading
|
32
|
+
# Add associations to the list of associations to eagerly load.
|
33
|
+
# Associations can be a symbol or a hash with symbol keys (for cascaded
|
34
|
+
# eager loading). Examples:
|
35
|
+
#
|
36
|
+
# Album.eager(:artist).all
|
37
|
+
# Album.eager(:artist, :genre).all
|
38
|
+
# Album.eager(:artist).eager(:genre).all
|
39
|
+
# Artist.eager(:albums=>:tracks).all
|
40
|
+
# Artist.eager(:albums=>{:tracks=>:genre}).all
|
41
|
+
def eager(*associations)
|
42
|
+
raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
|
43
|
+
opt = @opts[:eager]
|
44
|
+
opt = opt ? opt.dup : {}
|
45
|
+
check = Proc.new do |a|
|
46
|
+
raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(a)
|
47
|
+
raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
|
48
|
+
end
|
49
|
+
associations.flatten.each do |association|
|
50
|
+
case association
|
51
|
+
when Symbol
|
52
|
+
check.call(association)
|
53
|
+
opt[association] = nil
|
54
|
+
when Hash
|
55
|
+
association.keys.each{|assoc| check.call(assoc)}
|
56
|
+
opt.merge!(association)
|
57
|
+
else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
ds = clone(:eager=>opt)
|
61
|
+
ds.add_callback(:post_load, :eager_load) unless @opts[:eager]
|
62
|
+
ds
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# Eagerly load all specified associations
|
67
|
+
def eager_load(a)
|
68
|
+
return if a.empty?
|
69
|
+
# Current model class
|
70
|
+
model = @opts[:models][nil]
|
71
|
+
# All associations to eager load
|
72
|
+
eager_assoc = @opts[:eager]
|
73
|
+
# Key is foreign/primary key name symbol
|
74
|
+
# Value is hash with keys being foreign/primary key values (generally integers)
|
75
|
+
# and values being an array of current model objects with that
|
76
|
+
# specific foreign/primary key
|
77
|
+
key_hash = {}
|
78
|
+
# array of attribute_values keys to monitor
|
79
|
+
keys = []
|
80
|
+
# Reflections for all associations to eager load
|
81
|
+
reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}
|
82
|
+
|
83
|
+
# Populate keys to monitor
|
84
|
+
reflections.each do |reflection|
|
85
|
+
key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key
|
86
|
+
next if key_hash[key]
|
87
|
+
key_hash[key] = {}
|
88
|
+
keys << key
|
89
|
+
end
|
90
|
+
|
91
|
+
# Associate each object with every key being monitored
|
92
|
+
a.each do |r|
|
93
|
+
keys.each do |key|
|
94
|
+
((key_hash[key][r[key]] ||= []) << r) if r[key]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Iterate through eager associations and assign instance variables
|
99
|
+
# for the association for all model objects
|
100
|
+
reflections.each do |reflection|
|
101
|
+
assoc_class = model.send(:associated_class, reflection)
|
102
|
+
assoc_name = reflection[:name]
|
103
|
+
# Proc for setting cascaded eager loading
|
104
|
+
cascade = Proc.new do |d|
|
105
|
+
if c = eager_assoc[assoc_name]
|
106
|
+
d = d.eager(c)
|
107
|
+
end
|
108
|
+
if c = reflection[:eager]
|
109
|
+
d = d.eager(c)
|
110
|
+
end
|
111
|
+
d
|
112
|
+
end
|
113
|
+
case rtype = reflection[:type]
|
114
|
+
when :many_to_one
|
115
|
+
key = reflection[:key]
|
116
|
+
h = key_hash[key]
|
117
|
+
keys = h.keys
|
118
|
+
# No records have the foreign key set for this association, so skip it
|
119
|
+
next unless keys.length > 0
|
120
|
+
ds = assoc_class.filter(assoc_class.primary_key=>keys)
|
121
|
+
ds = cascade.call(ds)
|
122
|
+
ds.all do |assoc_object|
|
123
|
+
h[assoc_object.pk].each do |object|
|
124
|
+
object.instance_variable_set(:"@#{assoc_name}", assoc_object)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
when :one_to_many, :many_to_many
|
128
|
+
if rtype == :one_to_many
|
129
|
+
fkey = key = reflection[:key]
|
130
|
+
h = key_hash[model.primary_key]
|
131
|
+
reciprocal = model.send(:reciprocal_association, reflection)
|
132
|
+
ds = assoc_class.filter(key=>h.keys)
|
133
|
+
else
|
134
|
+
assoc_table = assoc_class.table_name
|
135
|
+
left = reflection[:left_key]
|
136
|
+
right = reflection[:right_key]
|
137
|
+
right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
|
138
|
+
join_table = reflection[:join_table]
|
139
|
+
fkey = (opts[:left_key_alias] ||= :"x_foreign_key_x")
|
140
|
+
table_selection = (opts[:select] ||= assoc_table.all)
|
141
|
+
key_selection = (opts[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
|
142
|
+
h = key_hash[model.primary_key]
|
143
|
+
ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
|
144
|
+
end
|
145
|
+
if order = reflection[:order]
|
146
|
+
ds = ds.order(order)
|
147
|
+
end
|
148
|
+
ds = cascade.call(ds)
|
149
|
+
ivar = :"@#{assoc_name}"
|
150
|
+
h.values.each do |object_array|
|
151
|
+
object_array.each do |object|
|
152
|
+
object.instance_variable_set(ivar, [])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
ds.all do |assoc_object|
|
156
|
+
fk = if rtype == :many_to_many
|
157
|
+
assoc_object.values.delete(fkey)
|
158
|
+
else
|
159
|
+
assoc_object[fkey]
|
160
|
+
end
|
161
|
+
h[fk].each do |object|
|
162
|
+
object.instance_variable_get(ivar) << assoc_object
|
163
|
+
assoc_object.instance_variable_set(reciprocal, object) if reciprocal
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
HOOKS = [
|
4
|
+
:after_initialize,
|
5
|
+
:before_create,
|
6
|
+
:after_create,
|
7
|
+
:before_update,
|
8
|
+
:after_update,
|
9
|
+
:before_save,
|
10
|
+
:after_save,
|
11
|
+
:before_destroy,
|
12
|
+
:after_destroy
|
13
|
+
]
|
14
|
+
|
15
|
+
# Some fancy code generation here in order to define the hook class methods...
|
16
|
+
HOOK_METHOD_STR = %Q{
|
17
|
+
def self.%s(method = nil, &block)
|
18
|
+
unless block
|
19
|
+
(raise SequelError, 'No hook method specified') unless method
|
20
|
+
block = proc {send method}
|
21
|
+
end
|
22
|
+
add_hook(%s, &block)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
def self.def_hook_method(m) #:nodoc:
|
27
|
+
instance_eval(HOOK_METHOD_STR % [m.to_s, m.inspect])
|
28
|
+
end
|
29
|
+
|
30
|
+
HOOKS.each {|h| define_method(h) {}}
|
31
|
+
HOOKS.each {|h| def_hook_method(h)}
|
32
|
+
|
33
|
+
# Returns the hooks hash for the model class.
|
34
|
+
def self.hooks #:nodoc:
|
35
|
+
@hooks ||= Hash.new {|h, k| h[k] = []}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.add_hook(hook, &block) #:nodoc:
|
39
|
+
chain = hooks[hook]
|
40
|
+
chain << block
|
41
|
+
define_method(hook) do
|
42
|
+
return false if super == false
|
43
|
+
chain.each {|h| break false if instance_eval(&h) == false}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if the model class or any of its ancestors have defined
|
48
|
+
# hooks for the given hook key. Notice that this method cannot detect
|
49
|
+
# hooks defined using overridden methods.
|
50
|
+
def self.has_hooks?(key)
|
51
|
+
has = hooks[key] && !hooks[key].empty?
|
52
|
+
has || ((self != Model) && superclass.has_hooks?(key))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins; end
|
3
|
+
|
4
|
+
class Model
|
5
|
+
class << self
|
6
|
+
# Loads a plugin for use with the model class, passing optional arguments
|
7
|
+
# to the plugin.
|
8
|
+
def is(plugin, *args)
|
9
|
+
m = plugin_module(plugin)
|
10
|
+
if m.respond_to?(:apply)
|
11
|
+
m.apply(self, *args)
|
12
|
+
end
|
13
|
+
if m.const_defined?("InstanceMethods")
|
14
|
+
class_def(:"#{plugin}_opts") {args.first}
|
15
|
+
include(m::InstanceMethods)
|
16
|
+
end
|
17
|
+
if m.const_defined?("ClassMethods")
|
18
|
+
meta_def(:"#{plugin}_opts") {args.first}
|
19
|
+
metaclass.send(:include, m::ClassMethods)
|
20
|
+
end
|
21
|
+
if m.const_defined?("DatasetMethods")
|
22
|
+
unless @dataset
|
23
|
+
raise Sequel::Error, "Plugin cannot be applied because the model class has no dataset"
|
24
|
+
end
|
25
|
+
dataset.meta_def(:"#{plugin}_opts") {args.first}
|
26
|
+
dataset.metaclass.send(:include, m::DatasetMethods)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_method :is_a, :is
|
30
|
+
|
31
|
+
# Returns the module for the specified plugin. If the module is not
|
32
|
+
# defined, the corresponding plugin gem is automatically loaded.
|
33
|
+
def plugin_module(plugin)
|
34
|
+
module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
|
35
|
+
if not Sequel::Plugins.const_defined?(module_name)
|
36
|
+
require plugin_gem(plugin)
|
37
|
+
end
|
38
|
+
Sequel::Plugins.const_get(module_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the gem name for the given plugin.
|
42
|
+
def plugin_gem(plugin)
|
43
|
+
"sequel_#{plugin}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Sequel
|
2
|
+
# Prints nice-looking plain-text tables
|
3
|
+
# +--+-------+
|
4
|
+
# |id|name |
|
5
|
+
# |--+-------|
|
6
|
+
# |1 |fasdfas|
|
7
|
+
# |2 |test |
|
8
|
+
# +--+-------+
|
9
|
+
module PrettyTable
|
10
|
+
def self.records_columns(records)
|
11
|
+
columns = []
|
12
|
+
records.each do |r|
|
13
|
+
if Array === r && (k = r.keys)
|
14
|
+
return k
|
15
|
+
elsif Hash === r
|
16
|
+
r.keys.each {|k| columns << k unless columns.include?(k)}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
columns
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.column_sizes(records, columns)
|
23
|
+
sizes = Hash.new {0}
|
24
|
+
columns.each do |c|
|
25
|
+
s = c.to_s.size
|
26
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
27
|
+
end
|
28
|
+
records.each do |r|
|
29
|
+
columns.each do |c|
|
30
|
+
s = r[c].to_s.size
|
31
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
sizes
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.separator_line(columns, sizes)
|
38
|
+
l = ''
|
39
|
+
'+' + columns.map {|c| '-' * sizes[c]}.join('+') + '+'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.format_cell(size, v)
|
43
|
+
case v
|
44
|
+
when Bignum, Fixnum
|
45
|
+
"%#{size}d" % v
|
46
|
+
when Float
|
47
|
+
"%#{size}g" % v
|
48
|
+
else
|
49
|
+
"%-#{size}s" % v.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.data_line(columns, sizes, record)
|
54
|
+
'|' + columns.map {|c| format_cell(sizes[c], record[c])}.join('|') + '|'
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.header_line(columns, sizes)
|
58
|
+
'|' + columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') + '|'
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.print(records, columns = nil) # records is an array of hashes
|
62
|
+
columns ||= records_columns(records)
|
63
|
+
sizes = column_sizes(records, columns)
|
64
|
+
|
65
|
+
puts separator_line(columns, sizes)
|
66
|
+
puts header_line(columns, sizes)
|
67
|
+
puts separator_line(columns, sizes)
|
68
|
+
records.each {|r| puts data_line(columns, sizes, r)}
|
69
|
+
puts separator_line(columns, sizes)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,336 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
attr_reader :values
|
4
|
+
attr_reader :changed_columns
|
5
|
+
|
6
|
+
# Returns value of attribute.
|
7
|
+
def [](column)
|
8
|
+
@values[column]
|
9
|
+
end
|
10
|
+
# Sets value of attribute and marks the column as changed.
|
11
|
+
def []=(column, value)
|
12
|
+
# If it is new, it doesn't have a value yet, so we should
|
13
|
+
# definitely set the new value.
|
14
|
+
# If the column isn't in @values, we can't assume it is
|
15
|
+
# NULL in the database, so assume it has changed.
|
16
|
+
if new? || !@values.include?(column) || value != @values[column]
|
17
|
+
@changed_columns << column unless @changed_columns.include?(column)
|
18
|
+
@values[column] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Enumerates through all attributes.
|
23
|
+
#
|
24
|
+
# === Example:
|
25
|
+
# Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
|
26
|
+
def each(&block)
|
27
|
+
@values.each(&block)
|
28
|
+
end
|
29
|
+
# Returns attribute names.
|
30
|
+
def keys
|
31
|
+
@values.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns value for <tt>:id</tt> attribute.
|
35
|
+
def id
|
36
|
+
@values[:id]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Compares model instances by values.
|
40
|
+
def ==(obj)
|
41
|
+
(obj.class == model) && (obj.values == @values)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Compares model instances by pkey.
|
45
|
+
def ===(obj)
|
46
|
+
(obj.class == model) && (obj.pk == pk)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns key for primary key.
|
50
|
+
def self.primary_key
|
51
|
+
:id
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns primary key attribute hash.
|
55
|
+
def self.primary_key_hash(value)
|
56
|
+
{:id => value}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets primary key, regular and composite are possible.
|
60
|
+
#
|
61
|
+
# == Example:
|
62
|
+
# class Tagging < Sequel::Model
|
63
|
+
# # composite key
|
64
|
+
# set_primary_key :taggable_id, :tag_id
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class Person < Sequel::Model
|
68
|
+
# # regular key
|
69
|
+
# set_primary_key :person_id
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# <i>You can even set it to nil!</i>
|
73
|
+
def self.set_primary_key(*key)
|
74
|
+
# if k is nil, we go to no_primary_key
|
75
|
+
if key.empty? || (key.size == 1 && key.first == nil)
|
76
|
+
return no_primary_key
|
77
|
+
end
|
78
|
+
|
79
|
+
# backwards compat
|
80
|
+
key = (key.length == 1) ? key[0] : key.flatten
|
81
|
+
|
82
|
+
# redefine primary_key
|
83
|
+
meta_def(:primary_key) {key}
|
84
|
+
|
85
|
+
unless key.is_a? Array # regular primary key
|
86
|
+
class_def(:this) do
|
87
|
+
@this ||= dataset.filter(key => @values[key]).limit(1).naked
|
88
|
+
end
|
89
|
+
class_def(:pk) do
|
90
|
+
@pk ||= @values[key]
|
91
|
+
end
|
92
|
+
class_def(:pk_hash) do
|
93
|
+
@pk ||= {key => @values[key]}
|
94
|
+
end
|
95
|
+
class_def(:cache_key) do
|
96
|
+
pk = @values[key] || (raise Error, 'no primary key for this record')
|
97
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
98
|
+
end
|
99
|
+
meta_def(:primary_key_hash) do |v|
|
100
|
+
{key => v}
|
101
|
+
end
|
102
|
+
else # composite key
|
103
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
104
|
+
block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}")
|
105
|
+
class_def(:this, &block)
|
106
|
+
|
107
|
+
exp_list = key.map {|k| "@values[#{k.inspect}]"}
|
108
|
+
block = eval("proc {@pk ||= [#{exp_list.join(',')}]}")
|
109
|
+
class_def(:pk, &block)
|
110
|
+
|
111
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
112
|
+
block = eval("proc {@this ||= {#{exp_list.join(',')}}}")
|
113
|
+
class_def(:pk_hash, &block)
|
114
|
+
|
115
|
+
exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',')
|
116
|
+
block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list)
|
117
|
+
class_def(:cache_key, &block)
|
118
|
+
|
119
|
+
meta_def(:primary_key_hash) do |v|
|
120
|
+
key.inject({}) {|m, i| m[i] = v.shift; m}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.no_primary_key #:nodoc:
|
126
|
+
meta_def(:primary_key) {nil}
|
127
|
+
meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"}
|
128
|
+
class_def(:this) {raise Error, "No primary key is associated with this model"}
|
129
|
+
class_def(:pk) {raise Error, "No primary key is associated with this model"}
|
130
|
+
class_def(:pk_hash) {raise Error, "No primary key is associated with this model"}
|
131
|
+
class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Creates new instance with values set to passed-in Hash ensuring that
|
135
|
+
# new? returns true.
|
136
|
+
def self.create(values = {}, &block)
|
137
|
+
db.transaction do
|
138
|
+
obj = new(values, &block)
|
139
|
+
obj.save
|
140
|
+
obj
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Updates the instance with the supplied values with support for virtual
|
145
|
+
# attributes, ignoring any values for which no setter method is available.
|
146
|
+
def update_with_params(values)
|
147
|
+
c = columns
|
148
|
+
values.each do |k, v| m = :"#{k}="
|
149
|
+
send(m, v) if c.include?(k) || respond_to?(m)
|
150
|
+
end
|
151
|
+
save_changes
|
152
|
+
end
|
153
|
+
alias_method :update_with, :update_with_params
|
154
|
+
|
155
|
+
class << self
|
156
|
+
def create_with_params(params)
|
157
|
+
record = new
|
158
|
+
record.update_with_params(params)
|
159
|
+
record
|
160
|
+
end
|
161
|
+
alias_method :create_with, :create_with_params
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns (naked) dataset bound to current instance.
|
165
|
+
def this
|
166
|
+
@this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns a key unique to the underlying record for caching
|
170
|
+
def cache_key
|
171
|
+
pk = @values[:id] || (raise Error, 'no primary key for this record')
|
172
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns primary key column(s) for object's Model class.
|
176
|
+
def primary_key
|
177
|
+
@primary_key ||= self.class.primary_key
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the primary key value identifying the model instance. If the
|
181
|
+
# model's primary key is changed (using #set_primary_key or #no_primary_key)
|
182
|
+
# this method is redefined accordingly.
|
183
|
+
def pk
|
184
|
+
@pk ||= @values[:id]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns a hash identifying the model instance. Stock implementation.
|
188
|
+
def pk_hash
|
189
|
+
@pk_hash ||= {:id => @values[:id]}
|
190
|
+
end
|
191
|
+
|
192
|
+
# Creates new instance with values set to passed-in Hash.
|
193
|
+
#
|
194
|
+
# This method guesses whether the record exists when
|
195
|
+
# <tt>new_record</tt> is set to false.
|
196
|
+
def initialize(values = nil, from_db = false, &block)
|
197
|
+
@changed_columns = []
|
198
|
+
unless from_db
|
199
|
+
@values = {}
|
200
|
+
if values
|
201
|
+
values.each do |k, v| m = :"#{k}="
|
202
|
+
if respond_to?(m)
|
203
|
+
send(m, v)
|
204
|
+
values.delete(k)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
values.inject(@values) {|m, kv| m[kv[0].to_sym] = kv[1]; m}
|
208
|
+
# @values.merge!(values)
|
209
|
+
end
|
210
|
+
else
|
211
|
+
@values = values || {}
|
212
|
+
end
|
213
|
+
|
214
|
+
k = primary_key
|
215
|
+
@new = !from_db
|
216
|
+
|
217
|
+
block[self] if block
|
218
|
+
after_initialize
|
219
|
+
end
|
220
|
+
|
221
|
+
# Initializes a model instance as an existing record. This constructor is
|
222
|
+
# used by Sequel to initialize model instances when fetching records.
|
223
|
+
def self.load(values)
|
224
|
+
new(values, true)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns true if the current instance represents a new record.
|
228
|
+
def new?
|
229
|
+
@new
|
230
|
+
end
|
231
|
+
alias :new_record? :new?
|
232
|
+
|
233
|
+
# Returns true when current instance exists, false otherwise.
|
234
|
+
def exists?
|
235
|
+
this.count > 0
|
236
|
+
end
|
237
|
+
|
238
|
+
# Creates or updates the associated record. This method can also
|
239
|
+
# accept a list of specific columns to update.
|
240
|
+
def save(*columns)
|
241
|
+
before_save
|
242
|
+
if @new
|
243
|
+
before_create
|
244
|
+
iid = model.dataset.insert(@values)
|
245
|
+
# if we have a regular primary key and it's not set in @values,
|
246
|
+
# we assume it's the last inserted id
|
247
|
+
if (pk = primary_key) && !(Array === pk) && !@values[pk]
|
248
|
+
@values[pk] = iid
|
249
|
+
end
|
250
|
+
if pk
|
251
|
+
@this = nil # remove memoized this dataset
|
252
|
+
refresh
|
253
|
+
end
|
254
|
+
@new = false
|
255
|
+
after_create
|
256
|
+
else
|
257
|
+
before_update
|
258
|
+
if columns.empty?
|
259
|
+
this.update(@values)
|
260
|
+
@changed_columns = []
|
261
|
+
else # update only the specified columns
|
262
|
+
this.update(@values.reject {|k, v| !columns.include?(k)})
|
263
|
+
@changed_columns.reject! {|c| columns.include?(c)}
|
264
|
+
end
|
265
|
+
after_update
|
266
|
+
end
|
267
|
+
after_save
|
268
|
+
self
|
269
|
+
end
|
270
|
+
|
271
|
+
# Saves only changed columns or does nothing if no columns are marked as
|
272
|
+
# chanaged.
|
273
|
+
def save_changes
|
274
|
+
save(*@changed_columns) unless @changed_columns.empty?
|
275
|
+
end
|
276
|
+
|
277
|
+
# Updates and saves values to database from the passed-in Hash.
|
278
|
+
def set(values)
|
279
|
+
v = values.inject({}) {|m, kv| m[kv[0].to_sym] = kv[1]; m}
|
280
|
+
this.update(v)
|
281
|
+
v.each {|k, v| @values[k] = v}
|
282
|
+
end
|
283
|
+
alias_method :update, :set
|
284
|
+
|
285
|
+
# Reloads values from database and returns self.
|
286
|
+
def refresh
|
287
|
+
@values = this.first || raise(Error, "Record not found")
|
288
|
+
self
|
289
|
+
end
|
290
|
+
alias_method :reload, :refresh
|
291
|
+
|
292
|
+
# Like delete but runs hooks before and after delete.
|
293
|
+
def destroy
|
294
|
+
db.transaction do
|
295
|
+
before_destroy
|
296
|
+
delete
|
297
|
+
after_destroy
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Deletes and returns self.
|
302
|
+
def delete
|
303
|
+
this.delete
|
304
|
+
self
|
305
|
+
end
|
306
|
+
|
307
|
+
ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze
|
308
|
+
EQUAL_SIGN = '='.freeze
|
309
|
+
|
310
|
+
def method_missing(m, *args) #:nodoc:
|
311
|
+
if m.to_s =~ ATTR_RE
|
312
|
+
att = $1.to_sym
|
313
|
+
write = $2 == EQUAL_SIGN
|
314
|
+
|
315
|
+
# check whether the column is legal
|
316
|
+
unless @values.has_key?(att) || columns.include?(att)
|
317
|
+
raise Error, "Invalid column (#{att.inspect}) for #{self}"
|
318
|
+
end
|
319
|
+
|
320
|
+
# define the column accessor
|
321
|
+
Thread.exclusive do
|
322
|
+
if write
|
323
|
+
model.class_def(m) {|v| self[att] = v}
|
324
|
+
else
|
325
|
+
model.class_def(m) {self[att]}
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# call the accessor
|
330
|
+
respond_to?(m) ? send(m, *args) : super(m, *args)
|
331
|
+
else
|
332
|
+
super(m, *args)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|