sequel 2.1.0 → 2.2.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.
- 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
|