Given:
create_table(:foos) do
  primary_key(:id)
  String(:name)
end
create_table(:bars) do
  primary_key(:id)
  String(:name)
end
create_table(:foos_bars) do
  primary_key(:id)
  foreign_key(:foo_id, :foos)
  foreign_key(:bar_id, :bars)
  String(:name)
end
class Foos < ROM::Relation[:sql]
  dataset :foos
  def with_bars(id)
    prefix('foos').qualified.select(
      :foos__id, :foos__name
    ).select_append(
      :bars__id, :bars__name
    ).left_join(
      :foos_bars, foos_bars__bars_id: :foos__id
    ).left_join(
      :bars, bars__id: :foos_bars__bars_id
    ).where(foos__id: id)
  end
end
class FoosModel
  include Virtus.model
  attribute :id
  attribute :name
  attribute :bars
end
class BarsModel
  include Virtus.model
  attribute :id
  attribute :name
  attribute :foos
end
I've tried many, many random variations on the keywords shown in (but not explained in) the ROM docs to no avail. Here's a literal interpretation of the docs into a mapper for Foos, which doesn't work:
class FoosMapper < ROM::Mapper
  relation :foos
  register_as :foos
  model Foo
  prefix('foos')
  attribute :id
  attribute :name
  group :bars do
    model Bar
    attribute :id, from: :bar
    attribute :name, from: :bar
  end
end
How does one write a mapper (or rework the relation to work with the mapper) to get the simple result of a foo with a bars attribute having all the bars linked by the foos_bars table?
 
                        
Building up complex joins like that is not recommended unless you have good reasons like performance. It's much simpler to use repositories to compose relations by defining relation views that you need for reusability and composing those in various way inside repos. Defining custom mappers should also be avoided unless you have some unique requirements.
I made a gist that illustrates that right here: https://gist.github.com/solnic/9307e1b2e3428718dd12
We're working on a new set of docs that will properly explain those things.