module Sequel::Plugins::ManyThroughMany::ClassMethods

Public Instance Methods

many_through_many(name, through, opts=OPTS, &block) click to toggle source

Create a many_through_many association. Arguments:

name

Same as associate, the name of the association.

through

The tables and keys to join between the current table and the associated table. Must be an array, with elements that are either 3 element arrays, or hashes with keys :table, :left, and :right. The required entries in the array/hash are:

:table (first array element)

The name of the table to join.

:left (middle array element)

The key joining the table to the previous table. Can use an array of symbols for a composite key association.

:right (last array element)

The key joining the table to the next table. Can use an array of symbols for a composite key association.

If a hash is provided, the following keys are respected when using eager_graph:

:db

The Database containing the table. This changes lookup to use a separate query for each join table.

:block

A proc to use as the block argument to join.

:conditions

Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.

:join_type

The join type to use for the join, defaults to :left_outer.

:only_conditions

Conditions to use for the join instead of the ones specified by the keys.

opts

The options for the associaion. Takes the same options as many_to_many.

    # File lib/sequel/plugins/many_through_many.rb
247 def many_through_many(name, through, opts=OPTS, &block)
248   associate(:many_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
249 end
one_through_many(name, through, opts=OPTS, &block) click to toggle source

Creates a one_through_many association. See many_through_many for arguments.

    # File lib/sequel/plugins/many_through_many.rb
252 def one_through_many(name, through, opts=OPTS, &block)
253   associate(:one_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
254 end

Private Instance Methods

def_many_through_many(opts) click to toggle source

Create the association methods and :eager_loader and :eager_grapher procs.

    # File lib/sequel/plugins/many_through_many.rb
259 def def_many_through_many(opts)
260   one_through_many = opts[:type] == :one_through_many
261   opts[:read_only] = true
262   if opts[:uniq]
263     opts[:after_load] ||= []
264     opts[:after_load].unshift(:array_uniq!)
265   end
266   opts[:cartesian_product_number] ||= one_through_many ? 0 : 2
267   separate_query_per_table = false
268   through = opts[:through] = opts[:through].map do |e|
269     case e
270     when Array
271       raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
272       {:table=>e[0], :left=>e[1], :right=>e[2]}
273     when Hash
274       raise(Error, "hash elements of the through option/argument for many_through_many associations must contain :table, :left, and :right keys") unless e[:table] && e[:left] && e[:right]
275       separate_query_per_table = true if e[:db]
276       e
277     else
278       raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
279     end
280   end
281   opts[:separate_query_per_table] = separate_query_per_table
282 
283   left_key = opts[:left_key] = opts[:through].first[:left]
284   opts[:left_keys] = Array(left_key)
285   uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
286   left_pk = (opts[:left_primary_key] ||= self.primary_key)
287   raise(Error, "no primary key specified for #{inspect}") unless left_pk
288   opts[:eager_loader_key] = left_pk unless opts.has_key?(:eager_loader_key)
289   opts[:left_primary_keys] = Array(left_pk)
290   lpkc = opts[:left_primary_key_column] ||= left_pk
291   lpkcs = opts[:left_primary_key_columns] ||= Array(lpkc)
292 
293   opts[:left_key_alias] ||= opts.default_associated_key_alias
294   if separate_query_per_table
295     opts[:use_placeholder_loader] = false
296     opts[:allow_eager_graph] = false
297     opts[:allow_filtering_by] = false
298     opts[:eager_limit_strategy] = nil
299 
300     opts[:dataset] ||= proc do |r|
301       def_db = r.associated_class.db
302       vals = uses_lcks ? [lpkcs.map{|k| get_column_value(k)}] : get_column_value(left_pk)
303 
304       has_results = through.each do |edge|
305         ds = (edge[:db] || def_db).from(edge[:table]).where(edge[:left]=>vals)
306         ds = ds.where(edge[:conditions]) if edge[:conditions]
307         right = edge[:right]
308         vals = ds.select_map(right)
309         if right.is_a?(Array)
310           vals.delete_if{|v| v.any?(&:nil?)}
311         else
312           vals.delete(nil)
313         end
314         break if vals.empty?
315       end
316 
317       ds = r.associated_dataset.where(opts.right_primary_key=>vals)
318       ds = ds.clone(:no_results=>true) unless has_results
319       ds
320     end
321     opts[:eager_loader] ||= proc do |eo|
322       h = eo[:id_map]
323       assign_singular = opts.assign_singular?
324       uses_rcks = opts.right_primary_key.is_a?(Array)
325       rpk = uses_rcks ? opts.right_primary_keys : opts.right_primary_key
326       name = opts[:name]
327       def_db = opts.associated_class.db
328       join_map = h
329 
330       run_query = through.each do |edge|
331         ds = (edge[:db] || def_db).from(edge[:table])
332         ds = ds.where(edge[:conditions]) if edge[:conditions]
333         left = edge[:left]
334         right = edge[:right]
335         prev_map = join_map
336         join_map = ds.where(left=>join_map.keys).select_hash_groups(right, left)
337         if right.is_a?(Array)
338           join_map.delete_if{|v,| v.any?(&:nil?)}
339         else
340           join_map.delete(nil)
341         end
342         break if join_map.empty?
343         join_map.each_value do |vs|
344           vs.replace(vs.flat_map{|v| prev_map[v]})
345           vs.uniq!
346         end
347       end
348 
349       eo = Hash[eo]
350 
351       if run_query
352         eo[:loader] = false
353         eo[:right_keys] = join_map.keys
354       else
355         eo[:no_results] = true
356       end
357 
358       opts[:model].eager_load_results(opts, eo) do |assoc_record|
359         rpkv = if uses_rcks
360           assoc_record.values.values_at(*rpk)
361         else
362           assoc_record.values[rpk]
363         end
364 
365         objects = join_map[rpkv]
366 
367         if assign_singular
368           objects.each do |object|
369             object.associations[name] ||= assoc_record
370           end
371         else
372           objects.each do |object|
373             object.associations[name].push(assoc_record)
374           end
375         end
376       end
377     end
378   else
379     opts[:dataset] ||= opts.association_dataset_proc
380     opts[:eager_loader] ||= opts.method(:default_eager_loader)
381   end
382 
383   join_type = opts[:graph_join_type]
384   select = opts[:graph_select]
385   graph_block = opts[:graph_block]
386   only_conditions = opts[:graph_only_conditions]
387   use_only_conditions = opts.include?(:graph_only_conditions)
388   conditions = opts[:graph_conditions]
389   opts[:eager_grapher] ||= proc do |eo|
390     ds = eo[:self]
391     iq = eo[:implicit_qualifier]
392     egls = eo[:limit_strategy]
393     if egls && egls != :ruby
394       associated_key_array = opts.associated_key_array
395       orig_egds = egds = eager_graph_dataset(opts, eo)
396       opts.reverse_edges.each{|t| egds = egds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
397       ft = opts.final_reverse_edge
398       egds = egds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])), :table_alias=>ft[:alias], :qualify=>:deep).
399         select_all(egds.first_source).
400         select_append(*associated_key_array)
401       egds = opts.apply_eager_graph_limit_strategy(egls, egds)
402       ds.graph(egds, associated_key_array.map(&:alias).zip(Array(lpkcs)) + conditions, :qualify=>:deep, :table_alias=>eo[:table_alias], :implicit_qualifier=>iq, :join_type=>eo[:join_type]||join_type, :join_only=>eo[:join_only], :from_self_alias=>eo[:from_self_alias], :select=>select||orig_egds.columns, &graph_block)
403     else
404       opts.edges.each do |t|
405         ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>eo[:join_type]||t[:join_type], :join_only=>eo[:join_only], :qualify=>:deep, :implicit_qualifier=>iq, :from_self_alias=>eo[:from_self_alias], &t[:block])
406         iq = nil
407       end
408       fe = opts.final_edge
409       ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, :join_only=>eo[:join_only], &graph_block)
410     end
411   end
412 end
def_one_through_many(opts) click to toggle source

Use def_many_through_many, since they share pretty much the same code.

    # File lib/sequel/plugins/many_through_many.rb
415 def def_one_through_many(opts)
416   def_many_through_many(opts)
417 end