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/CHANGELOG +37 -1
- data/Rakefile +2 -2
- data/doc/advanced_associations.rdoc +624 -0
- data/lib/sequel_model.rb +7 -2
- data/lib/sequel_model/association_reflection.rb +112 -2
- data/lib/sequel_model/associations.rb +171 -222
- data/lib/sequel_model/base.rb +28 -7
- data/lib/sequel_model/eager_loading.rb +12 -71
- data/lib/sequel_model/record.rb +143 -14
- data/lib/sequel_model/validations.rb +108 -17
- data/spec/association_reflection_spec.rb +10 -2
- data/spec/associations_spec.rb +449 -94
- data/spec/caching_spec.rb +72 -11
- data/spec/eager_loading_spec.rb +168 -21
- data/spec/hooks_spec.rb +53 -15
- data/spec/model_spec.rb +12 -11
- data/spec/spec_helper.rb +0 -7
- data/spec/validations_spec.rb +39 -16
- metadata +6 -4
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:
|
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
|
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]
|
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
|
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
|
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
|
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
|
-
# - :
|
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
|
-
# - :
|
98
|
-
#
|
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
|
-
# - :
|
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
|
-
|
149
|
-
raise
|
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}",
|
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
|
-
#
|
226
|
-
def
|
227
|
-
|
228
|
-
|
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
|
237
|
-
class_def(
|
238
|
-
private
|
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(
|
268
|
-
|
269
|
-
|
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] ||=
|
272
|
-
opts[:left_key_alias] ||= :
|
273
|
-
opts[:left_key_select] ||=
|
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
|
-
|
278
|
-
|
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
|
-
|
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(
|
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(
|
305
|
+
class_def(opts._remove_all_method) do
|
297
306
|
database[join_table].filter(left=>pk).delete
|
298
307
|
end
|
299
|
-
private
|
308
|
+
private opts._add_method, opts._remove_method, opts._remove_all_method
|
300
309
|
|
301
|
-
|
302
|
-
|
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(
|
343
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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(:"#{
|
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
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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(
|
380
|
-
|
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(
|
386
|
+
def_association_dataset_methods(opts)
|
384
387
|
|
385
388
|
unless opts[:read_only]
|
386
|
-
|
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
|
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
|
-
|
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(
|
401
|
+
class_def(opts._remove_all_method) do
|
416
402
|
opts.associated_class.filter(key=>pk).update(key=>nil)
|
417
403
|
end
|
418
|
-
private
|
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
|
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
|
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(
|
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
|
-
#
|
469
|
-
|
470
|
-
|
471
|
-
|
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
|