sequel 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,42 +1,87 @@
1
1
  module Sequel
2
2
  class Model
3
+ metaattr_reader :cache_store, :cache_ttl
4
+
5
+ ### Public Class Methods ###
6
+
7
+ # Set the cache store for the model, as well as the caching before_* hooks.
8
+ #
9
+ # The cache store should implement the following API:
10
+ #
11
+ # cache_store.set(key, obj, time) # Associate the obj with the given key
12
+ # # in the cache for the time (specified
13
+ # # in seconds)
14
+ # cache_store.get(key) => obj # Returns object set with same key
15
+ # cache_store.get(key2) => nil # nil returned if there isn't an object
16
+ # # currently in the cache with that key
3
17
  def self.set_cache(store, opts = {})
4
18
  @cache_store = store
5
- if (ttl = opts[:ttl])
6
- set_cache_ttl(ttl)
7
- end
8
-
9
- meta_def(:[]) do |*args|
10
- if (args.size == 1) && (Hash === (h = args.first))
11
- return dataset[h]
12
- end
13
-
14
- unless obj = @cache_store.get(cache_key_from_values(args))
15
- obj = dataset[primary_key_hash((args.size == 1) ? args.first : args)]
16
- @cache_store.set(cache_key_from_values(args), obj, cache_ttl)
17
- end
18
- obj
19
- end
20
-
21
- class_def(:update_values) {|v| store.delete(cache_key); super}
22
- class_def(:save) {store.delete(cache_key) unless new?; super}
23
- class_def(:delete) {store.delete(cache_key); super}
19
+ @cache_ttl = opts[:ttl] || 3600
20
+ before_save :cache_delete_unless_new
21
+ before_update_values :cache_delete
22
+ before_delete :cache_delete
24
23
  end
25
24
 
25
+ # Set the time to live for the cache store, in seconds (default is 3600,
26
+ # so 1 hour).
26
27
  def self.set_cache_ttl(ttl)
27
28
  @cache_ttl = ttl
28
29
  end
29
30
 
30
- def self.cache_store
31
- @cache_store
31
+ ### Private Class Methods ###
32
+
33
+ # Delete the entry with the matching key from the cache
34
+ def self.cache_delete(key) # :nodoc:
35
+ @cache_store.delete(key)
36
+ nil
32
37
  end
33
-
34
- def self.cache_ttl
35
- @cache_ttl ||= 3600
38
+
39
+ # Return a key string for the pk
40
+ def self.cache_key(pk) # :nodoc:
41
+ "#{self}:#{Array(pk).join(',')}"
36
42
  end
37
-
38
- def self.cache_key_from_values(values)
39
- "#{self}:#{values.join(',')}"
43
+
44
+ # Lookup the primary key in the cache.
45
+ # If found, return the matching object.
46
+ # Otherwise, get the matching object from the database and
47
+ # update the cache with it.
48
+ def self.cache_lookup(pk) # :nodoc:
49
+ ck = cache_key(pk)
50
+ unless obj = @cache_store.get(ck)
51
+ obj = dataset[primary_key_hash(pk)]
52
+ @cache_store.set(ck, obj, @cache_ttl)
53
+ end
54
+ obj
55
+ end
56
+
57
+ metaprivate :cache_delete, :cache_key, :cache_lookup
58
+
59
+ ### Instance Methods ###
60
+
61
+ # Return a key unique to the underlying record for caching, based on the
62
+ # primary key value(s) for the object. If the model does not have a primary
63
+ # key, raise an Error.
64
+ def cache_key
65
+ raise(Error, "No primary key is associated with this model") unless key = primary_key
66
+ pk = case key
67
+ when Array
68
+ key.collect{|k| @values[k]}
69
+ else
70
+ @values[key] || (raise Error, 'no primary key for this record')
71
+ end
72
+ model.send(:cache_key, pk)
73
+ end
74
+
75
+ private
76
+
77
+ # Delete this object from the cache
78
+ def cache_delete
79
+ model.send(:cache_delete, cache_key)
80
+ end
81
+
82
+ # Delete this object from the cache unless it is a new record
83
+ def cache_delete_unless_new
84
+ cache_delete unless new?
40
85
  end
41
86
  end
42
87
  end
@@ -1,36 +1,33 @@
1
1
  # Eager loading makes it so that you can load all associated records for a
2
2
  # set of objects in a single query, instead of a separate query for each object.
3
3
  #
4
- # Two separate implementations are provided. .eager should be used most of the
4
+ # Two separate implementations are provided. #eager should be used most of the
5
5
  # time, as it loads associated records using one query per association. However,
6
- # it does not allow you the ability to filter based on columns in associated tables. .eager_graph loads
7
- # all records in one query. Using .eager_graph you can filter based on columns in associated
8
- # tables. However, .eager_graph can be much slower than .eager, especially if multiple
6
+ # it does not allow you the ability to filter based on columns in associated tables. #eager_graph loads
7
+ # all records in one query. Using #eager_graph you can filter based on columns in associated
8
+ # tables. However, #eager_graph can be much slower than #eager, especially if multiple
9
9
  # *_to_many associations are joined.
10
10
  #
11
11
  # You can cascade the eager loading (loading associations' associations)
12
- # with no limit to the depth of the cascades. You do this by passing a hash to .eager or .eager_graph
12
+ # with no limit to the depth of the cascades. You do this by passing a hash to #eager or #eager_graph
13
13
  # with the keys being associations of the current model and values being
14
14
  # associations of the model associated with the current model via the key.
15
15
  #
16
- # You cannot eagerly load an association with a block argument, as the block argument is
17
- # evaluated in terms of a specific instance of the model, and no specific instance exists.
18
- #
19
16
  # The arguments can be symbols or hashes with symbol keys (for cascaded
20
17
  # eager loading). Examples:
21
18
  #
22
- # Album.eager(:artist).all
23
- # Album.eager_graph(:artist).all
24
- # Album.eager(:artist, :genre).all
25
- # Album.eager_graph(:artist, :genre).all
26
- # Album.eager(:artist).eager(:genre).all
27
- # Album.eager_graph(:artist).eager(:genre).all
28
- # Artist.eager(:albums=>:tracks).all
29
- # Artist.eager_graph(:albums=>:tracks).all
30
- # Artist.eager(:albums=>{:tracks=>:genre}).all
31
- # Artist.eager_graph(:albums=>{:tracks=>:genre}).all
19
+ # Album.eager(:artist).all
20
+ # Album.eager_graph(:artist).all
21
+ # Album.eager(:artist, :genre).all
22
+ # Album.eager_graph(:artist, :genre).all
23
+ # Album.eager(:artist).eager(:genre).all
24
+ # Album.eager_graph(:artist).eager(:genre).all
25
+ # Artist.eager(:albums=>:tracks).all
26
+ # Artist.eager_graph(:albums=>:tracks).all
27
+ # Artist.eager(:albums=>{:tracks=>:genre}).all
28
+ # Artist.eager_graph(:albums=>{:tracks=>:genre}).all
32
29
  module Sequel::Model::Associations::EagerLoading
33
- # Add the .eager! and .eager_graph! mutation methods to the dataset.
30
+ # Add the #eager! and #eager_graph! mutation methods to the dataset.
34
31
  def self.extended(obj)
35
32
  obj.def_mutation_method(:eager, :eager_graph)
36
33
  end
@@ -39,7 +36,7 @@ module Sequel::Model::Associations::EagerLoading
39
36
  # query for each association.
40
37
  #
41
38
  # The basic idea for how it works is that the dataset is first loaded normally.
42
- # Then it goes through all associations that have been specified via .eager.
39
+ # Then it goes through all associations that have been specified via eager.
43
40
  # It loads each of those associations separately, then associates them back
44
41
  # to the original dataset via primary/foreign keys. Due to the necessity of
45
42
  # all objects being present, you need to use .all to use eager loading, as it
@@ -55,11 +52,12 @@ module Sequel::Model::Associations::EagerLoading
55
52
  # based on values of columns in an associated table, since the associations are loaded
56
53
  # in separate queries. To do that you need to load all associations in the
57
54
  # same query, and extract an object graph from the results of that query. If you
58
- # need to filter based on columns in associated tables, look at .eager_graph
55
+ # need to filter based on columns in associated tables, look at #eager_graph
59
56
  # or join the tables you need to filter on manually.
60
57
  #
61
58
  # Each association's order, if definied, is respected. Eager also works
62
- # on a limited dataset.
59
+ # on a limited dataset. If the association uses a block or has an :eager_block
60
+ # argument, it is used.
63
61
  def eager(*associations)
64
62
  model = check_model
65
63
  opt = @opts[:eager]
@@ -72,7 +70,7 @@ module Sequel::Model::Associations::EagerLoading
72
70
  when Hash
73
71
  association.keys.each{|assoc| check_association(model, assoc)}
74
72
  opt.merge!(association)
75
- else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
73
+ else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash')
76
74
  end
77
75
  end
78
76
  clone(:eager=>opt)
@@ -81,7 +79,7 @@ module Sequel::Model::Associations::EagerLoading
81
79
  # The secondary eager loading method. Loads all associations in a single query. This
82
80
  # method should only be used if you need to filter based on columns in associated tables.
83
81
  #
84
- # This method builds an object graph using the .graph method. Then it uses the graph
82
+ # This method builds an object graph using Dataset#graph. Then it uses the graph
85
83
  # to build the associations, and finally replaces the graph with a simple array
86
84
  # of model objects.
87
85
  #
@@ -92,8 +90,12 @@ module Sequel::Model::Associations::EagerLoading
92
90
  # This does not respect each association's order, as all associations are loaded in
93
91
  # a single query. If you want to order the results, you must manually call .order.
94
92
  #
95
- # eager_graph probably won't work the way you suspect with limit, unless you are
93
+ # #eager_graph probably won't work the way you suspect with limit, unless you are
96
94
  # only graphing many_to_one associations.
95
+ #
96
+ # Does not use the block defined for the association, since it does a single query for
97
+ # all objects. You can use the :graph_join_type, :graph_conditions, and :graph_join_table_conditions
98
+ # association options to modify the SQL query.
97
99
  def eager_graph(*associations)
98
100
  model = check_model
99
101
  table_name = model.table_name
@@ -101,6 +103,7 @@ module Sequel::Model::Associations::EagerLoading
101
103
  self
102
104
  else
103
105
  # Each of the following have a symbol key for the table alias, with the following values:
106
+ # :reciprocals - the reciprocal instance variable to use for this association
104
107
  # :requirements - array of requirements for this association
105
108
  # :alias_association_type_map - the type of association for this association
106
109
  # :alias_association_name_map - the name of the association for this association
@@ -110,320 +113,325 @@ module Sequel::Model::Associations::EagerLoading
110
113
  end
111
114
 
112
115
  protected
113
- # Call graph on the association with the correct arguments,
114
- # update the eager_graph data structure, and recurse into
115
- # eager_graph_associations if there are any passed in associations
116
- # (which would be dependencies of the current association)
117
- #
118
- # Arguments:
119
- # * ds - Current dataset
120
- # * model - Current Model
121
- # * ta - table_alias used for the parent association
122
- # * requirements - an array, used as a stack for requirements
123
- # * r - association reflection for the current association
124
- # * *associations - any associations dependent on this one
125
- def eager_graph_association(ds, model, ta, requirements, r, *associations)
126
- klass = model.send(:associated_class, r)
127
- assoc_name = r[:name]
128
- assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
129
- ds = case assoc_type = r[:type]
130
- when :many_to_one
131
- ds.graph(klass, {klass.primary_key=>:"#{ta}__#{r[:key]}"}, :table_alias=>assoc_table_alias)
132
- when :one_to_many
133
- ds = ds.graph(klass, {r[:key]=>:"#{ta}__#{model.primary_key}"}, :table_alias=>assoc_table_alias)
134
- # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
135
- ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = model.send(:reciprocal_association, r)
136
- ds
137
- when :many_to_many
138
- ds = ds.graph(r[:join_table], {r[:left_key]=>:"#{ta}__#{model.primary_key}"}, :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]))
139
- ds.graph(klass, {klass.primary_key=>r[:right_key]}, :table_alias=>assoc_table_alias)
140
- end
141
- eager_graph = ds.opts[:eager_graph]
142
- eager_graph[:requirements][assoc_table_alias] = requirements.dup
143
- eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
144
- eager_graph[:alias_association_type_map][assoc_table_alias] = assoc_type
145
- ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
116
+
117
+ # Call graph on the association with the correct arguments,
118
+ # update the eager_graph data structure, and recurse into
119
+ # eager_graph_associations if there are any passed in associations
120
+ # (which would be dependencies of the current association)
121
+ #
122
+ # Arguments:
123
+ # * ds - Current dataset
124
+ # * model - Current Model
125
+ # * ta - table_alias used for the parent association
126
+ # * requirements - an array, used as a stack for requirements
127
+ # * r - association reflection for the current association
128
+ # * *associations - any associations dependent on this one
129
+ def eager_graph_association(ds, model, ta, requirements, r, *associations)
130
+ klass = r.associated_class
131
+ assoc_name = r[:name]
132
+ assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
133
+ join_type = r[:graph_join_type]
134
+ conditions = r[:graph_conditions]
135
+ ds = case assoc_type = r[:type]
136
+ when :many_to_one
137
+ ds.graph(klass, [[klass.primary_key, :"#{ta}__#{r[:key]}"]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
138
+ when :one_to_many
139
+ ds = ds.graph(klass, [[r[:key], :"#{ta}__#{model.primary_key}"]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
140
+ # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
141
+ ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = r.reciprocal
146
142
  ds
143
+ when :many_to_many
144
+ ds = ds.graph(r[:join_table], [[r[:left_key], :"#{ta}__#{model.primary_key}"]] + r[:graph_join_table_conditions], :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]), :join_type=>join_type)
145
+ ds.graph(klass, [[klass.primary_key, r[:right_key]]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
147
146
  end
148
-
149
- # Check the associations are valid for the given model.
150
- # Call eager_graph_association on each association.
151
- #
152
- # Arguments:
153
- # * ds - Current dataset
154
- # * model - Current Model
155
- # * ta - table_alias used for the parent association
156
- # * requirements - an array, used as a stack for requirements
157
- # * *associations - the associations to add to the graph
158
- def eager_graph_associations(ds, model, ta, requirements, *associations)
159
- return ds if associations.empty?
160
- associations.flatten.each do |association|
161
- ds = case association
162
- when Symbol
163
- ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association))
164
- when Hash
165
- association.each do |assoc, assoc_assocs|
166
- ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs)
167
- end
168
- ds
169
- else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
147
+ eager_graph = ds.opts[:eager_graph]
148
+ eager_graph[:requirements][assoc_table_alias] = requirements.dup
149
+ eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
150
+ eager_graph[:alias_association_type_map][assoc_table_alias] = assoc_type
151
+ ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
152
+ ds
153
+ end
154
+
155
+ # Check the associations are valid for the given model.
156
+ # Call eager_graph_association on each association.
157
+ #
158
+ # Arguments:
159
+ # * ds - Current dataset
160
+ # * model - Current Model
161
+ # * ta - table_alias used for the parent association
162
+ # * requirements - an array, used as a stack for requirements
163
+ # * *associations - the associations to add to the graph
164
+ def eager_graph_associations(ds, model, ta, requirements, *associations)
165
+ return ds if associations.empty?
166
+ associations.flatten.each do |association|
167
+ ds = case association
168
+ when Symbol
169
+ ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association))
170
+ when Hash
171
+ association.each do |assoc, assoc_assocs|
172
+ ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs)
170
173
  end
174
+ ds
175
+ else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash')
171
176
  end
172
- ds
173
177
  end
178
+ ds
179
+ end
174
180
 
175
- # Build associations out of the array of returned object graphs.
176
- def eager_graph_build_associations(record_graphs)
177
- # Dup the tables that will be used, so that self is not modified.
178
- eager_graph = @opts[:eager_graph]
179
- master = eager_graph[:master]
180
- requirements = eager_graph[:requirements]
181
- alias_map = eager_graph[:alias_association_name_map]
182
- type_map = eager_graph[:alias_association_type_map]
183
- reciprocal_map = eager_graph[:reciprocals]
181
+ # Build associations out of the array of returned object graphs.
182
+ def eager_graph_build_associations(record_graphs)
183
+ eager_graph = @opts[:eager_graph]
184
+ master = eager_graph[:master]
185
+ requirements = eager_graph[:requirements]
186
+ alias_map = eager_graph[:alias_association_name_map]
187
+ type_map = eager_graph[:alias_association_type_map]
188
+ reciprocal_map = eager_graph[:reciprocals]
184
189
 
185
- # Make dependency map hash out of requirements array for each association.
186
- # This builds a tree of dependencies that will be used for recursion
187
- # to ensure that all parts of the object graph are loaded into the
188
- # appropriate subordinate association.
189
- dependency_map = {}
190
- # Sort the associations be requirements length, so that
191
- # requirements are added to the dependency hash before their
192
- # dependencies.
193
- requirements.sort_by{|a| a[1].length}.each do |ta, deps|
194
- if deps.empty?
195
- dependency_map[ta] = {}
196
- else
197
- deps = deps.dup
198
- hash = dependency_map[deps.shift]
199
- deps.each do |dep|
200
- hash = hash[dep]
201
- end
202
- hash[ta] = {}
190
+ # Make dependency map hash out of requirements array for each association.
191
+ # This builds a tree of dependencies that will be used for recursion
192
+ # to ensure that all parts of the object graph are loaded into the
193
+ # appropriate subordinate association.
194
+ dependency_map = {}
195
+ # Sort the associations be requirements length, so that
196
+ # requirements are added to the dependency hash before their
197
+ # dependencies.
198
+ requirements.sort_by{|a| a[1].length}.each do |ta, deps|
199
+ if deps.empty?
200
+ dependency_map[ta] = {}
201
+ else
202
+ deps = deps.dup
203
+ hash = dependency_map[deps.shift]
204
+ deps.each do |dep|
205
+ hash = hash[dep]
203
206
  end
207
+ hash[ta] = {}
204
208
  end
209
+ end
205
210
 
206
- # This mapping is used to make sure that duplicate entries in the
207
- # result set are mapped to a single record. For example, using a
208
- # single one_to_many association with 10 associated records,
209
- # the main object will appear in the object graph 10 times.
210
- # We map by primary key, if available, or by the object's entire values,
211
- # if not. The mapping must be per table, so create sub maps for each table
212
- # alias.
213
- records_map = {master=>{}}
214
- alias_map.keys.each{|ta| records_map[ta] = {}}
211
+ # This mapping is used to make sure that duplicate entries in the
212
+ # result set are mapped to a single record. For example, using a
213
+ # single one_to_many association with 10 associated records,
214
+ # the main object will appear in the object graph 10 times.
215
+ # We map by primary key, if available, or by the object's entire values,
216
+ # if not. The mapping must be per table, so create sub maps for each table
217
+ # alias.
218
+ records_map = {master=>{}}
219
+ alias_map.keys.each{|ta| records_map[ta] = {}}
215
220
 
216
- # This will hold the final record set that we will be replacing the object graph with.
217
- records = []
218
- record_graphs.each do |record_graph|
219
- primary_record = record_graph[master]
220
- key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s}
221
- if cached_pr = records_map[master][key]
222
- primary_record = cached_pr
223
- else
224
- records_map[master][key] = primary_record
225
- # Only add it to the list of records to return if it is a new record
226
- records.push(primary_record)
227
- end
228
- # Build all associations for the current object and it's dependencies
229
- eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph)
221
+ # This will hold the final record set that we will be replacing the object graph with.
222
+ records = []
223
+ record_graphs.each do |record_graph|
224
+ primary_record = record_graph[master]
225
+ key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s}
226
+ if cached_pr = records_map[master][key]
227
+ primary_record = cached_pr
228
+ else
229
+ records_map[master][key] = primary_record
230
+ # Only add it to the list of records to return if it is a new record
231
+ records.push(primary_record)
230
232
  end
231
-
232
- # Remove duplicate records from all associations if this graph could possibly be a cartesian product
233
- eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.reject{|k,v| v == :many_to_one}.length > 1
234
-
235
- # Replace the array of object graphs with an array of model objects
236
- record_graphs.replace(records)
233
+ # Build all associations for the current object and it's dependencies
234
+ eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph)
237
235
  end
238
236
 
239
- # Creates a unique table alias that hasn't already been used in the query.
240
- # Will either be the table_alias itself or table_alias_N for some integer
241
- # N (starting at 0 and increasing until an unused one is found).
242
- def eager_unique_table_alias(ds, table_alias)
243
- if (graph = ds.opts[:graph]) && (table_aliases = graph[:table_aliases]) && (table_aliases.include?(table_alias))
244
- i = 0
245
- loop do
246
- ta = :"#{table_alias}_#{i}"
247
- return ta unless table_aliases[ta]
248
- i += 1
249
- end
250
- else
251
- table_alias
237
+ # Remove duplicate records from all associations if this graph could possibly be a cartesian product
238
+ eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.reject{|k,v| v == :many_to_one}.length > 1
239
+
240
+ # Replace the array of object graphs with an array of model objects
241
+ record_graphs.replace(records)
242
+ end
243
+
244
+ # Creates a unique table alias that hasn't already been used in the query.
245
+ # Will either be the table_alias itself or table_alias_N for some integer
246
+ # N (starting at 0 and increasing until an unused one is found).
247
+ def eager_unique_table_alias(ds, table_alias)
248
+ if (graph = ds.opts[:graph]) && (table_aliases = graph[:table_aliases]) && (table_aliases.include?(table_alias))
249
+ i = 0
250
+ loop do
251
+ ta = :"#{table_alias}_#{i}"
252
+ return ta unless table_aliases[ta]
253
+ i += 1
252
254
  end
255
+ else
256
+ table_alias
253
257
  end
254
-
258
+ end
259
+
255
260
  private
256
- # Make sure a standard (non-polymorphic model) is used for this dataset, and return the model
257
- def check_model
258
- raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
259
- model
260
- end
261
261
 
262
- # Make sure the association is valid for this model, and return the association's reflection
263
- def check_association(model, association)
264
- raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(association)
265
- raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
266
- reflection
262
+ # Make sure this dataset is associated with a model, and return the default model for it.
263
+ def check_model
264
+ raise(Sequel::Error, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
265
+ model
266
+ end
267
+
268
+ # Make sure the association is valid for this model, and return the related AssociationReflection.
269
+ def check_association(model, association)
270
+ raise(Sequel::Error, 'Invalid association') unless reflection = model.association_reflection(association)
271
+ raise(Sequel::Error, "Eager loading is not allowed for #{model.name} association #{association}") if reflection[:allow_eager] == false
272
+ reflection
273
+ end
274
+
275
+ # Build associations for the current object. This is called recursively
276
+ # to build object's dependencies.
277
+ def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph)
278
+ return if dependency_map.empty?
279
+ # Don't clobber the instance variable array for *_to_many associations if it has already been setup
280
+ dependency_map.keys.each do |ta|
281
+ current.instance_variable_set("@#{alias_map[ta]}", type_map[ta] == :many_to_one ? :null : []) unless current.instance_variable_get("@#{alias_map[ta]}")
267
282
  end
268
-
269
- # Build associations for the current object. This is called recursively
270
- # to build object's dependencies.
271
- def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph)
272
- return if dependency_map.empty?
273
- # Don't clobber the instance variable array for *_to_many associations if it has already been setup
274
- dependency_map.keys.each do |ta|
275
- current.instance_variable_set("@#{alias_map[ta]}", type_map[ta] == :many_to_one ? :null : []) unless current.instance_variable_get("@#{alias_map[ta]}")
283
+ dependency_map.each do |ta, deps|
284
+ next unless rec = record_graph[ta]
285
+ key = rec.pk || rec.values.sort_by{|x| x[0].to_s}
286
+ if cached_rec = records_map[ta][key]
287
+ rec = cached_rec
288
+ else
289
+ records_map[ta][rec.pk] = rec
276
290
  end
277
- dependency_map.each do |ta, deps|
278
- next unless rec = record_graph[ta]
279
- key = rec.pk || rec.values.sort_by{|x| x[0].to_s}
280
- if cached_rec = records_map[ta][key]
281
- rec = cached_rec
282
- else
283
- records_map[ta][rec.pk] = rec
284
- end
285
- ivar = "@#{alias_map[ta]}"
286
- case assoc_type = type_map[ta]
287
- when :many_to_one
288
- current.instance_variable_set(ivar, rec)
289
- else
290
- list = current.instance_variable_get(ivar)
291
- list.push(rec)
292
- if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta])
293
- rec.instance_variable_set(reciprocal, current)
294
- end
291
+ ivar = "@#{alias_map[ta]}"
292
+ case assoc_type = type_map[ta]
293
+ when :many_to_one
294
+ current.instance_variable_set(ivar, rec)
295
+ else
296
+ list = current.instance_variable_get(ivar)
297
+ list.push(rec)
298
+ if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta])
299
+ rec.instance_variable_set(reciprocal, current)
295
300
  end
296
- # Recurse into dependencies of the current object
297
- eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
298
301
  end
302
+ # Recurse into dependencies of the current object
303
+ eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
299
304
  end
305
+ end
300
306
 
301
- # If the result set is the result of a cartesian product, then it is possible that
302
- # there a multiple records for each association when there should only be one.
303
- def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
304
- records.each do |record|
305
- dependency_map.each do |ta, deps|
306
- list = if type_map[ta] == :many_to_one
307
- item = record.send(alias_map[ta])
308
- [item] if item
309
- else
310
- list = record.send(alias_map[ta])
311
- list.uniq!
312
- # Recurse into dependencies
313
- list.each{|rec| eager_graph_make_associations_unique(rec, deps, alias_map, type_map)}
314
- end
307
+ # If the result set is the result of a cartesian product, then it is possible that
308
+ # there a multiple records for each association when there should only be one.
309
+ # In that case, for each object in all associations loaded via #eager_graph, run
310
+ # uniq! on the association instance variables to make sure no duplicate records show up.
311
+ # Note that this can cause legitimate duplicate records to be removed.
312
+ def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
313
+ records.each do |record|
314
+ dependency_map.each do |ta, deps|
315
+ list = if type_map[ta] == :many_to_one
316
+ item = record.send(alias_map[ta])
317
+ [item] if item
318
+ else
319
+ list = record.send(alias_map[ta])
320
+ list.uniq!
321
+ # Recurse into dependencies
322
+ list.each{|rec| eager_graph_make_associations_unique(rec, deps, alias_map, type_map)}
315
323
  end
316
324
  end
317
325
  end
326
+ end
318
327
 
319
- # Eagerly load all specified associations
320
- def eager_load(a)
321
- return if a.empty?
322
- # Current model class
323
- model = @opts[:models][nil]
324
- # All associations to eager load
325
- eager_assoc = @opts[:eager]
326
- # Key is foreign/primary key name symbol
327
- # Value is hash with keys being foreign/primary key values (generally integers)
328
- # and values being an array of current model objects with that
329
- # specific foreign/primary key
330
- key_hash = {}
331
- # array of attribute_values keys to monitor
332
- keys = []
333
- # Reflections for all associations to eager load
334
- reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}
328
+ # Eagerly load all specified associations
329
+ def eager_load(a)
330
+ return if a.empty?
331
+ # Current model class
332
+ model = @opts[:models][nil]
333
+ # All associations to eager load
334
+ eager_assoc = @opts[:eager]
335
+ # Key is foreign/primary key name symbol
336
+ # Value is hash with keys being foreign/primary key values (generally integers)
337
+ # and values being an array of current model objects with that
338
+ # specific foreign/primary key
339
+ key_hash = {}
340
+ # array of attribute_values keys to monitor
341
+ keys = []
342
+ # Reflections for all associations to eager load
343
+ reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}
335
344
 
336
- # Populate keys to monitor
337
- reflections.each do |reflection|
338
- key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key
339
- next if key_hash[key]
340
- key_hash[key] = {}
341
- keys << key
345
+ # Populate keys to monitor
346
+ reflections.each do |reflection|
347
+ key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key
348
+ next if key_hash[key]
349
+ key_hash[key] = {}
350
+ keys << key
351
+ end
352
+
353
+ # Associate each object with every key being monitored
354
+ a.each do |r|
355
+ keys.each do |key|
356
+ ((key_hash[key][r[key]] ||= []) << r) if r[key]
342
357
  end
343
-
344
- # Associate each object with every key being monitored
345
- a.each do |r|
346
- keys.each do |key|
347
- ((key_hash[key][r[key]] ||= []) << r) if r[key]
358
+ end
359
+
360
+ # Iterate through eager associations and assign instance variables
361
+ # for the association for all model objects
362
+ reflections.each do |reflection|
363
+ assoc_class = reflection.associated_class
364
+ assoc_name = reflection[:name]
365
+ assoc_iv = :"@#{assoc_name}"
366
+ # Proc for setting cascaded eager loading
367
+ assoc_block = Proc.new do |d|
368
+ if order = reflection[:order]
369
+ d = d.order(*order)
370
+ end
371
+ if c = eager_assoc[assoc_name]
372
+ d = d.eager(c)
373
+ end
374
+ if c = reflection[:eager]
375
+ d = d.eager(c)
376
+ end
377
+ if b = reflection[:eager_block]
378
+ d = b.call(d)
348
379
  end
380
+ d
349
381
  end
350
-
351
- # Iterate through eager associations and assign instance variables
352
- # for the association for all model objects
353
- reflections.each do |reflection|
354
- assoc_class = model.send(:associated_class, reflection)
355
- assoc_name = reflection[:name]
356
- # Proc for setting cascaded eager loading
357
- cascade = Proc.new do |d|
358
- if c = eager_assoc[assoc_name]
359
- d = d.eager(c)
382
+ case rtype = reflection[:type]
383
+ when :many_to_one
384
+ key = reflection[:key]
385
+ h = key_hash[key]
386
+ keys = h.keys
387
+ # No records have the foreign key set for this association, so skip it
388
+ next unless keys.length > 0
389
+ # Set the instance variable to null by default, so records that
390
+ # don't have a associated records will cache the negative lookup.
391
+ a.each do |object|
392
+ object.instance_variable_set(assoc_iv, :null)
360
393
  end
361
- if c = reflection[:eager]
362
- d = d.eager(c)
394
+ assoc_block.call(assoc_class.select(*reflection.select).filter(assoc_class.primary_key=>keys)).all do |assoc_object|
395
+ next unless objects = h[assoc_object.pk]
396
+ objects.each do |object|
397
+ object.instance_variable_set(assoc_iv, assoc_object)
398
+ end
363
399
  end
364
- d
365
- end
366
- case rtype = reflection[:type]
367
- when :many_to_one
368
- key = reflection[:key]
369
- h = key_hash[key]
370
- keys = h.keys
371
- # No records have the foreign key set for this association, so skip it
372
- next unless keys.length > 0
373
- ds = assoc_class.filter(assoc_class.primary_key=>keys)
374
- ds = cascade.call(ds)
375
- ds.all do |assoc_object|
376
- h[assoc_object.pk].each do |object|
377
- object.instance_variable_set(:"@#{assoc_name}", assoc_object)
378
- end
400
+ when :one_to_many, :many_to_many
401
+ h = key_hash[model.primary_key]
402
+ ds = if rtype == :one_to_many
403
+ fkey = reflection[:key]
404
+ reciprocal = reflection.reciprocal
405
+ assoc_class.select(*reflection.select).filter(fkey=>h.keys)
406
+ else
407
+ fkey = reflection[:left_key_alias]
408
+ assoc_class.select(*(Array(reflection.select)+Array(reflection[:left_key_select]))).inner_join(reflection[:join_table], [[reflection[:right_key], reflection.associated_primary_key], [reflection[:left_key], h.keys]])
409
+ end
410
+ h.values.each do |object_array|
411
+ object_array.each do |object|
412
+ object.instance_variable_set(assoc_iv, [])
379
413
  end
380
- when :one_to_many, :many_to_many
381
- if rtype == :one_to_many
382
- fkey = key = reflection[:key]
383
- h = key_hash[model.primary_key]
384
- reciprocal = model.send(:reciprocal_association, reflection)
385
- ds = assoc_class.filter(key=>h.keys)
414
+ end
415
+ assoc_block.call(ds).all do |assoc_object|
416
+ fk = if rtype == :many_to_many
417
+ assoc_object.values.delete(fkey)
386
418
  else
387
- assoc_table = assoc_class.table_name
388
- left = reflection[:left_key]
389
- right = reflection[:right_key]
390
- right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
391
- join_table = reflection[:join_table]
392
- fkey = (reflection[:left_key_alias] ||= :"x_foreign_key_x")
393
- table_selection = (reflection[:select] ||= assoc_table.*)
394
- key_selection = (reflection[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
395
- h = key_hash[model.primary_key]
396
- ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
397
- end
398
- if order = reflection[:order]
399
- ds = ds.order(order)
419
+ assoc_object[fkey]
400
420
  end
401
- ds = cascade.call(ds)
402
- ivar = :"@#{assoc_name}"
403
- h.values.each do |object_array|
404
- object_array.each do |object|
405
- object.instance_variable_set(ivar, [])
406
- end
421
+ next unless objects = h[fk]
422
+ objects.each do |object|
423
+ object.instance_variable_get(assoc_iv) << assoc_object
424
+ assoc_object.instance_variable_set(reciprocal, object) if reciprocal
407
425
  end
408
- ds.all do |assoc_object|
409
- fk = if rtype == :many_to_many
410
- assoc_object.values.delete(fkey)
411
- else
412
- assoc_object[fkey]
413
- end
414
- h[fk].each do |object|
415
- object.instance_variable_get(ivar) << assoc_object
416
- assoc_object.instance_variable_set(reciprocal, object) if reciprocal
417
- end
418
- end
419
- end
426
+ end
420
427
  end
421
428
  end
429
+ end
422
430
 
423
- # Build associations from the graph if .eager_graph was used,
424
- # and/or load other associations if .eager was used.
425
- def post_load(all_records)
426
- eager_graph_build_associations(all_records) if @opts[:eager_graph]
427
- eager_load(all_records) if @opts[:eager]
428
- end
431
+ # Build associations from the graph if #eager_graph was used,
432
+ # and/or load other associations if #eager was used.
433
+ def post_load(all_records)
434
+ eager_graph_build_associations(all_records) if @opts[:eager_graph]
435
+ eager_load(all_records) if @opts[:eager]
436
+ end
429
437
  end