Skip to content

Instantly share code, notes, and snippets.

@joakimk
Last active December 17, 2015 18:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joakimk/5656945 to your computer and use it in GitHub Desktop.
Save joakimk/5656945 to your computer and use it in GitHub Desktop.
Extensions to minimapper's ActiveRecord mappers that haven't yet been included in minimapper or minimapper-extras (they are only tested as part of the project that uses them).

Association loading

# Load data from the DB
user = user_mapper.find(params[:id], include: { profile: :group })

# Use data in memory (you can't do lazy-loads with minimapper, which is probably
# a very good thing. Lets you avoid N+1 by default).
user.profile.group.name
class ApplicationMapper < Minimapper::Mapper
include ApplicationMapper::Extensions
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# Without this, Minimapper doesn't realize it shouldn't
# set these attributes, and Rails complains they're not
# mass-assignable.
attr_protected :created_at, :updated_at
# A class like FooMapper::Record automatically
# gets the table name "foos" configured.
def self.inherited(klass)
mapper_name = klass.name[/(\w+)Mapper/, 1]
klass.table_name = mapper_name.tableize
super
end
end
end
module ApplicationMapper::Extensions
extend ActiveSupport::Concern
included do
extend ClassMethods
cattr_accessor :allow_setting_timestamps
end
# Build with loaded associations through calling after_find (useful when
# creating new entities and you need to validate on the presense of
# associations or use associated objects in views)
def build(attributes)
entity = entity_class.new(attributes)
record = record_class.new(entity.attributes)
after_find(entity, record)
entity
end
def create(entity)
result = with_save_filters(entity) { super }
after_create(entity)
result
end
def update(entity)
with_save_filters(entity) { super }
end
# FIXME: Extract, use proper RecordInvalid exception class.
def create!(entity)
create(entity) || raise("Nay! #{entity.errors.full_messages}")
end
# FIXME: Extract, use proper RecordInvalid exception class.
def update!(entity)
update(entity) || raise("Nay! #{entity.errors.full_messages}")
end
def find(id, opts = {})
process_options(opts)
super(id)
end
def reload(entity)
find(entity.id)
end
private
module ClassMethods
def default_include(*list)
define_method(:default_includes) do
list
end
end
end
def process_options(opts = {})
if opts[:include].is_a?(Hash)
@custom_includes = [ opts[:include] ]
else
@custom_includes = Array(opts[:include])
end
end
def with_save_filters(entity)
before_save(entity)
value = yield
after_save(entity)
value
end
def record_class
self.class.const_get(:Record)
end
def find_record(id)
id && find_scope.find_by_id(id)
end
def before_save(entity)
update_serialized_data(entity)
update_ids(entity)
end
# Override to persist associated data, etc.
def after_save(entity)
end
def after_create(entity)
end
# Override to load associated data, ex. [ :employee ]
def default_includes
[]
end
def update_ids(entity)
entity_class.column_names.find_all { |name| name.ends_with?("_id") }.each do |column|
association_name = column.sub(/_id/, '')
associated_entity = entity.public_send(association_name) rescue next
entity.public_send("#{column}=", associated_entity && associated_entity.id)
end
end
# Minimapper's entities_for doesn't take a second argument yet.
def entities_for(records, klass = entity_class)
records.map { |record| entity_for(record, klass) }
end
# Copies entity.foo.attributes to entity.foo_attributes before saving
def update_serialized_data(entity)
entity_class.column_names.find_all { |name| name.end_with?("_attributes") }.each do |column|
serialized_association_name = column.sub(/_attributes$/, '')
serialized_association = entity.public_send(serialized_association_name)
if serialized_association.is_a?(Array)
data = serialized_association.map(&:attributes)
else
data = serialized_association.attributes
end
entity.public_send("#{column}=", data)
end
end
def included_associations
default_includes + (@custom_includes || [])
end
def after_find(entity, record)
included_associations.each do |association|
load_association(association, entity, record)
end
end
def load_association(association, entity, record)
if association.is_a?(Hash)
# Load associated records recursivly when it's a hash
association.each do |k, v|
associated_entity = load_association(k, entity, record)
associated_record = record.public_send(k)
load_association(v, associated_entity, associated_record)
end
elsif association.is_a?(Array)
association.each do |a|
load_association(a, entity, record)
end
else
association_entity_class = lookup_entity_class(association, record)
associated_record_or_records = record.public_send(association)
# Don't want to hard-code to ActiveRecord::Relation, and need to handle
# nil values correctly.
if associated_record_or_records.respond_to?(:length)
associated_entity_or_entities = entities_for(associated_record_or_records, association_entity_class)
else
associated_entity_or_entities = entity_for(associated_record_or_records, association_entity_class)
end
entity.send("#{association}=", associated_entity_or_entities)
associated_entity_or_entities
end
end
def lookup_entity_class(association, record)
# Resolve type when using polymorphic associations
if record.respond_to?("#{association}_type")
association = record.send("#{association}_type").sub(/Mapper.+/, '').underscore
end
association.to_s.classify.constantize
end
def copy_attributes_to_record(record, entity)
super(record, entity)
if allow_setting_timestamps
if record.class.column_names.include?("created_at")
record.created_at ||= entity.created_at
record.updated_at ||= entity.updated_at
end
end
end
# Override to customize includes, etc.
def find_scope
record_class.includes(included_associations)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment