Snapi

Section 4

Mapper

Mapper

Responsibilities

  1. Maps database row into a ruby object

  2. Adds find/search/create/update/delete to the curator

  3. "lazy_loads" calculate/derived data into the ruby object

Uses the data mapper pattern

include magic

# include                                   # curator       # mapper
include Omicron.destruction(ContactMapper) => delete        => destroy
include Mufasa.find(ContactMapper)         => find          => find
include Mufasa.search(ContactMapper)       => search        => search
include Omicron.persistance(ContactMapper) => create/update => save

We use the include magic to inject functions into the curator which calls functions on the mapper. 

These functions will modify or use response.collection.

class Response
  include Virtus.value_object

  values do
    attribute :status, Integer, :default => 200
    attribute :headers, Hash, :default => {}
    attribute :collection, Array, :default => []
    attribute :original_collection, Array, :default => []
    attribute :page_information, Hash
    attribute :error_message, String
  end
end
class ContactMapper
  include Cartographer.mapper

  private

  def model
    Contact
  end

  def table
    :contacts
  end

  def mapping
    {
      :id => :id,
      :label => :label,
      :address_street1 => :address,
      :address_city => :city,
      :address_state => :state,
      :address_zip => :zip,
      :created_at => :created_at,
      :updated_at => :updated_at,
      :first_name => :first,
      :last_name => :last,
      :is_address_hidden => :hide_address,
      :allow_shared_access => :allow_shared_access,
      :invitation_code => :activation,
      :invitation_declined => :invitation_declined,
      :member_id => :roster_id,
      :user_id => :user_id
    }
  end
end

Anatomy of a mapper

  • Cartographer.mapper
  • table
  • model
  • mapping
  • lazy_load

Cartographer.mapper includes
the following functions into the mapper:

  • find
  • find_by_ids
  • search
  • count
  • save
  • destroy
  • reload

Cartographer.mapper

find: Finds the rows in the database using the table value and then converts them into ruby objects using the model.

 

This is the name of the table in the database in symbol form used by the Sequel gem.

table

def table   
  :contacts
end
class ContactMapper
  include Cartographer.mapper

  private

  def model
    Contact
  end

  def table
    :contacts
  end

  def mapping
    {
      :id => :id,
      :label => :label,
      :address_street1 => :address,
      :address_city => :city,
      :address_state => :state,
      :address_zip => :zip,
      :created_at => :created_at,
      :updated_at => :updated_at,
      :first_name => :first,
      :last_name => :last,
      :is_address_hidden => :hide_address,
      :allow_shared_access => :allow_shared_access,
      :invitation_code => :activation,
      :invitation_declined => :invitation_declined,
      :member_id => :roster_id,
      :user_id => :user_id
    }
  end
end

This is the name of the ruby model that will get mapped into.

model

def model
  Contact
end

What's the ruby model?

class ContactMapper
  include Cartographer.mapper

  private

  def model
    Contact
  end

  def table
    :contacts
  end

  def mapping
    {
      :id => :id,
      :label => :label,
      :address_street1 => :address,
      :address_city => :city,
      :address_state => :state,
      :address_zip => :zip,
      :created_at => :created_at,
      :updated_at => :updated_at,
      :first_name => :first,
      :last_name => :last,
      :is_address_hidden => :hide_address,
      :allow_shared_access => :allow_shared_access,
      :invitation_code => :activation,
      :invitation_declined => :invitation_declined,
      :member_id => :roster_id,
      :user_id => :user_id
    }
  end
end
class Contact
  include Cartographer.value_object

  values do
    attribute :id, Integer
    attribute :label, String
    attribute :address_street1, String
    attribute :address_city, String
    attribute :address_state, String
    attribute :address_zip, String
    attribute :created_at, DateTime
    attribute :updated_at, DateTime
    attribute :first_name, String
    attribute :last_name, String
    attribute :is_address_hidden, Boolean, :default => false
    attribute :allow_shared_access, Boolean, :default => false
  end

  lazy_attribute :user_first_name
  lazy_attribute :user_last_name
  lazy_attribute :team_id
  lazy_attribute :division_id
  lazy_attribute :is_alertable
  lazy_attribute :is_emailable
  lazy_attribute :is_invitable

  def is_invited
    @is_invited ||= invitation_code && !user_id
  end
end
  • attribute

    • has a corresponding column in the database

model

attribute :id, Integer
attribute :label, String
attribute :is_address_hidden, Boolean, :default => false

  lazy_attribute :user_first_name
  lazy_attribute :user_last_name
  lazy_attribute :team_id
  lazy_attribute :division_id
  lazy_attribute :is_alertable
  lazy_attribute :is_emailable
  lazy_attribute :is_invitable

  def is_invited
    @is_invited ||= invitation_code && !user_id
  end
end
lazy_attribute :user_first_name
lazy_attribute :team_id
lazy_attribute :is_alertable
lazy_attribute :is_emailable
lazy_attribute :is_invitable
  • lazy_attribute

    • has to be calculated or derived                

Maps the model attributes
from the database table columns.

mapping

#   model => table column
def mapping
  {
    :id => :id,
    :label => :label,
    :address_street1 => :address,
    :address_city => :city,
    :address_state => :state,
    :address_zip => :zip,
    :created_at => :created_at,
    :updated_at => :updated_at,
    :is_address_hidden => :hide_address,
    :allow_shared_access => :allow_shared_access,
    :invitation_code => :activation,
    :invitation_declined => :invitation_declined,
    :member_id => :roster_id,
    :user_id => :user_id
  }
end
class ContactMapper
  include Cartographer.mapper

  private

  def model
    Contact
  end

  def table
    :contacts
  end

  def mapping
    {
      :id => :id,
      :label => :label,
      :address_street1 => :address,
      :address_city => :city,
      :address_state => :state,
      :address_zip => :zip,
      :created_at => :created_at,
      :updated_at => :updated_at,
      :first_name => :first,
      :last_name => :last,
      :is_address_hidden => :hide_address,
      :allow_shared_access => :allow_shared_access,
      :invitation_code => :activation,
      :invitation_declined => :invitation_declined,
      :member_id => :roster_id,
      :user_id => :user_id
    }
  end
end

We have to calculate all the lazy_attributes
in the mapper.

lazy_loading

class Contact
  lazy_attribute :user_first_name
  lazy_attribute :user_last_name
  lazy_attribute :team_id
  lazy_attribute :division_id
  lazy_attribute :is_alertable
  lazy_attribute :is_emailable
  lazy_attribute :is_invitable
end

Cartographer.mapper will call lazy_load on the mapper with list of objects in response.collection.

lazy_loading

class ContactMapper
  include Cartographer.mapper
  private

  def lazy_load(objects)
    lazy_load_user_first_and_last_name(objects)
    lazy_load_team_ids_division_ids_and_is_owner(objects)
    lazy_load_is_alertable(objects)
    lazy_load_is_emailable(objects)
    lazy_load_is_invitable(objects)
    lazy_load_is_pushable(objects)
  end

  def lazy_load_user_first_and_last_name(objects)
    user_ids = objects.map(&:user_id)
    if user_ids.any?
      users = DB[:users]
        .where(:id => user_ids)
        .select_hash(:id, [:first, :last])
      objects.each do |object|
        user = users.fetch(object.user_id) { [nil, nil] }
        object.user_first_name = user[0]
        object.user_last_name = user[1]
      end
    end
  end
end

lazy_loading

class ContactMapper
  def lazy_load_is_alertable(objects)
    contact_ids = objects.map(&:id)
    if contact_ids.any?
      entries = DB[:telephones]
        .where(:contact_id => contact_ids)
        .where(:enable_sms => true)
        .group_and_count(:contact_id)
        .to_hash(:contact_id, :count)

      objects.each do |object|
        object.is_alertable = !!entries.fetch(object.id) { false }
      end
    end
  end
end

Mapper

Responsibilities

  1. Maps database row into a ruby object

  2. Adds find/search/create/update/delete to the curator

  3. "lazy_loads" calculate/derived data into the ruby object

  4. Curator calls mapper functions to modify response.collection

Thank you!

Snapi Section 04 Mapper

By Dustin McCraw

Snapi Section 04 Mapper

  • 845