sequel 1.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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