This is really more of a summary of what you can do with RowBoat::Base since you subclass it to do everything :)
- Basic Usage
.importinitializeimportimport_intocsv_sourcecolumn_mappingpreprocess_rowpreprocess_rowsoptionshandle_failed_rowhandle_failed_rowsvalue_convertersrollback_transaction?
Just subclass RowBoat::Base and define the import_into and column_mapping methods to get started (They're the only methods that you're required to implement).
class ImportProduct < RowBoat::Base
def import_into
Product
end
def column_mapping
{
downcased_csv_column_header: :model_attribute_name,
another_downcased_csv_column_header: :another_model_attribute_name
}
end
endImports database records form the given CSV-like object. The CSV-like object can be anything that can be passed to SmarterCSV.process (string paths to files, files, tempfiles, instances of StringIO, etc).
It returns a hash containing
:invalid_records- an array of all records that failed to import since they were invalid. If you've configured the:validateoption to befalseit will be an empty array.:total_inserts- the total number of database inserts that were run.:inserted_ids- an array of all of the ids of records inserted into the database.:skipped_rows- every row skipped by returningnilfrompreprocess_row.
If you want to pass additional information to help import CSVs, don't override this method. It just passes through to initialize so override that :)
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
end
ImportProduct.import("path/to/my.csv")class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def intitialize(csv_source, my_options)
super(csv_source)
@my_options = my_options
end
end
ImportProduct.import("path/to/my.csv", foo: "bar")Makes a new instance with the given CSV-like object. See .import for more details around when and how to override this method.
The instance method that actually parses and imports the CSV. Generally, you wouldn't call this directly and would instead call .import.
It is required that you override this method to return whatever ActiveRecord class you want your CSV imported into.
class ImportProduct < RowBoat::Base
# other required configuration omitted for brevity
def import_into
Product
end
endclass ImportProduct < RowBoat::Base
# other required configuration omitted for brevity
def import_into
if csv_source.is_a?(String) && csv_source.match(/category/i)
ProductCategory
else
Product
end
end
end
ImportProduct.import("path/to/category.csv")
ImportProduct.import("path/to/product.csv")Whatever you originally passed in as the CSV source.
class ImportProduct < RowBoat::Base
# other required configuration omitted for brevity
def import_into
# `csv_source` is available in any of our instance methods
if csv_source.is_a?(String) && csv_source.match(/category/i)
ProductCategory
else
Product
end
end
end
ImportProduct.import("path/to/category.csv")
ImportProduct.import("path/to/product.csv")It is required that you override this method with either a hash that maps columns in your CSV to their preferred names or an array of your preferred column names.
By default when using a hash
- CSV column names are downcased symbols of what they look like in the CSV.
- CSV columns that are not mapped are ignored when processing the CSV.
If you're familiar with SmarterCSV, this method essentially defines your :key_mapping with the :remove_unmapped_keys setting set to true when provided with a hash. When given an array it is the :user_provided_headers option.
You can change these defaults by overriding the options method.
class ImportProduct < RowBoat::Base
# other required configuration omitted for brevity
def column_mapping
{
prdct_nm: :name,
"price/cost_amnt": :price_in_cents
}
end
end
# or...
class ImportProduct < RowBoat::Base
# other required configuration omitted for brevity
def column_mapping
[:name, :price_in_cents]
end
endImplement this method if you need to do some work on the row before the record is inserted/updated.
If you return nil from this method, the row will be skipped in the import. You can access these rows in the return value from .import (the :skipped_rows key).
You also have access to row_number here.
If the work you intend to do with the row only requires changing one attribute, it is recommended that you override value_converters instead of this.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def preprocess_row(row)
{ position: row_number }.merge(row)
end
# or...
def preprocess_row(row)
if row[:name] && row[:price]
row
else
nil
end
end
endOverride this method if you need to do something with a chunk of rows (the chunk size is determined by the :chunk_size option in the options method).
If you need to filter particular rows, it's better to just implement preprocess_row and return nil for the rows you want to ignore.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def preprocess_rows(rows)
if skip_batch?(rows)
super([])
else
super
end
end
def skip_batch?(rows)
# decide whether or not to skip the batch
end
endImplement this to configure RowBoat, SmarterCSV, and activerecord-import.
Except for :wrap_in_transaction, all options pass through to SmarterCSV and activerecord-import.
:wrap_in_transaction simply tells RowBoat whether or not you want your whole import wrapped in a database transaction.
Whatever you define in this method will be merged into the defaults:
:chunk_size-500:key_mapping-column_mapping:recursive-false:remove_unmapped_keys-true:validate-true:value_converters-csv_value_converters:wrap_in_transaction-true
Don't provide value_converters or key_mapping options here. Implement the value_converters and column_mapping respectively.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def options
{
chunk_size: 1000,
validate: false,
wrap_in_transaction: false
}
end
endImplement this to do some work with a row that has failed to import.
It's important to note that
- This happens after the import has completed.
- The given row is an instance of whatever class was returned by
import_into.
These records are also available in the return value of .import.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def handle_failed_row(row)
puts row.errors.full_messages.join(", ")
end
endOverride this method to do some work will all of the rows that failed to import.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def handle_failed_rows(rows)
puts "Failed to import #{rows.size} rows :("
super
end
endImplement to specify how to translate values from the CSV into whatever sorts of objects you need.
Simply return a hash that has the mapped column name (ie, what you mapped it to in the column_mapping method) as a key pointing to either
- a method name as a symbol
- a proc or lambda
- an object that implements
convert
Regardless of which one you choose, it takes a value and returns a converted value.
This is essentially a sugared up version of :value_converters option in SmarterCSV.
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def value_converters
{
sell_by: :convert_date,
name: -> (value) { value.titlelize },
price: proc { |value| value.to_i },
description: DescriptionConverter
}
end
def convert_date(value)
Date.parse(value) rescue nil
end
end
module DescriptionConverter
def self.convert(value)
value.present? ? value : "default description :("
end
endImplement this method if you'd like to rollback the transaction after it otherwise has completed.
Note: imports are only wrapped in a transaction if the wrap_in_transaction option is true. It defaults to true but this can be configured in options
class ImportProduct < RowBoat::Base
# required configuration omitted for brevity
def rollback_transaction?
CsvService.already_imported?(csv_source)
end
end