| Class | Sequel::Model::Associations::AssociationReflection |
| In: |
lib/sequel/model/associations.rb
|
| Parent: | Hash |
AssociationReflection is a Hash subclass that keeps information on Sequel::Model associations. It provides methods to reduce internal code duplication. It should not be instantiated by the user.
| ASSOCIATION_DATASET_PROC | = | proc{|r| r.association_dataset_for(self)} | ||
| FINALIZE_SETTINGS | = | { :associated_class=>:class, :associated_dataset=>:_dataset, :associated_eager_dataset=>:associated_eager_dataset, :eager_limit_strategy=>:_eager_limit_strategy, :filter_by_associations_conditions_dataset=>:filter_by_associations_conditions_dataset, :placeholder_loader=>:placeholder_loader, :predicate_key=>:predicate_key, :predicate_keys=>:predicate_keys, :reciprocal=>:reciprocal, }.freeze | Map of methods to cache keys used for finalizing associations. |
Name symbol for the _add internal association method
# File lib/sequel/model/associations.rb, line 36
36: def _add_method
37: self[:_add_method]
38: end
Name symbol for the _remove_all internal association method
# File lib/sequel/model/associations.rb, line 41
41: def _remove_all_method
42: self[:_remove_all_method]
43: end
Name symbol for the _remove internal association method
# File lib/sequel/model/associations.rb, line 46
46: def _remove_method
47: self[:_remove_method]
48: end
Name symbol for the _setter association method
# File lib/sequel/model/associations.rb, line 51
51: def _setter_method
52: self[:_setter_method]
53: end
Name symbol for the add association method
# File lib/sequel/model/associations.rb, line 56
56: def add_method
57: self[:add_method]
58: end
Apply all non-instance specific changes to the given dataset and return it.
# File lib/sequel/model/associations.rb, line 84
84: def apply_dataset_changes(ds)
85: ds = ds.with_extend(AssociationDatasetMethods).clone(:association_reflection => self)
86: if exts = self[:reverse_extend]
87: ds = ds.with_extend(*exts)
88: end
89: ds = ds.select(*select) if select
90: if c = self[:conditions]
91: ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.where(*c) : ds.where(c)
92: end
93: ds = ds.order(*self[:order]) if self[:order]
94: ds = ds.limit(*self[:limit]) if self[:limit]
95: ds = ds.limit(1).skip_limit_check if limit_to_single_row?
96: ds = ds.eager(self[:eager]) if self[:eager]
97: ds = ds.distinct if self[:distinct]
98: ds
99: end
Use DISTINCT ON and ORDER BY clauses to limit the results to the first record with matching keys.
# File lib/sequel/model/associations.rb, line 138
138: def apply_distinct_on_eager_limit_strategy(ds)
139: keys = predicate_key
140: ds.distinct(*keys).order_prepend(*keys)
141: end
Apply all non-instance specific changes and the eager_block option to the given dataset and return it.
# File lib/sequel/model/associations.rb, line 103
103: def apply_eager_dataset_changes(ds)
104: ds = apply_dataset_changes(ds)
105: if block = self[:eager_block]
106: ds = block.call(ds)
107: end
108: ds
109: end
Apply the eager graph limit strategy to the dataset to graph into the current dataset, or return the dataset unmodified if no SQL limit strategy is needed.
# File lib/sequel/model/associations.rb, line 113
113: def apply_eager_graph_limit_strategy(strategy, ds)
114: case strategy
115: when :distinct_on
116: apply_distinct_on_eager_limit_strategy(ds.order_prepend(*self[:order]))
117: when :window_function
118: apply_window_function_eager_limit_strategy(ds.order_prepend(*self[:order])).select(*ds.columns)
119: else
120: ds
121: end
122: end
Apply an eager limit strategy to the dataset, or return the dataset unmodified if it doesn‘t need an eager limit strategy.
# File lib/sequel/model/associations.rb, line 126
126: def apply_eager_limit_strategy(ds, strategy=eager_limit_strategy, limit_and_offset=limit_and_offset())
127: case strategy
128: when :distinct_on
129: apply_distinct_on_eager_limit_strategy(ds)
130: when :window_function
131: apply_window_function_eager_limit_strategy(ds, limit_and_offset)
132: else
133: ds
134: end
135: end
If the ruby eager limit strategy is being used, slice the array using the slice range to return the object(s) at the correct offset/limit.
# File lib/sequel/model/associations.rb, line 165
165: def apply_ruby_eager_limit_strategy(rows, limit_and_offset = limit_and_offset())
166: name = self[:name]
167: if returns_array?
168: range = slice_range(limit_and_offset)
169: rows.each{|o| o.associations[name] = o.associations[name][range] || []}
170: elsif sr = slice_range(limit_and_offset)
171: offset = sr.begin
172: rows.each{|o| o.associations[name] = o.associations[name][offset]}
173: end
174: end
Use a window function to limit the results of the eager loading dataset.
# File lib/sequel/model/associations.rb, line 144
144: def apply_window_function_eager_limit_strategy(ds, limit_and_offset=limit_and_offset())
145: rn = ds.row_number_column
146: limit, offset = limit_and_offset
147: ds = ds.unordered.select_append{|o| o.row_number.function.over(:partition=>predicate_key, :order=>ds.opts[:order]).as(rn)}.from_self
148: ds = ds.order(rn) if ds.db.database_type == :mysql
149: ds = if !returns_array?
150: ds.where(rn => offset ? offset+1 : 1)
151: elsif offset
152: offset += 1
153: if limit
154: ds.where(rn => (offset...(offset+limit)))
155: else
156: ds.where{SQL::Identifier.new(rn) >= offset}
157: end
158: else
159: ds.where{SQL::Identifier.new(rn) <= limit}
160: end
161: end
Whether the associations cache should use an array when storing the associated records during eager loading.
# File lib/sequel/model/associations.rb, line 178
178: def assign_singular?
179: !returns_array?
180: end
The class associated to the current model class via this association
# File lib/sequel/model/associations.rb, line 66
66: def associated_class
67: cached_fetch(:class) do
68: begin
69: constantize(self[:class_name])
70: rescue NameError => e
71: raise NameError, "#{e.message} (this happened when attempting to find the associated class for #{inspect})", e.backtrace
72: end
73: end
74: end
The dataset associated via this association, with the non-instance specific changes already applied. This will be a joined dataset if the association requires joining tables.
# File lib/sequel/model/associations.rb, line 79
79: def associated_dataset
80: cached_fetch(:_dataset){apply_dataset_changes(_associated_dataset)}
81: end
Return an dataset that will load the appropriate associated objects for the given object using this association.
# File lib/sequel/model/associations.rb, line 215
215: def association_dataset_for(object)
216: condition = if can_have_associated_objects?(object)
217: predicate_keys.zip(predicate_key_values(object))
218: else
219: false
220: end
221:
222: associated_dataset.where(condition)
223: end
Proc used to create the association dataset method.
# File lib/sequel/model/associations.rb, line 227
227: def association_dataset_proc
228: ASSOCIATION_DATASET_PROC
229: end
Name symbol for association method, the same as the name of the association.
# File lib/sequel/model/associations.rb, line 61
61: def association_method
62: self[:name]
63: end
Whether this association can have associated objects, given the current object. Should be false if obj cannot have associated objects because the necessary key columns are NULL.
# File lib/sequel/model/associations.rb, line 185
185: def can_have_associated_objects?(obj)
186: true
187: end
Whether you are able to clone from the given association type to the current association type, true by default only if the types match.
# File lib/sequel/model/associations.rb, line 191
191: def cloneable?(ref)
192: ref[:type] == self[:type]
193: end
Name symbol for the dataset association method
# File lib/sequel/model/associations.rb, line 196
196: def dataset_method
197: self[:dataset_method]
198: end
Whether the dataset needs a primary key to function, true by default.
# File lib/sequel/model/associations.rb, line 201
201: def dataset_need_primary_key?
202: true
203: end
Return the symbol used for the row number column if the window function eager limit strategy is being used, or nil otherwise.
# File lib/sequel/model/associations.rb, line 207
207: def delete_row_number_column(ds=associated_dataset)
208: if eager_limit_strategy == :window_function
209: ds.row_number_column
210: end
211: end
Whether to eagerly graph a lazy dataset, true by default. If this is false, the association won‘t respect the :eager_graph option when loading the association for a single record.
# File lib/sequel/model/associations.rb, line 334
334: def eager_graph_lazy_dataset?
335: true
336: end
The eager_graph limit strategy to use for this dataset
# File lib/sequel/model/associations.rb, line 232
232: def eager_graph_limit_strategy(strategy)
233: if self[:limit] || !returns_array?
234: strategy = strategy[self[:name]] if strategy.is_a?(Hash)
235: case strategy
236: when true
237: true_eager_graph_limit_strategy
238: when Symbol
239: strategy
240: else
241: if returns_array? || offset
242: :ruby
243: end
244: end
245: end
246: end
The eager limit strategy to use for this dataset.
# File lib/sequel/model/associations.rb, line 249
249: def eager_limit_strategy
250: cached_fetch(:_eager_limit_strategy) do
251: if self[:limit] || !returns_array?
252: case s = cached_fetch(:eager_limit_strategy){default_eager_limit_strategy}
253: when true
254: true_eager_limit_strategy
255: else
256: s
257: end
258: end
259: end
260: end
Eager load the associated objects using the hash of eager options, yielding each row to the block.
# File lib/sequel/model/associations.rb, line 264
264: def eager_load_results(eo, &block)
265: rows = eo[:rows]
266: initialize_association_cache(rows) unless eo[:initialize_rows] == false
267: if eo[:id_map]
268: ids = eo[:id_map].keys
269: return ids if ids.empty?
270: end
271: strategy = eager_limit_strategy
272: cascade = eo[:associations]
273: eager_limit = nil
274:
275: if eo[:eager_block] || eo[:loader] == false
276: ds = eager_loading_dataset(eo)
277:
278: strategy = ds.opts[:eager_limit_strategy] || strategy
279:
280: eager_limit =
281: if el = ds.opts[:eager_limit]
282: raise Error, "The :eager_limit dataset option is not supported for associations returning a single record" unless returns_array?
283: strategy ||= true_eager_graph_limit_strategy
284: if el.is_a?(Array)
285: el
286: else
287: [el, nil]
288: end
289: else
290: limit_and_offset
291: end
292:
293: strategy = true_eager_graph_limit_strategy if strategy == :union
294: # Correlated subqueries are not supported for regular eager loading
295: strategy = :ruby if strategy == :correlated_subquery
296: strategy = nil if strategy == :ruby && assign_singular?
297: objects = apply_eager_limit_strategy(ds, strategy, eager_limit).all
298: elsif strategy == :union
299: objects = []
300: ds = associated_dataset
301: loader = union_eager_loader
302: joiner = " UNION ALL "
303: ids.each_slice(subqueries_per_union).each do |slice|
304: objects.concat(ds.with_sql(slice.map{|k| loader.sql(*k)}.join(joiner)).to_a)
305: end
306: ds = ds.eager(cascade) if cascade
307: ds.send(:post_load, objects)
308: else
309: loader = placeholder_eager_loader
310: loader = loader.with_dataset{|dataset| dataset.eager(cascade)} if cascade
311: objects = loader.all(ids)
312: end
313:
314: objects.each(&block)
315: if strategy == :ruby
316: apply_ruby_eager_limit_strategy(rows, eager_limit || limit_and_offset)
317: end
318: end
The key to use for the key hash when eager loading
# File lib/sequel/model/associations.rb, line 321
321: def eager_loader_key
322: self[:eager_loader_key]
323: end
Whether additional conditions should be added when using the filter by associations support.
# File lib/sequel/model/associations.rb, line 340
340: def filter_by_associations_add_conditions?
341: self[:conditions] || self[:eager_block] || self[:limit]
342: end
The expression to use for the additional conditions to be added for the filter by association support, when the association itself is filtered. Works by using a subquery to test that the objects passed also meet the association filter criteria.
# File lib/sequel/model/associations.rb, line 348
348: def filter_by_associations_conditions_expression(obj)
349: ds = filter_by_associations_conditions_dataset.where(filter_by_associations_conditions_subquery_conditions(obj))
350: {filter_by_associations_conditions_key=>ds}
351: end
Finalize the association by first attempting to populate the thread-safe cache, and then transfering the thread-safe cache value to the association itself, so that a mutex is not needed to get the value.
# File lib/sequel/model/associations.rb, line 356
356: def finalize
357: return unless cache = self[:cache]
358:
359: finalize_settings.each do |meth, key|
360: next if has_key?(key)
361:
362: # Allow calling private methods to make sure caching is done appropriately
363: send(meth)
364: self[key] = cache.delete(key) if cache.has_key?(key)
365: end
366:
367: nil
368: end
# File lib/sequel/model/associations.rb, line 382
382: def finalize_settings
383: FINALIZE_SETTINGS
384: end
Whether to handle silent modification failure when adding/removing associated records, false by default.
# File lib/sequel/model/associations.rb, line 388
388: def handle_silent_modification_failure?
389: false
390: end
Initialize the associations cache for the current association for the given objects.
# File lib/sequel/model/associations.rb, line 393
393: def initialize_association_cache(objects)
394: name = self[:name]
395: if assign_singular?
396: objects.each{|object| object.associations[name] = nil}
397: else
398: objects.each{|object| object.associations[name] = []}
399: end
400: end
Show which type of reflection this is, and a guess at what code was used to create the association.
# File lib/sequel/model/associations.rb, line 404
404: def inspect
405: o = self[:orig_opts].dup
406: o.delete(:class)
407: o.delete(:class_name)
408: o.delete(:block) unless o[:block]
409: o[:class] = self[:orig_class] if self[:orig_class]
410:
411: "#<#{self.class} #{self[:model]}.#{self[:type]} #{self[:name].inspect}#{", #{o.inspect[1...-1]}" unless o.empty?}>"
412: end
The limit and offset for this association (returned as a two element array).
# File lib/sequel/model/associations.rb, line 415
415: def limit_and_offset
416: if (v = self[:limit]).is_a?(Array)
417: v
418: else
419: [v, nil]
420: end
421: end
Whether the associated object needs a primary key to be added/removed, false by default.
# File lib/sequel/model/associations.rb, line 425
425: def need_associated_primary_key?
426: false
427: end
A placeholder literalizer that can be used to lazily load the association. If one can‘t be used, returns nil.
# File lib/sequel/model/associations.rb, line 431
431: def placeholder_loader
432: if use_placeholder_loader?
433: cached_fetch(:placeholder_loader) do
434: Sequel::Dataset::PlaceholderLiteralizer.loader(associated_dataset) do |pl, ds|
435: ds.where(Sequel.&(*predicate_keys.map{|k| SQL::BooleanExpression.new('=''=', k, pl.arg)}))
436: end
437: end
438: end
439: end
The values that predicate_keys should match for objects to be associated.
# File lib/sequel/model/associations.rb, line 447
447: def predicate_key_values(object)
448: predicate_key_methods.map{|k| object.get_column_value(k)}
449: end
The keys to use for loading of the regular dataset, as an array.
# File lib/sequel/model/associations.rb, line 442
442: def predicate_keys
443: cached_fetch(:predicate_keys){Array(predicate_key)}
444: end
Qualify col with the given table name.
# File lib/sequel/model/associations.rb, line 452
452: def qualify(table, col)
453: transform(col) do |k|
454: case k
455: when Symbol, SQL::Identifier
456: SQL::QualifiedIdentifier.new(table, k)
457: else
458: Sequel::Qualifier.new(table).transform(k)
459: end
460: end
461: end
Qualify col with the associated model‘s table name.
# File lib/sequel/model/associations.rb, line 464
464: def qualify_assoc(col)
465: qualify(associated_class.table_name, col)
466: end
Qualify col with the current model‘s table name.
# File lib/sequel/model/associations.rb, line 469
469: def qualify_cur(col)
470: qualify(self[:model].table_name, col)
471: end
Returns the reciprocal association variable, if one exists. The reciprocal association is the association in the associated class that is the opposite of the current association. For example, Album.many_to_one :artist and Artist.one_to_many :albums are reciprocal associations. This information is to populate reciprocal associations. For example, when you do this_artist.add_album(album) it sets album.artist to this_artist.
# File lib/sequel/model/associations.rb, line 479
479: def reciprocal
480: cached_fetch(:reciprocal) do
481: possible_recips = []
482:
483: associated_class.all_association_reflections.each do |assoc_reflect|
484: if reciprocal_association?(assoc_reflect)
485: possible_recips << assoc_reflect
486: end
487: end
488:
489: if possible_recips.length == 1
490: cached_set(:reciprocal_type, possible_recips.first[:type]) if ambiguous_reciprocal_type?
491: possible_recips.first[:name]
492: end
493: end
494: end
Whether the reciprocal of this association returns an array of objects instead of a single object, true by default.
# File lib/sequel/model/associations.rb, line 498
498: def reciprocal_array?
499: true
500: end
Name symbol for the remove_all_ association method
# File lib/sequel/model/associations.rb, line 503
503: def remove_all_method
504: self[:remove_all_method]
505: end
Whether associated objects need to be removed from the association before being destroyed in order to preserve referential integrity.
# File lib/sequel/model/associations.rb, line 509
509: def remove_before_destroy?
510: true
511: end
Name symbol for the remove_ association method
# File lib/sequel/model/associations.rb, line 514
514: def remove_method
515: self[:remove_method]
516: end
Whether to check that an object to be disassociated is already associated to this object, false by default.
# File lib/sequel/model/associations.rb, line 519
519: def remove_should_check_existing?
520: false
521: end
Whether this association returns an array of objects instead of a single object, true by default.
# File lib/sequel/model/associations.rb, line 525
525: def returns_array?
526: true
527: end
Whether to set the reciprocal association to self when loading associated records, false by default.
# File lib/sequel/model/associations.rb, line 536
536: def set_reciprocal_to_self?
537: false
538: end
Name symbol for the setter association method
# File lib/sequel/model/associations.rb, line 541
541: def setter_method
542: self[:setter_method]
543: end
The range used for slicing when using the :ruby eager limit strategy.
# File lib/sequel/model/associations.rb, line 546
546: def slice_range(limit_and_offset = limit_and_offset())
547: limit, offset = limit_and_offset
548: if limit || offset
549: (offset||0)..(limit ? (offset||0)+limit-1 : -1)
550: end
551: end