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