sequel 1.5.1 → 2.0.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.
@@ -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