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, line 225
def many_through_many(name, through, opts=OPTS, &block)
  associate(:many_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
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, line 230
def one_through_many(name, through, opts=OPTS, &block)
  associate(:one_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
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, line 237
def def_many_through_many(opts)
  one_through_many = opts[:type] == :one_through_many
  opts[:read_only] = true
  if opts[:uniq]
    opts[:after_load] ||= []
    opts[:after_load].unshift(:array_uniq!)
  end
  opts[:cartesian_product_number] ||= one_through_many ? 0 : 2
  separate_query_per_table = false
  through = opts[:through] = opts[:through].map do |e|
    case e
    when Array
      raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
      {:table=>e[0], :left=>e[1], :right=>e[2]}
    when Hash
      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]
      separate_query_per_table = true if e[:db]
      e
    else
      raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
    end
  end
  opts[:separate_query_per_table] = separate_query_per_table

  left_key = opts[:left_key] = opts[:through].first[:left]
  opts[:left_keys] = Array(left_key)
  uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
  left_pk = (opts[:left_primary_key] ||= self.primary_key)
  raise(Error, "no primary key specified for #{inspect}") unless left_pk
  opts[:eager_loader_key] = left_pk unless opts.has_key?(:eager_loader_key)
  opts[:left_primary_keys] = Array(left_pk)
  lpkc = opts[:left_primary_key_column] ||= left_pk
  lpkcs = opts[:left_primary_key_columns] ||= Array(lpkc)

  opts[:left_key_alias] ||= opts.default_associated_key_alias
  if separate_query_per_table
    opts[:use_placeholder_loader] = false
    opts[:allow_eager_graph] = false
    opts[:allow_filtering_by] = false
    opts[:eager_limit_strategy] = nil

    opts[:dataset] ||= proc do |r|
      def_db = r.associated_class.db
      vals = uses_lcks ? [lpkcs.map{|k| get_column_value(k)}] : get_column_value(left_pk)

      has_results = through.each do |edge|
        ds = (edge[:db] || def_db).from(edge[:table]).where(edge[:left]=>vals)
        ds = ds.where(edge[:conditions]) if edge[:conditions]
        right = edge[:right]
        vals = ds.select_map(right)
        if right.is_a?(Array)
          vals.delete_if{|v| v.any?(&:nil?)}
        else
          vals.delete(nil)
        end
        break if vals.empty?
      end

      ds = r.associated_dataset.where(opts.right_primary_key=>vals)
      ds = ds.clone(:no_results=>true) unless has_results
      ds
    end
    opts[:eager_loader] ||= proc do |eo|
      h = eo[:id_map]
      assign_singular = opts.assign_singular?
      uses_rcks = opts.right_primary_key.is_a?(Array)
      rpk = uses_rcks ? opts.right_primary_keys : opts.right_primary_key
      name = opts[:name]
      def_db = opts.associated_class.db
      join_map = h

      run_query = through.each do |edge|
        ds = (edge[:db] || def_db).from(edge[:table])
        ds = ds.where(edge[:conditions]) if edge[:conditions]
        left = edge[:left]
        right = edge[:right]
        prev_map = join_map
        join_map = ds.where(left=>join_map.keys).select_hash_groups(right, left)
        if right.is_a?(Array)
          join_map.delete_if{|v,| v.any?(&:nil?)}
        else
          join_map.delete(nil)
        end
        break if join_map.empty?
        join_map.each_value do |vs|
          vs.replace(vs.flat_map{|v| prev_map[v]})
          vs.uniq!
        end
      end

      eo = Hash[eo]

      if run_query
        eo[:loader] = false
        eo[:right_keys] = join_map.keys
      else
        eo[:no_results] = true
      end

      opts[:model].eager_load_results(opts, eo) do |assoc_record|
        rpkv = if uses_rcks
          assoc_record.values.values_at(*rpk)
        else
          assoc_record.values[rpk]
        end

        objects = join_map[rpkv]

        if assign_singular
          objects.each do |object|
            object.associations[name] ||= assoc_record
          end
        else
          objects.each do |object|
            object.associations[name].push(assoc_record)
          end
        end
      end
    end
  else
    opts[:dataset] ||= opts.association_dataset_proc
    opts[:eager_loader] ||= opts.method(:default_eager_loader)
  end

  join_type = opts[:graph_join_type]
  select = opts[:graph_select]
  graph_block = opts[:graph_block]
  only_conditions = opts[:graph_only_conditions]
  use_only_conditions = opts.include?(:graph_only_conditions)
  conditions = opts[:graph_conditions]
  opts[:eager_grapher] ||= proc do |eo|
    ds = eo[:self]
    iq = eo[:implicit_qualifier]
    egls = eo[:limit_strategy]
    if egls && egls != :ruby
      associated_key_array = opts.associated_key_array
      orig_egds = egds = eager_graph_dataset(opts, eo)
      opts.reverse_edges.each{|t| egds = egds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
      ft = opts.final_reverse_edge
      egds = egds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])), :table_alias=>ft[:alias], :qualify=>:deep).
        select_all(egds.first_source).
        select_append(*associated_key_array)
      egds = opts.apply_eager_graph_limit_strategy(egls, egds)
      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)
    else
      opts.edges.each do |t|
        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])
        iq = nil
      end
      fe = opts.final_edge
      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)
    end
  end
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, line 393
def def_one_through_many(opts)
  def_many_through_many(opts)
end