sequel 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sequel_model.rb CHANGED
@@ -53,9 +53,14 @@ module Sequel
53
53
  # cache_store, cache_ttl, dataset_methods, primary_key, restricted_columns,
54
54
  # sti_dataset, and sti_key. You should not usually need to
55
55
  # access these directly.
56
- # * The following class level attr_accessors are created: strict_param_setting
57
- # and typecast_on_assignment:
56
+ # * The following class level attr_accessors are created: raise_on_save_failure,
57
+ # strict_param_setting, and typecast_on_assignment:
58
58
  #
59
+ # # Don't raise an error if a validation attempt fails in
60
+ # # save/create/save_changes/etc.
61
+ # Model.raise_on_save_failure = false
62
+ # Model.before_save{false}
63
+ # Model.new.save # => nil
59
64
  # # Don't raise errors in new/set/update/etc. if an attempt to
60
65
  # # access a missing/restricted method occurs (just silently
61
66
  # # skip it)
@@ -2,11 +2,47 @@ module Sequel
2
2
  class Model
3
3
  module Associations
4
4
  # AssociationReflection is a Hash subclass that keeps information on Sequel::Model associations. It
5
- # provides a few methods to reduce the amount of internal code duplication. It should not
5
+ # provides methods to reduce internal code duplication. It should not
6
6
  # be instantiated by the user.
7
7
  class AssociationReflection < Hash
8
+ ASSOCIATION_TYPES = [:many_to_one, :one_to_many, :many_to_many]
8
9
  RECIPROCAL_ASSOCIATIONS = {:many_to_one=>:one_to_many, :one_to_many=>:many_to_one, :many_to_many=>:many_to_many}
9
10
 
11
+ # Name symbol for _add_ internal association method
12
+ def _add_method
13
+ :"_add_#{self[:name].to_s.singularize}"
14
+ end
15
+
16
+ # Name symbol for _dataset association method
17
+ def _dataset_method
18
+ :"_#{self[:name]}_dataset"
19
+ end
20
+
21
+ # Name symbol for _remove_all internal association method
22
+ def _remove_all_method
23
+ :"_remove_all_#{self[:name]}"
24
+ end
25
+
26
+ # Name symbol for _remove_ internal association method
27
+ def _remove_method
28
+ :"_remove_#{self[:name].to_s.singularize}"
29
+ end
30
+
31
+ # Name symbol for setter association method
32
+ def _setter_method
33
+ :"_#{self[:name]}="
34
+ end
35
+
36
+ # Name symbol for add_ association method
37
+ def add_method
38
+ :"add_#{self[:name].to_s.singularize}"
39
+ end
40
+
41
+ # Name symbol for association method, the same as the name of the association.
42
+ def association_method
43
+ self[:name]
44
+ end
45
+
10
46
  # The class associated to the current model class via this association
11
47
  def associated_class
12
48
  self[:class] ||= self[:class_name].constantize
@@ -17,6 +53,54 @@ module Sequel
17
53
  self[:associated_primary_key] ||= associated_class.primary_key
18
54
  end
19
55
 
56
+ # Name symbol for dataset association method
57
+ def dataset_method
58
+ :"#{self[:name]}_dataset"
59
+ end
60
+
61
+ # Name symbol for _helper internal association method
62
+ def dataset_helper_method
63
+ :"_#{self[:name]}_dataset_helper"
64
+ end
65
+
66
+ # Whether the dataset needs a primary key to function
67
+ def dataset_need_primary_key?
68
+ self[:type] != :many_to_one
69
+ end
70
+
71
+ # Name symbol for default join table
72
+ def default_join_table
73
+ ([self[:class_name].demodulize, self[:model].name.demodulize]. \
74
+ map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
75
+ end
76
+
77
+ # Default foreign key name symbol for key in associated table that points to
78
+ # current table's primary key.
79
+ def default_left_key
80
+ :"#{self[:model].name.demodulize.underscore}_id"
81
+ end
82
+
83
+ # Default foreign key name symbol for foreign key in current model's table that points to
84
+ # the given association's table's primary key.
85
+ def default_right_key
86
+ :"#{self[:type] == :many_to_one ? self[:name] : self[:name].to_s.singularize}_id"
87
+ end
88
+
89
+ # Name symbol for _dataset association method
90
+ def eager_dataset_method
91
+ :"#{self[:name]}_eager_dataset"
92
+ end
93
+
94
+ # Whether to eagerly graph a lazy dataset
95
+ def eager_graph_lazy_dataset?
96
+ self[:type] != :many_to_one or opts[:key]
97
+ end
98
+
99
+ # Whether the associated object needs a primary key to be added/removed
100
+ def need_associated_primary_key?
101
+ self[:type] == :many_to_many
102
+ end
103
+
20
104
  # Returns/sets the reciprocal association variable, if one exists
21
105
  def reciprocal
22
106
  return self[:reciprocal] if include?(:reciprocal)
@@ -42,9 +126,35 @@ module Sequel
42
126
  self[:reciprocal] = nil
43
127
  end
44
128
 
129
+ # Name symbol for remove_all_ association method
130
+ def remove_all_method
131
+ :"remove_all_#{self[:name]}"
132
+ end
133
+
134
+ # Name symbol for remove_ association method
135
+ def remove_method
136
+ :"remove_#{self[:name].to_s.singularize}"
137
+ end
138
+
45
139
  # The columns to select when loading the association
46
140
  def select
47
- self[:select] ||= associated_class.table_name.*
141
+ return self[:select] if include?(:select)
142
+ self[:select] = self[:type] == :many_to_many ? associated_class.table_name.* : nil
143
+ end
144
+
145
+ # Whether to set the reciprocal to the current object when loading
146
+ def set_reciprocal_to_self?
147
+ self[:type] == :one_to_many
148
+ end
149
+
150
+ # Name symbol for setter association method
151
+ def setter_method
152
+ :"#{self[:name]}="
153
+ end
154
+
155
+ # Whether the association should return a single object or multiple objects.
156
+ def single_associated_object?
157
+ self[:type] == :many_to_one
48
158
  end
49
159
  end
50
160
  end
@@ -11,15 +11,17 @@
11
11
  # end
12
12
  #
13
13
  # The project class now has the following instance methods:
14
- # * portfolio - Returns the associated portfolio
14
+ # * portfolio - Returns the associated portfolio.
15
15
  # * portfolio=(obj) - Sets the associated portfolio to the object,
16
16
  # but the change is not persisted until you save the record.
17
+ # * portfolio_dataset - Returns a dataset that would return the associated
18
+ # portfolio, only useful in fairly specific circumstances.
17
19
  # * milestones - Returns an array of associated milestones
20
+ # * add_milestone(obj) - Associates the passed milestone with this object.
21
+ # * remove_milestone(obj) - Removes the association with the passed milestone.
22
+ # * remove_all_milestones - Removes associations with all associated milestones.
18
23
  # * milestones_dataset - Returns a dataset that would return the associated
19
24
  # milestones, allowing for further filtering/limiting/etc.
20
- # * add_milestone(obj) - Associates the passed milestone with this object
21
- # * remove_milestone(obj) - Removes the association with the passed milestone
22
- # * remove_all_milestones - Removes associations with all associated milestones
23
25
  #
24
26
  # If you want to override the behavior of the add_/remove_/remove_all_ methods,
25
27
  # there are private instance methods created that a prepended with an
@@ -61,18 +63,18 @@ module Sequel::Model::Associations
61
63
  # associated model's primary key. Each associated model object can
62
64
  # be associated with more than one current model objects. Each current
63
65
  # model object can be associated with only one associated model object.
64
- # Similar to ActiveRecord/DataMapper's belongs_to.
66
+ # Similar to ActiveRecord's belongs_to.
65
67
  # * :one_to_many - Foreign key in associated model's table points to this
66
68
  # model's primary key. Each current model object can be associated with
67
69
  # more than one associated model objects. Each associated model object
68
70
  # can be associated with only one current model object.
69
- # Similar to ActiveRecord/DataMapper's has_many.
71
+ # Similar to ActiveRecord's has_many.
70
72
  # * :many_to_many - A join table is used that has a foreign key that points
71
73
  # to this model's primary key and a foreign key that points to the
72
74
  # associated model's primary key. Each current model object can be
73
75
  # associated with many associated model objects, and each associated
74
76
  # model object can be associated with many current model objects.
75
- # Similar to ActiveRecord/DataMapper's has_and_belongs_to_many.
77
+ # Similar to ActiveRecord's has_and_belongs_to_many.
76
78
  #
77
79
  # A one to one relationship can be set up with a many_to_one association
78
80
  # on the table with the foreign key, and a one_to_many association with the
@@ -82,25 +84,55 @@ module Sequel::Model::Associations
82
84
  #
83
85
  # The following options can be supplied:
84
86
  # * *ALL types*:
87
+ # - :after_add - Symbol, Proc, or array of both/either specifying a callback to call
88
+ # after a new item is added to the association.
89
+ # - :after_load - Symbol, Proc, or array of both/either specifying a callback to call
90
+ # after the associated record(s) have been retrieved from the database. Not called
91
+ # when eager loading (see the :eager_loader option to accomplish it when eager loading).
92
+ # - :after_remove - Symbol, Proc, or array of both/either specifying a callback to call
93
+ # after an item is removed from the association.
85
94
  # - :allow_eager - If set to false, you cannot load the association eagerly
86
95
  # via eager or eager_graph
96
+ # - :before_add - Symbol, Proc, or array of both/either specifying a callback to call
97
+ # before a new item is added to the association.
98
+ # - :before_remove - Symbol, Proc, or array of both/either specifying a callback to call
99
+ # before an item is removed from the association.
87
100
  # - :class - The associated class or its name. If not
88
101
  # given, uses the association's name, which is camelized (and
89
102
  # singularized unless the type is :many_to_one)
90
- # - :eager - The associations to eagerly load when loading the associated object.
103
+ # - :dataset - A proc that is instance_evaled to get the base dataset
104
+ # to use for the _dataset method (before the other options are applied).
105
+ # - :eager - The associations to eagerly load via EagerLoading#eager when loading the associated object(s).
91
106
  # For many_to_one associations, this is ignored unless this association is
92
107
  # being eagerly loaded, as it doesn't save queries unless multiple objects
93
108
  # can be loaded at once.
94
109
  # - :eager_block - If given, use the block instead of the default block when
95
110
  # eagerly loading. To not use a block when eager loading (when one is used normally),
96
111
  # set to nil.
97
- # - :graph_conditions - The conditions to use on the SQL join when eagerly loading
98
- # the association via eager_graph
112
+ # - :eager_graph - The associations to eagerly load via EagerLoading#eager_graph when loading the associated object(s).
113
+ # For many_to_one associations, this is ignored unless this association is
114
+ # being eagerly loaded, as it doesn't save queries unless multiple objects
115
+ # can be loaded at once.
116
+ # - :eager_loader - A proc to use to implement eager loading, overriding the default. Takes three arguments,
117
+ # a key hash (used solely to enhance performance), an array of records,
118
+ # and a hash of dependent associations. The associated records should
119
+ # be queried from the database and the associations cache for each
120
+ # record should be populated for this to work correctly.
121
+ # - :extend - A module or array of modules to extend the dataset with.
122
+ # - :graph_block - The block to pass to join_table when eagerly loading
123
+ # the association via eager_graph.
124
+ # - :graph_conditions - The additional conditions to use on the SQL join when eagerly loading
125
+ # the association via eager_graph. Should be a hash or an array of all two pairs.
99
126
  # - :graph_join_type - The type of SQL join to use when eagerly loading the association via
100
- # eager_graph
127
+ # eager_graph. Defaults to :left_outer.
128
+ # - :graph_only_conditions - The conditions to use on the SQL join when eagerly loading
129
+ # the association via eager_graph, instead of the default conditions specified by the
130
+ # foreign/primary keys. This option causes the :graph_conditions option to be ignored.
101
131
  # - :graph_select - A column or array of columns to select from the associated table
102
132
  # when eagerly loading the association via eager_graph. Defaults to all
103
133
  # columns in the associated table.
134
+ # - :limit - Limit the number of records to the provided value. Use
135
+ # an array with two arguments for the value to specify a limit and offset.
104
136
  # - :order - the column(s) by which to order the association dataset. Can be a
105
137
  # singular column or an array.
106
138
  # - :read_only - Do not add a setter method (for many_to_one or one_to_many with :one_to_one),
@@ -115,9 +147,6 @@ module Sequel::Model::Associations
115
147
  # use this option, but beware that the join table attributes can clash with
116
148
  # attributes from the model table, so you should alias any attributes that have
117
149
  # the same name in both the join table and the associated table.
118
- # * :one_to_many, :many_to_many:
119
- # - :limit - Limit the number of records to the provided value. Use
120
- # an array with two arguments for the value to specify a limit and offset.
121
150
  # * :many_to_one:
122
151
  # - :key - foreign_key in current model's table that references
123
152
  # associated model's primary key, as a symbol. Defaults to :"#{name}_id".
@@ -142,11 +171,21 @@ module Sequel::Model::Associations
142
171
  # primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
143
172
  # - :right_key - foreign key in join table that points to associated
144
173
  # model's primary key, as a symbol. Defaults to Defaults to :"#{name.to_s.singularize}_id".
145
- # - :graph_join_table_conditions - The conditions to use on the SQL join for the join table when eagerly loading
146
- # the association via eager_graph
174
+ # - :graph_join_table_block - The block to pass to join_table for
175
+ # the join table when eagerly loading the association via eager_graph.
176
+ # - :graph_join_table_conditions - The additional conditions to use on the SQL join for
177
+ # the join table when eagerly loading the association via eager_graph. Should be a hash
178
+ # or an array of all two pairs.
179
+ # - :graph_join_type - The type of SQL join to use for the join table when eagerly
180
+ # loading the association via eager_graph. Defaults to the :graph_join_type option or
181
+ # :left_outer.
182
+ # - :graph_join_table_only_conditions - The conditions to use on the SQL join for the join
183
+ # table when eagerly loading the association via eager_graph, instead of the default
184
+ # conditions specified by the foreign/primary keys. This option causes the
185
+ # :graph_join_table_conditions option to be ignored.
147
186
  def associate(type, name, opts = {}, &block)
148
- # check arguments
149
- raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
187
+ raise(Error, 'invalid association type') unless AssociationReflection::ASSOCIATION_TYPES.include?(type)
188
+ raise(Error, 'Model.associate name argument must be a symbol') unless Symbol === name
150
189
 
151
190
  # merge early so we don't modify opts
152
191
  opts = opts.merge(:type => type, :name => name, :block => block, :cache => true, :model => self)
@@ -155,6 +194,9 @@ module Sequel::Model::Associations
155
194
  opts[:graph_join_type] ||= :left_outer
156
195
  opts[:graph_conditions] = opts[:graph_conditions] ? opts[:graph_conditions].to_a : []
157
196
  opts[:graph_select] = Array(opts[:graph_select]) if opts[:graph_select]
197
+ [:before_add, :before_remove, :after_add, :after_remove, :after_load, :extend].each do |cb_type|
198
+ opts[cb_type] = Array(opts[cb_type])
199
+ end
158
200
 
159
201
  # find class
160
202
  case opts[:class]
@@ -165,7 +207,7 @@ module Sequel::Model::Associations
165
207
  opts[:class_name] ||= opts[:class].name
166
208
  end
167
209
 
168
- send(:"def_#{type}", name, opts)
210
+ send(:"def_#{type}", opts)
169
211
 
170
212
  # don't add to association_reflections until we are sure there are no errors
171
213
  association_reflections[name] = opts
@@ -201,250 +243,170 @@ module Sequel::Model::Associations
201
243
 
202
244
  private
203
245
 
204
- # Name symbol for add association method
205
- def association_add_method_name(name)
206
- :"add_#{name.to_s.singularize}"
207
- end
208
-
209
- # Name symbol for remove_all association method
210
- def association_remove_all_method_name(name)
211
- :"remove_all_#{name}"
212
- end
213
-
214
- # Name symbol for remove association method
215
- def association_remove_method_name(name)
216
- :"remove_#{name.to_s.singularize}"
217
- end
218
-
219
246
  # Hash storing the association reflections. Keys are association name
220
247
  # symbols, values are association reflection hashes.
221
248
  def association_reflections
222
249
  @association_reflections ||= {}
223
250
  end
224
251
 
225
- # Adds association methods to the model for *_to_many associations.
226
- def def_association_dataset_methods(name, opts, &block)
227
- dataset_method = :"#{name}_dataset"
228
- helper_method = :"#{name}_helper"
229
- dataset_block = opts[:block]
230
- order = opts[:order]
231
- eager = opts[:eager]
232
- limit = opts[:limit]
252
+ # Add the add_ instance method
253
+ def def_add_method(opts)
254
+ class_def(opts.add_method){|o| add_associated_object(opts, o)}
255
+ end
233
256
 
257
+ # Adds association methods to the model for *_to_many associations.
258
+ def def_association_dataset_methods(opts)
234
259
  # If a block is given, define a helper method for it, because it takes
235
260
  # an argument. This is unnecessary in Ruby 1.9, as that has instance_exec.
236
- if dataset_block
237
- class_def(helper_method, &dataset_block)
238
- private helper_method
239
- end
240
-
241
- # define a method returning the association dataset (with optional order)
242
- class_def(dataset_method) do
243
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
244
- ds = instance_eval(&block).select(*opts.select)
245
- ds = ds.order(*order) if order
246
- ds = ds.limit(*limit) if limit
247
- ds = ds.eager(eager) if eager
248
- ds = send(helper_method, ds) if dataset_block
249
- ds
250
- end
251
-
252
- class_def(name) do |*reload|
253
- if (assoc = @associations).include?(name) and !reload[0]
254
- assoc[name]
255
- else
256
- objs = send(dataset_method).all
257
- # Only one_to_many associations should set the reciprocal object
258
- if (opts[:type] == :one_to_many) && (reciprocal = opts.reciprocal)
259
- objs.each{|o| o.associations[reciprocal] = self}
260
- end
261
- assoc[name] = objs
262
- end
261
+ if opts[:block]
262
+ class_def(opts.dataset_helper_method, &opts[:block])
263
+ private opts.dataset_helper_method
263
264
  end
265
+ class_def(opts._dataset_method, &opts[:dataset])
266
+ private opts._dataset_method
267
+ class_def(opts.dataset_method){_dataset(opts)}
268
+ class_def(opts.association_method){|*reload| load_associated_objects(opts, reload[0])}
264
269
  end
265
270
 
266
271
  # Adds many_to_many association instance methods
267
- def def_many_to_many(name, opts)
268
- left = (opts[:left_key] ||= default_remote_key)
269
- right = (opts[:right_key] ||= default_foreign_key(opts))
272
+ def def_many_to_many(opts)
273
+ name = opts[:name]
274
+ model = self
275
+ left = (opts[:left_key] ||= opts.default_left_key)
276
+ right = (opts[:right_key] ||= opts.default_right_key)
270
277
  opts[:class_name] ||= name.to_s.singularize.camelize
271
- join_table = (opts[:join_table] ||= default_join_table_name(opts))
272
- opts[:left_key_alias] ||= :"x_foreign_key_x"
273
- opts[:left_key_select] ||= :"#{join_table}__#{left}___#{opts[:left_key_alias]}"
278
+ join_table = (opts[:join_table] ||= opts.default_join_table)
279
+ left_key_alias = opts[:left_key_alias] ||= :x_foreign_key_x
280
+ left_key_select = opts[:left_key_select] ||= left.qualify(join_table).as(opts[:left_key_alias])
274
281
  opts[:graph_join_table_conditions] = opts[:graph_join_table_conditions] ? opts[:graph_join_table_conditions].to_a : []
282
+ opts[:graph_join_table_join_type] ||= opts[:graph_join_type]
283
+ opts[:dataset] ||= proc{opts.associated_class.inner_join(join_table, [[right, opts.associated_primary_key], [left, pk]])}
275
284
  database = db
276
285
 
277
- def_association_dataset_methods(name, opts) do
278
- opts.associated_class.inner_join(join_table, [[right, opts.associated_primary_key], [left, pk]])
286
+ opts[:eager_loader] ||= proc do |key_hash, records, associations|
287
+ h = key_hash[model.primary_key]
288
+ records.each{|object| object.associations[name] = []}
289
+ model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, [[right, opts.associated_primary_key], [left, h.keys]]), Array(opts.select) + Array(left_key_select), associations).all do |assoc_record|
290
+ next unless objects = h[assoc_record.values.delete(left_key_alias)]
291
+ objects.each{|object| object.associations[name].push(assoc_record)}
292
+ end
279
293
  end
294
+
295
+ def_association_dataset_methods(opts)
280
296
 
281
297
  return if opts[:read_only]
282
298
 
283
- add_meth = association_add_method_name(name)
284
- internal_add_meth = :"_#{add_meth}"
285
- remove_meth = association_remove_method_name(name)
286
- internal_remove_meth = :"_#{remove_meth}"
287
- remove_all_meth = association_remove_all_method_name(name)
288
- internal_remove_all_meth = :"_#{remove_all_meth}"
289
-
290
- class_def(internal_add_meth) do |o|
299
+ class_def(opts._add_method) do |o|
291
300
  database[join_table].insert(left=>pk, right=>o.pk)
292
301
  end
293
- class_def(internal_remove_meth) do |o|
302
+ class_def(opts._remove_method) do |o|
294
303
  database[join_table].filter([[left, pk], [right, o.pk]]).delete
295
304
  end
296
- class_def(internal_remove_all_meth) do
305
+ class_def(opts._remove_all_method) do
297
306
  database[join_table].filter(left=>pk).delete
298
307
  end
299
- private internal_add_meth, internal_remove_meth, internal_remove_all_meth
308
+ private opts._add_method, opts._remove_method, opts._remove_all_method
300
309
 
301
- class_def(add_meth) do |o|
302
- raise(Sequel::Error, 'model object does not have a primary key') unless pk && o.pk
303
- send(internal_add_meth, o)
304
- if (assoc = @associations).include?(name)
305
- assoc[name].push(o)
306
- end
307
- if reciprocal = opts.reciprocal and array = o.associations[reciprocal] and !array.include?(self)
308
- array.push(self)
309
- end
310
- o
311
- end
312
- class_def(remove_meth) do |o|
313
- raise(Sequel::Error, 'model object does not have a primary key') unless pk && o.pk
314
- send(internal_remove_meth, o)
315
- if (assoc = @associations).include?(name)
316
- assoc[name].delete_if{|x| o === x}
317
- end
318
- if reciprocal = opts.reciprocal and array = o.associations[reciprocal]
319
- array.delete_if{|x| self === x}
320
- end
321
- o
322
- end
323
- class_def(remove_all_meth) do
324
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
325
- send(internal_remove_all_meth)
326
- if (assoc = @associations).include?(name)
327
- reciprocal = opts.reciprocal
328
- arr = assoc[name]
329
- ret = arr.dup
330
- arr.each do |o|
331
- if reciprocal and array = o.associations[reciprocal]
332
- array.delete_if{|x| self === x}
333
- end
334
- end
335
- end
336
- assoc[name] = []
337
- ret
338
- end
310
+ def_add_method(opts)
311
+ def_remove_methods(opts)
339
312
  end
340
313
 
341
314
  # Adds many_to_one association instance methods
342
- def def_many_to_one(name, opts)
343
- key = (opts[:key] ||= default_foreign_key(opts))
315
+ def def_many_to_one(opts)
316
+ name = opts[:name]
317
+ model = self
318
+ opts[:key] = opts.default_right_key unless opts.include?(:key)
319
+ key = opts[:key]
344
320
  opts[:class_name] ||= name.to_s.camelize
345
-
346
- class_def(name) do |*reload|
347
- if (assoc = @associations).include?(name) and !reload[0]
348
- assoc[name]
349
- else
350
- obj = if fk = send(key)
351
- opts.associated_class.select(*opts.select).filter(opts.associated_primary_key=>fk).first
321
+ opts[:dataset] ||= proc do
322
+ klass = opts.associated_class
323
+ klass.filter(opts.associated_primary_key.qualify(klass.table_name)=>send(key))
324
+ end
325
+ opts[:eager_loader] ||= proc do |key_hash, records, associations|
326
+ h = key_hash[key]
327
+ keys = h.keys
328
+ # Skip eager loading if no objects have a foreign key for this association
329
+ unless keys.empty?
330
+ # Default the cached association to nil, so any object that doesn't have it
331
+ # populated will have cached the negative lookup.
332
+ records.each{|object| object.associations[name] = nil}
333
+ model.eager_loading_dataset(opts, opts.associated_class.filter(opts.associated_primary_key.qualify(opts.associated_class.table_name)=>keys), opts.select, associations).all do |assoc_record|
334
+ next unless objects = h[assoc_record.pk]
335
+ objects.each{|object| object.associations[name] = assoc_record}
352
336
  end
353
- assoc[name] = obj
354
337
  end
355
338
  end
339
+
340
+ def_association_dataset_methods(opts)
341
+
356
342
  return if opts[:read_only]
357
343
 
358
- class_def(:"#{name}=") do |o|
344
+ class_def(opts._setter_method){|o| send(:"#{key}=", (o.pk if o))}
345
+ private opts._setter_method
346
+
347
+ class_def(opts.setter_method) do |o|
359
348
  raise(Sequel::Error, 'model object does not have a primary key') if o && !o.pk
360
- reciprocal = opts.reciprocal
361
- if (assoc = @associations).include?(name) and reciprocal
362
- old_val = assoc[name]
363
- end
364
- assoc[name] = o
365
- send(:"#{key}=", (o.pk if o))
366
- if reciprocal and old_val != o
367
- if old_val and array = old_val.associations[reciprocal]
368
- array.delete_if{|x| self === x}
369
- end
370
- if o and array = o.associations[reciprocal] and !array.include?(self)
371
- array.push(self)
372
- end
373
- end
349
+ old_val = send(opts.association_method)
350
+ return o if old_val == o
351
+ return if old_val and run_association_callbacks(opts, :before_remove, old_val) == false
352
+ return if o and run_association_callbacks(opts, :before_add, o) == false
353
+ send(opts._setter_method, o)
354
+ @associations[name] = o
355
+ remove_reciprocal_object(opts, old_val) if old_val
356
+ add_reciprocal_object(opts, o) if o
357
+ run_association_callbacks(opts, :after_add, o) if o
358
+ run_association_callbacks(opts, :after_remove, old_val) if old_val
374
359
  o
375
360
  end
376
361
  end
377
362
 
378
363
  # Adds one_to_many association instance methods
379
- def def_one_to_many(name, opts)
380
- key = (opts[:key] ||= default_remote_key)
364
+ def def_one_to_many(opts)
365
+ name = opts[:name]
366
+ model = self
367
+ key = (opts[:key] ||= opts.default_left_key)
381
368
  opts[:class_name] ||= name.to_s.singularize.camelize
369
+ opts[:dataset] ||= proc do
370
+ klass = opts.associated_class
371
+ klass.filter(key.qualify(klass.table_name) => pk)
372
+ end
373
+ opts[:eager_loader] ||= proc do |key_hash, records, associations|
374
+ h = key_hash[model.primary_key]
375
+ records.each{|object| object.associations[name] = []}
376
+ reciprocal = opts.reciprocal
377
+ model.eager_loading_dataset(opts, opts.associated_class.filter(key.qualify(opts.associated_class.table_name)=>h.keys), opts.select, associations).all do |assoc_record|
378
+ next unless objects = h[assoc_record[key]]
379
+ objects.each do |object|
380
+ object.associations[name].push(assoc_record)
381
+ assoc_record.associations[reciprocal] = object if reciprocal
382
+ end
383
+ end
384
+ end
382
385
 
383
- def_association_dataset_methods(name, opts) {opts.associated_class.filter(key => pk)}
386
+ def_association_dataset_methods(opts)
384
387
 
385
388
  unless opts[:read_only]
386
- add_meth = association_add_method_name(name)
387
- internal_add_meth = :"_#{add_meth}"
388
- class_def(internal_add_meth) do |o|
389
+ class_def(opts._add_method) do |o|
389
390
  o.send(:"#{key}=", pk)
390
391
  o.save || raise(Sequel::Error, "invalid associated object, cannot save")
391
392
  end
392
- private internal_add_meth
393
+ private opts._add_method
394
+ def_add_method(opts)
393
395
 
394
- class_def(add_meth) do |o|
395
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
396
- send(internal_add_meth, o)
397
- if (assoc = @associations).include?(name)
398
- assoc[name].push(o)
399
- end
400
- if reciprocal = opts.reciprocal
401
- o.associations[reciprocal] = self
402
- end
403
- o
404
- end
405
396
  unless opts[:one_to_one]
406
- remove_meth = association_remove_method_name(name)
407
- internal_remove_meth = :"_#{remove_meth}"
408
- remove_all_meth = association_remove_all_method_name(name)
409
- internal_remove_all_meth = :"_#{remove_all_meth}"
410
-
411
- class_def(internal_remove_meth) do |o|
397
+ class_def(opts._remove_method) do |o|
412
398
  o.send(:"#{key}=", nil)
413
399
  o.save || raise(Sequel::Error, "invalid associated object, cannot save")
414
400
  end
415
- class_def(internal_remove_all_meth) do
401
+ class_def(opts._remove_all_method) do
416
402
  opts.associated_class.filter(key=>pk).update(key=>nil)
417
403
  end
418
- private internal_remove_meth, internal_remove_all_meth
419
-
420
- class_def(remove_meth) do |o|
421
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
422
- send(internal_remove_meth, o)
423
- if (assoc = @associations).include?(name)
424
- assoc[name].delete_if{|x| o === x}
425
- end
426
- if reciprocal = opts.reciprocal
427
- o.associations[reciprocal] = nil
428
- end
429
- o
430
- end
431
- class_def(remove_all_meth) do
432
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
433
- send(internal_remove_all_meth)
434
- if (assoc = @associations).include?(name)
435
- arr = assoc[name]
436
- ret = arr.dup
437
- if reciprocal = opts.reciprocal
438
- arr.each{|o| o.associations[reciprocal] = nil}
439
- end
440
- end
441
- assoc[name] = []
442
- ret
443
- end
404
+ private opts._remove_method, opts._remove_all_method
405
+ def_remove_methods(opts)
444
406
  end
445
407
  end
446
408
  if opts[:one_to_one]
447
- private name, :"#{name}_dataset"
409
+ private opts.association_method, opts.dataset_method
448
410
  n = name.to_s.singularize.to_sym
449
411
  raise(Sequel::Error, "one_to_many association names should still be plural even when using the :one_to_one option") if n == name
450
412
  class_def(n) do |*o|
@@ -453,11 +415,11 @@ module Sequel::Model::Associations
453
415
  objs.first
454
416
  end
455
417
  unless opts[:read_only]
456
- private add_meth
418
+ private opts.add_method
457
419
  class_def(:"#{n}=") do |o|
458
420
  klass = opts.associated_class
459
421
  model.db.transaction do
460
- send(add_meth, o)
422
+ send(opts.add_method, o)
461
423
  klass.filter(Sequel::SQL::BooleanExpression.new(:AND, {key=>pk}, ~{klass.primary_key=>o.pk}.sql_expr)).update(key=>nil)
462
424
  end
463
425
  end
@@ -465,22 +427,9 @@ module Sequel::Model::Associations
465
427
  end
466
428
  end
467
429
 
468
- # Default foreign key name symbol for foreign key in current model's table that points to
469
- # the given association's table's primary key.
470
- def default_foreign_key(reflection)
471
- name = reflection[:name]
472
- :"#{reflection[:type] == :many_to_one ? name : name.to_s.singularize}_id"
473
- end
474
-
475
- # Name symbol for default join table
476
- def default_join_table_name(opts)
477
- ([opts[:class_name].demodulize, name.demodulize]. \
478
- map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
479
- end
480
-
481
- # Default foreign key name symbol for key in associated table that points to
482
- # current table's primary key.
483
- def default_remote_key
484
- :"#{name.demodulize.underscore}_id"
430
+ # Add the remove_ and remove_all instance methods
431
+ def def_remove_methods(opts)
432
+ class_def(opts.remove_method){|o| remove_associated_object(opts, o)}
433
+ class_def(opts.remove_all_method){remove_all_associated_objects(opts)}
485
434
  end
486
435
  end