sequel 1.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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