ROM 0.8.0 Released

We're very happy to announce the release of Ruby Object Mapper 0.8.0. This release ships with the support for nested input for commands and many improvements in mappers. You can look at the changelog for the full overview.

Apart from ROM 0.8.0 release there are also updates of the following gems:

  • rom-sql 0.5.2 which comes with improved migration tasks that no longer require env finalization CHANGELOG
  • rom-rails 0.4.0 with the support for embedded validators CHANGELOG

There are 2 new adapters added to the rom-rb organization so check them out:

Support For Nested Input

ROM commands are now even more powerful by allowing composition of multiple commands into one that can receive a nested input which will be used to insert data into multiple relations. This feature is compatible with combined relation mapping which means you can pipe results from a combined command through mappers just like in case of combined relations.

When do you want to use this feature? Every time you want to persist entire object graph, in example a post with its tags or a user with an address.

Here's a complete example of using combined commands with a mapper:

ROM.setup(:sql, 'postgres://localhost/rom')

ROM::SQL.gateway.connection.create_table :posts do
  primary_key :id
  column :title, String
end

ROM::SQL.gateway.connection.create_table :tags do
  primary_key :id
  foreign_key :post_id, :posts, null: false
  column :name, String
end

class Posts < ROM::Relation[:sql]
end

class Tags < ROM::Relation[:sql]
end

class CreatePost < ROM::Commands::Create[:sql]
  relation :posts
  result :one
  register_as :create
  input Transproc(:accept_keys, [:title]) # filters out `:tags` key
end

class CreateTag < ROM::Commands::Create[:sql]
  relation :tags
  register_as :create
  input Transproc(:accept_keys, [:name, :post_id])
  associates :post, key: [:post_id, :id] # automatically sets FK value
end

class PostMapper < ROM::Mapper
  relation :posts
  register_as :entity

  combine :tags, on: { id: :post_id }
end

rom = ROM.finalize.env

create_post_with_tags = rom
  .command([{ post: :posts }, [:create, [:tags, [:create]]]])
  .as(:entity)

create_post_with_tags.call(
  post: { title: 'Hello World', tags: [{ name: 'red' }, { name: 'green' }] }
).to_a
# [
#   {
#     :id => 1,
#     :title => "Hello World",
#     :tags => [
#       { :id=>1, :post_id=>1, :name=>"red" },
#       { :id=>2, :post_id=>1, :name=>"green" }
#      ]
#   }
# ]

Mapper Steps

Transforming data with mappers can be really complex and sometimes you may want to define multiple mapping steps. That's why we introduced a new interface in Mapper DSL where you can do just that:

class UserMapper < ROM::Mapper
  step do
    attribute :id, from: :user_id
    attribute :name, from: :user_name
  end

  step do
    wrap :details do
      attribute :name
    end
  end
end

mapper = UserMapper.build

mapper.call([{ user_id: 1, user_name: 'Jane' }])
# [{ :id => 1, :details => { :name => "Jane" } }]

Typically you want to use this feature when mapping logic is too complex to be expressed using nested blocks. It's especially useful when dealing with multiple group/ungroup/wrap/unwrap/fold/unfold operations that simply cannot be defined as a deeply nested mapping definition block.

New Mapping Transformations

We have 3 new transformations fold, unfold and ungroup which makes mappers even more powerful.

Folding can be used to collapse values from multiple tuples under a single array attribute:

class PostFoldMapper < ROM::Mapper
  fold tag_list: [:tag_name]
end

mapper = PostFoldMapper.build

puts mapper.call([
  { title: 'Hello World', tag_name: 'red' },
  { title: 'Hello World', tag_name: 'green' }
]).inspect
# [{:title=>"Hello World", :tag_list=>["red", "green"]}]

Unfolding is, unsurprisingly, an inversion of folding:

class PostUnfoldMapper < ROM::Mapper
  unfold :tag_name, from: :tag_list
end

mapper = PostUnfoldMapper.build

puts mapper.call([{ title: 'Hello World', tag_list: ['red', 'green'] }]).inspect
# [{:tag_name=>"red", :title=>"Hello World"}, {:tag_name=>"green", :title=>"Hello World"}]

Now you can also ungroup tuples:

class PostUngroupMapper < ROM::Mapper
  ungroup :tags do
    attribute :tag_name, from: :name
  end
end

mapper = PostUngroupMapper.build

puts mapper.call([
  { title: 'Hello World', tags: [{ name: 'red' }, { name: 'green' }] }
]).inspect
# [{:tag_name=>"red", :title=>"Hello World"}, {:tag_name=>"green", :title=>"Hello World"}]

Guides

ROM is growing really fast and there's a lot of functionality that is difficult to describe in API documentation. That's why we started a new Guides section on the official rom-rb.org website.

You can already find a lot of information about ROM setup, adapters, relations, commands and mappers. We'll be adding more content and improving existing documentation based on the feedback so please check them out and let us know what you think.

In the upcoming weeks you should also see new tutorials covering topics like building your own persistence layer with ROM, handling data import with ROM or how to use ROM with various JSON serializers like Roar or Yaks, so stay tuned!

Next Release

We have a pretty good understanding of what we want to achieve with the next 0.9.0 release which will improve the internal architecture of ROM. We're planning to split rom gem into smaller pieces and introduce cleaner and more explicit interfaces for setting up ROM.

Another planned change is introducing Policy Over Configuration API which should improve ROM configuration and handling various conventions.

This release will be a big step towards 1.0.0 which is scheduled for September (yes, this year ;)).