# 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
Last active
December 17, 2015 18:59
-
-
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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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