sequel 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,26 @@
1
1
  module Sequel
2
2
  class Model
3
+ # If possible, set the dataset for the model subclass as soon as it
4
+ # is created.
5
+ def self.inherited(subclass)
6
+ begin
7
+ if subclass.superclass == Model
8
+ unless subclass.name.empty?
9
+ subclass.set_dataset(Model.db[subclass.implicit_table_name])
10
+ end
11
+ elsif ds = subclass.superclass.instance_variable_get(:@dataset)
12
+ subclass.set_dataset(ds.clone)
13
+ end
14
+ rescue StandardError
15
+ end
16
+ end
17
+
3
18
  # Returns the database associated with the Model class.
4
19
  def self.db
5
- @db ||= (superclass != Object) && superclass.db or
6
- raise Error, "No database associated with #{self}"
20
+ return @db if @db
21
+ @db = self == Model ? ::Sequel::DATABASES.first : superclass.db
22
+ raise(Error, "No database associated with #{self}") unless @db
23
+ @db
7
24
  end
8
25
 
9
26
  # Sets the database associated with the Model class.
@@ -14,12 +31,6 @@ module Sequel
14
31
  end
15
32
  end
16
33
 
17
- # Called when a database is opened in order to automatically associate the
18
- # first opened database with model classes.
19
- def self.database_opened(db)
20
- @db = db if (self == Model) && !@db
21
- end
22
-
23
34
  # Returns the implicit table name for the model class.
24
35
  def self.implicit_table_name
25
36
  name.demodulize.underscore.pluralize.to_sym
@@ -27,34 +38,39 @@ module Sequel
27
38
 
28
39
  # Returns the dataset associated with the Model class.
29
40
  def self.dataset
30
- unless @dataset
31
- if ds = super_dataset
32
- set_dataset(ds.clone)
33
- elsif !name.empty?
34
- set_dataset(db[implicit_table_name])
35
- else
36
- raise Error, "No dataset associated with #{self}"
37
- end
38
- end
39
- @dataset
41
+ @dataset || raise(Error, "No dataset associated with #{self}")
40
42
  end
41
-
42
- # def self.dataset
43
- # @dataset ||= super_dataset ||
44
- # (!(n = name).empty? && db[n.underscore.pluralize.to_sym]) ||
45
- # (raise Error, "No dataset associated with #{self}")
46
- # end
47
-
48
- def self.super_dataset # :nodoc:
49
- superclass.dataset if (superclass != Sequel::Model) && superclass.respond_to?(:dataset)
43
+
44
+ # If a block is given, define a method on the dataset with the given argument name using
45
+ # the given block as well as a method on the model that calls the
46
+ # dataset method.
47
+ #
48
+ # If a block is not given, define a method on the model for each argument
49
+ # that calls the dataset method of the same argument name.
50
+ def self.def_dataset_method(*args, &block)
51
+ raise(Error, "No arguments given") if args.empty?
52
+ if block_given?
53
+ raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
54
+ dataset.meta_def(args.first, &block)
55
+ end
56
+ args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
50
57
  end
51
58
 
52
59
  # Returns the columns in the result set in their original order.
53
60
  #
54
61
  # See Dataset#columns for more information.
55
62
  def self.columns
56
- @columns ||= dataset.columns or
63
+ return @columns if @columns
64
+ @columns = dataset.naked.columns or
57
65
  raise Error, "Could not fetch columns for #{self}"
66
+ def_column_accessor(*@columns)
67
+ @str_columns = nil
68
+ @columns
69
+ end
70
+
71
+ # Returns the columns as a list of frozen strings.
72
+ def self.str_columns
73
+ @str_columns ||= columns.map{|c| c.to_s.freeze}
58
74
  end
59
75
 
60
76
  # Sets the dataset associated with the Model class.
@@ -64,6 +80,32 @@ module Sequel
64
80
  @dataset.set_model(self)
65
81
  @dataset.extend(Associations::EagerLoading)
66
82
  @dataset.transform(@transform) if @transform
83
+ begin
84
+ @columns = nil
85
+ columns
86
+ rescue StandardError
87
+ end
88
+ end
89
+ class << self; alias :dataset= :set_dataset; end
90
+
91
+ class << self
92
+ private
93
+ def def_column_accessor(*columns)
94
+ Thread.exclusive do
95
+ columns.each do |column|
96
+ im = instance_methods
97
+ meth = "#{column}="
98
+ define_method(column){self[column]} unless im.include?(column.to_s)
99
+ unless im.include?(meth)
100
+ define_method(meth) do |*v|
101
+ len = v.length
102
+ raise(ArgumentError, "wrong number of arguments (#{len} for 1)") unless len == 1
103
+ self[column] = v.first
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
67
109
  end
68
110
 
69
111
  # Returns the database assoiated with the object's Model class.
@@ -83,6 +125,11 @@ module Sequel
83
125
  model.columns
84
126
  end
85
127
 
128
+ # Returns the str_columns associated with the object's Model class.
129
+ def str_columns
130
+ model.str_columns
131
+ end
132
+
86
133
  # Serializes column with YAML or through marshalling.
87
134
  def self.serialize(*columns)
88
135
  format = columns.pop[:format] if Hash === columns.last
@@ -108,13 +155,11 @@ module Sequel
108
155
  # # ...
109
156
  #
110
157
  # end
158
+ @models = {}
111
159
  def self.Model(source)
112
- @models ||= {}
113
- @models[source] ||= Class.new(Sequel::Model) do
114
- meta_def(:inherited) do |c|
115
- c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
116
- end
117
- end
160
+ return @models[source] if @models[source]
161
+ klass = Class.new(Sequel::Model)
162
+ klass.set_dataset(source.is_a?(Dataset) ? source : Model.db[source])
163
+ @models[source] = klass
118
164
  end
119
-
120
165
  end
@@ -18,8 +18,8 @@ module Sequel
18
18
  obj
19
19
  end
20
20
 
21
- class_def(:set) {|v| store.delete(cache_key); super}
22
- class_def(:save) {store.delete(cache_key); super}
21
+ class_def(:update_values) {|v| store.delete(cache_key); super}
22
+ class_def(:save) {store.delete(cache_key) unless new?; super}
23
23
  class_def(:delete) {store.delete(cache_key); super}
24
24
  end
25
25
 
@@ -39,4 +39,4 @@ module Sequel
39
39
  "#{self}:#{values.join(',')}"
40
40
  end
41
41
  end
42
- end
42
+ end
@@ -0,0 +1,81 @@
1
+ module Sequel
2
+ class Model
3
+ include Sequel::Deprecation
4
+ extend Sequel::Deprecation
5
+
6
+ # Check the Model.associate method to remove the :from option
7
+
8
+ def self.is_dataset_magic_method?(m) #:nodoc:
9
+ method_name = m.to_s
10
+ Sequel::Dataset::MAGIC_METHODS.each_key do |r|
11
+ return true if method_name =~ r
12
+ end
13
+ false
14
+ end
15
+
16
+ def self.method_missing(m, *args, &block) #:nodoc:
17
+ Thread.exclusive do
18
+ if dataset.respond_to?(m) || is_dataset_magic_method?(m)
19
+ instance_eval("def #{m}(*args, &block); deprecate('Sequel::Model.method_missing', 'Please define Sequel::Model.#{m} or use def_dataset_method :#{m}'); dataset.#{m}(*args, &block); end")
20
+ end
21
+ end
22
+ respond_to?(m) ? send(m, *args, &block) : super(m, *args)
23
+ end
24
+
25
+ def method_missing(m, *args, &block) #:nodoc:
26
+ if m.to_s =~ /=\z/
27
+ attribute = m.to_s.chop
28
+ values.keys.each do |k|
29
+ next unless k.to_s == attribute
30
+ deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}] = ...")
31
+ return self[attribute.to_sym] = args.first
32
+ end
33
+ super
34
+ else
35
+ attribute = m.to_s
36
+ values.keys.each do |k|
37
+ next unless k.to_s == attribute
38
+ deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}]")
39
+ return self[attribute.to_sym]
40
+ end
41
+ super
42
+ end
43
+ end
44
+
45
+ def self.create_with_params(params) #:nodoc:
46
+ deprecate("Sequel::Model.create_with_params", "Use .create")
47
+ create(params)
48
+ end
49
+
50
+ def self.create_with(params) #:nodoc:
51
+ deprecate("Sequel::Model.create_with", "Use .create")
52
+ create(params)
53
+ end
54
+
55
+ def update_with(params) #:nodoc:
56
+ deprecate("Sequel::Model#update_with", "Use #update_with_params")
57
+ update_with_params(params)
58
+ end
59
+
60
+ def new_record? #:nodoc:
61
+ deprecate("Sequel::Model#new_record?", "Use #new?")
62
+ new?
63
+ end
64
+
65
+ def set(values) #:nodoc:
66
+ deprecate("Sequel::Model#set", "Use #update_values")
67
+ update_values(values)
68
+ end
69
+
70
+ def update(values) #:nodoc:
71
+ deprecate("Sequel::Model#update", "Use #update_values")
72
+ update_values(values)
73
+ end
74
+
75
+ # deprecated, please use many_to_one instead
76
+ def self.one_to_one(*args, &block) #:nodoc:
77
+ deprecate("Sequel::Model.one_to_one", "Use many_to_one")
78
+ many_to_one(*args, &block)
79
+ end
80
+ end
81
+ end
@@ -1,68 +1,321 @@
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
- # The basic idea for how it works is that the dataset is first loaded normally.
5
- # Then it goes through all associations that have been specified via .eager.
6
- # It loads each of those associations separately, then associates them back
7
- # to the original dataset via primary/foreign keys. Due to the necessity of
8
- # all objects being present, you need to use .all to use eager loading, as it
9
- # can't work with .each.
10
- #
11
- # This implementation avoids the complexity of extracting an object graph out
12
- # of a single dataset, by building the object graph out of multiple datasets,
13
- # one for each association. By using a separate dataset for each association,
14
- # it avoids problems such as aliasing conflicts and creating cartesian product
15
- # result sets if multiple *_to_many eager associations are requested.
16
- #
17
- # One limitation of using this method is that you cannot filter the dataset
18
- # based on values of columns in an associated table, since the associations are loaded
19
- # in separate queries. To do that you need to load all associations in the
20
- # same query, and extract an object graph from the results of that query.
4
+ # Two separate implementations are provided. .eager should be used most of the
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
9
+ # *_to_many associations are joined.
21
10
  #
22
11
  # You can cascade the eager loading (loading associations' associations)
23
- # with no limit to the depth of the cascades. You do this by passing a hash to .eager
12
+ # with no limit to the depth of the cascades. You do this by passing a hash to .eager or .eager_graph
24
13
  # with the keys being associations of the current model and values being
25
14
  # associations of the model associated with the current model via the key.
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.
26
18
  #
27
- # The associations' order, if defined, is respected. You cannot eagerly load
28
- # an association with a block argument, as the block argument is evaluated in
29
- # terms of a specific instance of the model, and no specific instance exists
30
- # when eagerly loading.
19
+ # The arguments can be symbols or hashes with symbol keys (for cascaded
20
+ # eager loading). Examples:
21
+ #
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
31
32
  module Sequel::Model::Associations::EagerLoading
32
- # Add associations to the list of associations to eagerly load.
33
- # Associations can be a symbol or a hash with symbol keys (for cascaded
34
- # eager loading). Examples:
33
+ # Add the .eager! and .eager_graph! mutation methods to the dataset.
34
+ def self.extended(obj)
35
+ obj.def_mutation_method(:eager, :eager_graph)
36
+ end
37
+
38
+ # The preferred eager loading method. Loads all associated records using one
39
+ # query for each association.
40
+ #
41
+ # 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.
43
+ # It loads each of those associations separately, then associates them back
44
+ # to the original dataset via primary/foreign keys. Due to the necessity of
45
+ # all objects being present, you need to use .all to use eager loading, as it
46
+ # can't work with .each.
47
+ #
48
+ # This implementation avoids the complexity of extracting an object graph out
49
+ # of a single dataset, by building the object graph out of multiple datasets,
50
+ # one for each association. By using a separate dataset for each association,
51
+ # it avoids problems such as aliasing conflicts and creating cartesian product
52
+ # result sets if multiple *_to_many eager associations are requested.
53
+ #
54
+ # One limitation of using this method is that you cannot filter the dataset
55
+ # based on values of columns in an associated table, since the associations are loaded
56
+ # in separate queries. To do that you need to load all associations in the
57
+ # 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
59
+ # or join the tables you need to filter on manually.
35
60
  #
36
- # Album.eager(:artist).all
37
- # Album.eager(:artist, :genre).all
38
- # Album.eager(:artist).eager(:genre).all
39
- # Artist.eager(:albums=>:tracks).all
40
- # Artist.eager(:albums=>{:tracks=>:genre}).all
61
+ # Each association's order, if definied, is respected. Eager also works
62
+ # on a limited dataset.
41
63
  def eager(*associations)
42
- raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
64
+ model = check_model
43
65
  opt = @opts[:eager]
44
66
  opt = opt ? opt.dup : {}
45
- check = Proc.new do |a|
46
- raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(a)
47
- raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
48
- end
49
67
  associations.flatten.each do |association|
50
68
  case association
51
69
  when Symbol
52
- check.call(association)
70
+ check_association(model, association)
53
71
  opt[association] = nil
54
72
  when Hash
55
- association.keys.each{|assoc| check.call(assoc)}
73
+ association.keys.each{|assoc| check_association(model, assoc)}
56
74
  opt.merge!(association)
57
75
  else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
58
76
  end
59
77
  end
60
- ds = clone(:eager=>opt)
61
- ds.add_callback(:post_load, :eager_load) unless @opts[:eager]
62
- ds
78
+ clone(:eager=>opt)
79
+ end
80
+
81
+ # The secondary eager loading method. Loads all associations in a single query. This
82
+ # method should only be used if you need to filter based on columns in associated tables.
83
+ #
84
+ # This method builds an object graph using the .graph method. Then it uses the graph
85
+ # to build the associations, and finally replaces the graph with a simple array
86
+ # of model objects.
87
+ #
88
+ # Be very careful when using this with multiple *_to_many associations, as you can
89
+ # create large cartesian products. If you must graph multiple *_to_many associations,
90
+ # make sure your filters are specific if you have a large database.
91
+ #
92
+ # This does not respect each association's order, as all associations are loaded in
93
+ # a single query. If you want to order the results, you must manually call .order.
94
+ #
95
+ # eager_graph probably won't work the way you suspect with limit, unless you are
96
+ # only graphing many_to_one associations.
97
+ def eager_graph(*associations)
98
+ model = check_model
99
+ table_name = model.table_name
100
+ ds = if @opts[:eager_graph]
101
+ self
102
+ else
103
+ # Each of the following have a symbol key for the table alias, with the following values:
104
+ # :requirements - array of requirements for this association
105
+ # :alias_association_type_map - the type of association for this association
106
+ # :alias_association_name_map - the name of the association for this association
107
+ clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}})
108
+ end
109
+ ds.eager_graph_associations(ds, model, table_name, [], *associations)
63
110
  end
64
111
 
112
+ 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?
146
+ ds
147
+ 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')
170
+ end
171
+ end
172
+ ds
173
+ end
174
+
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]
184
+
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] = {}
203
+ end
204
+ end
205
+
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] = {}}
215
+
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)
230
+ 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)
237
+ end
238
+
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
252
+ end
253
+ end
254
+
65
255
  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
+
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
267
+ 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]}", []) unless type_map[ta] == :many_to_one || current.instance_variable_get("@#{alias_map[ta]}")
276
+ end
277
+ dependency_map.each do |ta, deps|
278
+ 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
295
+ 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
+ end
299
+ end
300
+
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
315
+ end
316
+ end
317
+ end
318
+
66
319
  # Eagerly load all specified associations
67
320
  def eager_load(a)
68
321
  return if a.empty?
@@ -136,9 +389,9 @@ module Sequel::Model::Associations::EagerLoading
136
389
  right = reflection[:right_key]
137
390
  right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
138
391
  join_table = reflection[:join_table]
139
- fkey = (opts[:left_key_alias] ||= :"x_foreign_key_x")
140
- table_selection = (opts[:select] ||= assoc_table.all)
141
- key_selection = (opts[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
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}")
142
395
  h = key_hash[model.primary_key]
143
396
  ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
144
397
  end
@@ -166,4 +419,11 @@ module Sequel::Model::Associations::EagerLoading
166
419
  end
167
420
  end
168
421
  end
422
+
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
169
429
  end