Simple Data Mapper

I recently tackled a pretty typical data migration task where some source model had to be transformed into some target model. About 80% of the elements were mapped field for field, and the other 20% had to be mutated in some way. Tired of writing one off rake tasks to pull and transform data, I came up with a little data mapper class that I cold reuse in the future.

class Mapper
  attr_accessor :source_model
  attr_accessor :mappings

  class AttributeMapping;end

  class ListMapping < AttributeMapping
    attr_accessor :fields

    # fields - array of field symbols we want to map data into
    def initialize(fields)
      self.fields = fields
    end

    def transform(source_obj, source_attribute)
      {}.tap do |values|
        fields.each do |field|
          values[field] = source_obj.send(source_attribute)
        end
      end
    end
  end

  class ComplexMapping < AttributeMapping
    attr_accessor :field
    attr_accessor :instruction

    # field       - field symbol we want to map data into
    # instruction - lambda which accepts source object and source attribute.
    #               expected to return value to be mapped to field (on target obj)
    def initialize(field, instruction)
      self.field = field
      self.instruction = instruction
    end

    def transform(source_obj, source_attribute)
      { field => instruction.call(source_obj, source_attribute) }
    end
  end

  def initialize(source_model, mappings)
    self.source_model = source_model
    self.mappings = mappings
  end

  def conjure(model)
    values = map_values_for( self.source_model, self.mappings )
    model.to_s.camelize.constantize.new(values)
  end

  def map_values_for(source_model, mappings)
    {}.tap do |values|
      mappings.each do |attr, mapping|
        if AttributeMapping === mapping
          values.update(mapping.transform(source_model, attr))
        else
          values[mapping] = source_model.send(attr)
        end
      end
    end
  end
end

Using the mapper is really simple. Let's say my source model, LegacyCustomer, is based off of a legacy table from an older system and looks like:

LegacyCustomer(
  userid: integer,        # primary key
  creationdate: datetime, # date record was created
  accountnum: string      # customer number. it's prefixed with
                          # "LGCY-" string that we no longer need!
)

And we want to migrate the LegacyCustomer data to a new Customer model that looks like:

Customer(
  id: integer,
  created_at: datetime,
  updated_at: datetime,
  account_number: string
)

My rake task to run the migration would look like:

namespace :migrate do
  task :legacy_customers do
    field_mappings = {
      :userid         => :id,
      :creation_date  => Mapper::ListMapping.new(
                          [:created_at, :update_at]
                         ),
      :user_data      => Mapper::ComplexMapping.new(
                          :account_number,
                          lambda {|obj, attr| obj.send(attr).gsub('LGCY-','')}
                         )
    }

    LegacyCustomer.all.each do |legacy_customer|
      mapper = Mapper.new(legacy_customer, field_mappings)
      customer = mapper.conjure(:customer)
      customer.save!
    end
  end
end

As my migration marched on 'one off' data errors would pop up causing the script to fail. This is what ultimately led me to create the ComplexMapping class. Every time some white space, funny character, or field split requirement bombed my script I was able to add some code to my ComplexMapping requirement to solve it.

I wanted to share this experience for two reasons:

Ruby is awesome. Metaprogramming and Procs made this mapper possible. When I first started programming with Ruby, these were the two hardest concepts for me to wrap my head around. Investing time into learning these aspects of ruby have made me such a better ruby programmer.

The second reason is to reinforce the lesson that doing things the lazy (comfortable) way will rarely ever benefit you. This mapper class not only made writing and maintaining my migration script easier. It has also found it's way into some production code.

Abstracting concepts (or remembering the single responsibility principle) will always benefit you in the future.

by Karle Durante