class SpeakyCsv::ActiveRecordImport

Imports a csv file as unsaved active record instances

Constants

QUERY_BATCH_SIZE
TRUE_VALUES

Attributes

logger[RW]

Public Class Methods

new(config, input_io_or_enumerable, klass) click to toggle source
# File lib/speaky_csv/active_record_import.rb, line 14
def initialize(config, input_io_or_enumerable, klass)
  @config = config
  @klass = klass

  @log_output = StringIO.new
  @logger = Logger.new @log_output

  if duck_type_is_io?(input_io_or_enumerable)
    @rx = AttrImport.new @config, input_io_or_enumerable
    @rx.logger = @logger
  else
    @rx = input_io_or_enumerable
  end
end

Public Instance Methods

each() { |a| ... } click to toggle source
# File lib/speaky_csv/active_record_import.rb, line 29
def each
  block_given? ? enumerator.each { |a| yield a } : enumerator
end
eager_load(options) click to toggle source

Add eager_load options which will be used when querying records.

# File lib/speaky_csv/active_record_import.rb, line 48
def eager_load(options)
  @eager_load = options
end
includes(options) click to toggle source

Add includes options which will be used when querying records.

Useful to avoid N+1 type problems. Configured has_manys are automaticaly included and don't need to be specified here.

# File lib/speaky_csv/active_record_import.rb, line 43
def includes(options)
  @includes = options
end
log() click to toggle source

Returns a string of all the log output from the import. Or returns nothing if a custom logger was used.

# File lib/speaky_csv/active_record_import.rb, line 35
def log
  @log_output.string
end

Private Instance Methods

duck_type_is_io?(val) click to toggle source
# File lib/speaky_csv/active_record_import.rb, line 117
def duck_type_is_io?(val)
  # check some arbitrary methods
  val.respond_to?(:gets) && val.respond_to?(:seek)
end
enumerator() click to toggle source
# File lib/speaky_csv/active_record_import.rb, line 54
def enumerator
  # One based index, where the header is row 1 and first record is row 2
  row_index = 1

  Enumerator.new do |yielder|
    @rx.each_slice(QUERY_BATCH_SIZE) do |rows|
      keys = rows.map { |attrs| attrs[@config.primary_key.to_s] }
      query = @klass.includes(@config.has_manys.keys)
              .where(@config.primary_key => keys)
      query = query.includes(@includes) if @includes
      query = query.eager_load(@eager_load) if @eager_load

      records = query.inject({}) { |a, e| a[e.send(@config.primary_key).to_s] = e; a }

      rows.each do |attrs|
        record = if attrs[@config.primary_key.to_s].present?
                   records[attrs[@config.primary_key.to_s]]
                 else
                   @klass.new
                 end

        unless record
          logger.error "[row #{row_index}] record not found with primary key: #{attrs[@config.primary_key.to_s].inspect}"
          yielder << nil
          next
        end

        if @config.fields.include?(:_destroy)
          if TRUE_VALUES.include?(attrs['_destroy'])
            record.mark_for_destruction
            yielder << record
            next

          else
            attrs.delete '_destroy'
          end
        end

        @config.has_manys.keys.map(&:to_s).select{|n| attrs.key? n}.each do |name|
          # assume nested attributes feature is used
          attrs["#{name}_attributes"] = attrs.delete name
        end

        @config.has_ones.keys.map(&:to_s).select{|n| attrs.key? n}.each do |name|
          attrs["#{name}_attributes"] = attrs.delete name.to_s
        end

        attrs.each do |attr, value|
          writer_method = "#{attr}="
          if record.respond_to? writer_method
            record.send writer_method, value
          else
            logger.error "[row #{row_index}] record doesn't respond to #{attr.inspect}"
          end
        end

        yielder << record
        row_index += 1
      end
    end
  end
end