Scenario:
Brand new Rails 6.1 project with postgres database using rom-rails 2.3.0, rom 5.2.6, rom-sql 3.5.0.
Code:
class MembersRelation < ROM::Relation[:sql]
schema(:members, infer: true)
auto_map true
end
class MembersMapper < ROM::Transformer
relation :members, as: :members_mapper
map do
nest(:profile, %i[first_name, last_name, email])
map_value(:profile, &:map_profile)
end
def map_profile(profile_attrs)
Members::Profile.new(*profile_attrs)
end
end
In rails console run:
ds = ROM::Memory::Dataset.new([{id: SecureRandom.uuid, first_name: 'bob'}])
rel = MembersRelation.new(ds)
rel.map_with(:members_mapper)
Traceback (most recent call last):
2: from (irb):10
1: from (irb):11:in `rescue in irb_binding'
ROM::MapperMissingError (:members_mapper doesn't exist in ROM::MapperRegistry registry)
Gives a MapperMissingError even though ROM.env.mappers[:members][:members_mapper] returns the mapper.
You need to set up your relation using configuration rather than instantiating it manually. Things are wired together through configuration, this way relation will know about its mapper.
I knew it had to be something dumb I was doing. This probably should have been obvious in retrospect, but it’s been probably 10 years since I’ve done any significant amount of work involving DI / service location and containers.
One follow-up question if you get a moment –
How would you configure the ‘default mapper’ for a relation? In my example above I want my default mapping for members to use the MembersMapper because that table contains columns I want to hydrate into a Profile object in the domain model?
Nothing dumb about it There’s a bit of a learning curve with ROM setup because it doesn’t rely on global state outside of the setup. Mappers are “detached” from relations that’s why you need to wire things together, which is one of the things that the setup does.
How would you configure the ‘default mapper’ for a relation?
If you call it the same as the relation then it will be used by default. Just do relation :members in the mapper class definition.
The default mapper trick doesn’t seem to be working for me.
Given the scenario:
class MembersRelation < ROM::Relation[:sql]
schema(:members, infer: true)
auto_map true
end
class MembersMapper < ROM::Transformer
relation :members
map do
nest(:profile, %i[first_name, last_name, email])
map_value(:profile, &:map_profile)
end
def map_profile(profile_attrs)
Members::Profile.new(profile_attrs)
end
end
I’ve also tried relation :members, as: :members in the MembersRelation with no luck as well. If I’m thinking about this wrong and there is a better way without needing a default mapper to go from a flat table structure like:
members
- id
- company_id
- first_name
- last_name
- email
To models like:
module Members
class Member < ROM::Struct
attribute :id, Types::Integer
attribute :company_id, Types::Integer
attribute :profile, Types.Instance(Profile)
end
end
module Members
class Profile < ROM::Struct
attribute :first_name, Types::String
attribute :last_name, Types::String
attribute :email, Types::String
end
end
Ah you enabled auto_map too, IIRC that’s why it doesn’t pick up any mapper because it infers a default mapper for you through auto-mapping. You can just do members.map_with(:your_mapper_name) to explicitly tell the relation which mapper it should use.
hmm… I’ve been trying various combinations of map_with as well. I can map_with directly on the relation in the console and it works just fine. If I however try and define a new method on the repository that uses map_with on the relation it appears to not actually be executing the mapper before trying to instantiate the Member object.
Maybe I should ask the question a different way and state the goal I’m going for and just get your advice on what you think the best way to use the various pieces of ROM is to achieve it.
What I want is to take a table like members below and hydrate it into a domain model where the first_name, last_name, email are part of a Profile attached to a Member. If possible I would like to be able to do this in such a way that I don’t need to remember to call map_with everywhere and that the repository just returns correctly hydrated domain objects.
Unfortunately still does not seem to be working. With all the functionality available in ROM this seems like it shouldn’t be a difficult thing to do so I feel like I’m either thinking about the problem wrong or just misconfiguring something that’s not obvious from my questions.
I think I’ll post a minimal reproduction repo on github for inspection when I get a chance. Really appreciate your time looking into this so far!
As per the sample run above, coercion works properly when manually instantiating a Models::Member. I was expecting the same to happen when reading from the database and having the repository wrapping the result.
I’m not really sure what I’m doing wrong here. Any help would be greatly appreciated. Please let me know if there’s anything else I can do to help debugging.