Module: PreloadPluck

Defined in:
lib/preload_pluck.rb,
lib/preload_pluck/version.rb

Defined Under Namespace

Classes: Field

Constant Summary collapse

VERSION =
'0.3.0'

Instance Method Summary collapse

Instance Method Details

#__preload_pluck_to_header_lookup(array) ⇒ Object



143
144
145
# File 'lib/preload_pluck.rb', line 143

def __preload_pluck_to_header_lookup(array)
  Hash[array.map.with_index {|x, i| [x, i]}]
end

#preload_pluck(*args) ⇒ Array<Array>

Return a 2-dimensional array of values where columns correspond to supplied arguments. Data from associations is eager loaded.

Attributes on the current model can be supplied by name:

Comment.preload_pluck(:title, :text)

Nested attributes should be separated by a period:

Comment.preload_pluck('post.title', 'post.text')

Both immediate and nested attributes can be mixed:

Comment.preload_pluck(:title, :text, 'post.title', 'post.text')

Any SQL conditions should be set before ‘preload_pluck` is called:

Comment.order(:created_at)
       .joins(:user)
       .where(user: {name: 'Alice'))
       .preload_pluck(:title, :text, 'post.title', 'post.text')

Parameters:

  • args (Array<Symbol or String>)

    list of immediate and/or nested model attributes.

Returns:

  • (Array<Array>)

    2-dimensional array where columns correspond to supplied arguments.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/preload_pluck.rb', line 55

def preload_pluck(*args)
  fields = args.map {|arg| Field.new(self, arg.to_s.split('.'))}

  plucked_cols = fields.map do |field|
    if field.nested?(0)
      field.assoc(0).foreign_key
    else
      field.path.last
    end
  end.uniq
  data = pluck(*plucked_cols)
  if fields.length == 1
    # Pluck returns a flat array if only one value, so use a consistent structure if there is one or multiple fields
    data.map! {|val| [val]}
  end
  data_headers = __preload_pluck_to_header_lookup(plucked_cols)

  # A cache of records that we populate then join to later based on foreign keys
  nested_data = {}

  # Incrementally process nested fields by level
  max_level = fields.map {|f| f.path.length - 1}.max
  max_level.times do |level|
    fields.select {|f| f.nested?(level)}
          .group_by {|f| f.path_upto(level)}
          .each do |current_path, group|
      # Just use the first item - could use any item in the group
      assoc = group.first.assoc(level)
      klass = assoc.class_name.constantize

      # List of ids that are related to the previous objects (the IN clause in SQL preload statement)
      if level == 0 # Level 0 is different as data is stored in a different structure
        index = data_headers[assoc.foreign_key]
        collection = data
      else
        prev_path = group.first.path_upto(level - 1)
        index = nested_data[prev_path][:header][assoc.foreign_key]
        collection = nested_data[prev_path][:data].values
      end
      ids = collection.map {|d| d[index]}.uniq

      # Select id and other fields at the next level
      cols = group.map do |f|
        if f.nested?(level + 1)
          f.assoc(level + 1).foreign_key
        else
          f.path[level + 1]
        end
      end.uniq
      # If id is specified by user, we need to make this list unique
      plucked_cols = [klass.primary_key, *cols].uniq
      indexed_data = klass.where(klass.primary_key => ids)
                          .pluck(*plucked_cols)
                          .index_by {|d| d[0]} # Index to quickly search on id
      nested_data[current_path] = {
        header: __preload_pluck_to_header_lookup(plucked_cols),
        data: indexed_data
      }
    end
  end

  data.map do |attr|
    fields.map do |field|
      if field.nested?(0)
        assoc = field.assoc(0)
        val = attr[data_headers[assoc.foreign_key]]
        (field.path.length - 1).times do |level|
          current_path = field.path_upto(level)
          if field.nested?(level + 1)
            col = field.assoc(level + 1).foreign_key
          else
            col = field.path.last
          end
          current_data = nested_data[current_path]
          current_row = current_data[:data][val]
          if current_row
            index = current_data[:header][col]
            val = current_row[index]
          end
        end
        val
      else
        attr[data_headers[field.path.last]]
      end
    end
  end
end