ROM 0.7.0 Released

Ruby Object Mapper has reached another milestone and today we're happy to announce the release of ROM 0.7.0! This version solidifies core functionality of ROM and adds new, great features on top of existing APIs.

This release includes updates of the following gems:

We're also excited to see development of new adapters:

Let's see what this new release brings.

Explicit eager-loading

Probably the most important addition in this release is the ability to eager-load entire object graph using the new combine interface. Previously the only way to load a nested data-structure into memory was to use a join. Starting from 0.7.0 you can combine relations together and map the resulting graph into an aggregate with ease. As a bonus side-effect it allows to "join" relations coming from different datastores in memory. This feature leverages data-pipeline feature of ROM that was introduced in 0.6.0.

Let's see how this works:

require 'rom'

ROM.setup :sql, 'sqlite::memory'

class Users < ROM::Relation[:sql]
  def by_name(name)
    where(name: name)
  end
end

class Tasks < ROM::Relation[:sql]
  def for_users(users)
    where(user_id: users.map { |user| user[:id] })
  end
end

rom = ROM.finalize.env

db = rom.repositories[:default].connection

db.create_table :users do
  primary_key :id
  String :name
end

db.create_table :tasks do
  primary_key :id
  Integer :user_id
  String :title
end

db[:users].insert id: 1, name: 'Jane'
db[:users].insert id: 2, name: 'John'

db[:tasks].insert id: 1, user_id: 2, title: 'Task for John'
db[:tasks].insert id: 2, user_id: 1, title: 'Task for Jane'

users = rom.relation(:users)
tasks = rom.relation(:tasks)

user_with_tasks = users.by_name('Jane').combine(tasks.for_users)
puts user_with_tasks.to_a.inspect

# [
#   #<ROM::Relation::Loaded:0x007fde8220e708
#     @source=#<Users dataset=#<Sequel::SQLite::Dataset: "SELECT * FROM `users` WHERE (`name` = 'Jane')">>,
#     @collection=[{:id=>1, :name=>"Jane"}]>,
#     [
#       #<ROM::Relation::Loaded:0x007fde82207c00
#         @source=#<Tasks dataset=#<Sequel::SQLite::Dataset: "SELECT * FROM `tasks` WHERE (`user_id` IN (1))">>,
#         @collection=[{:id=>2, :user_id=>1, :title=>"Task for Jane"}]>
#     ]
# ]

As you can see ROM executes the minimum amount of needed SQL queries. It shouldn't surprise given that you explicitly told ROM which relations should be used to load the data. As you can probably imagine you are free to use whatever strategy fits better in a particular use case. You can use combine, or you can use joins, or both at the same time. It doesn't really matter since it's just data flowing from one object to another producing end result - a nested data structure.

Mapping combined relations

To map result from our previous example we can create a mapper using the mapping DSL named the same as in relations - combine:

Entity = Virtus.model(coerce: false)

class Task
  include Entity

  attribute :id
  attribute :title
end

class User
  include Entity

  attribute :id
  attribute :name

  attribute :tasks, Array[Task]
end

class UserMapper < ROM::Mapper
  relation :users
  register_as :entity

  model User

  combine :tasks, on: { id: :user_id } do
    model Task
  end
end

puts user_with_tasks.as(:entity).to_a.inspect
# [
#   #<User:0x007fc86a820250 @id=1, @name="Jane",
#       @tasks=[#<Task:0x007fc86a8221b8 @id=2, @title="Task for Jane">]>
# ]

More mapping goodies

Mapper DSL was extended with a couple of nifty features, let's quickly go through them:

Ability to reuse existing mappers:

class TaskMapper < ROM::Mapper
  model Task

  attribute :title
end

class UserMapper < ROM::Mapper
  model User

  group :tasks, mapper: TaskMapper
end

Ability to reject any unspecified keys:

class UserMapper < ROM::Mapper
  reject_keys true

  attribute :id
  attribute :name
end

mapper = UserMapper.build

mapper.call [{ id: 1, name: 'Jane', email: 'jane@doe.org' }]
# [{ :id => 1, :name => "Jane" }]

Ability to unwrap a nested hash:

class UserMapper < ROM::Mapper
  unwrap address: [:street, :city]
end

mapper = UserMapper.build

mapper.call [{ name: 'Jane', address: { street: 'Street 1', city: 'NYC' } }]
# [{ :name => "Jane", :street => "Street 1", :city => "NYC" }]

Registering Custom Objects as Mappers

Powerful ROM mapping DSL still not covering your specific needs? No worries, now you can register anything that responds to #call(data) as your mapper:

require 'rom'

my_mapper = -> data {
  # for the sake of example...
  data.map { |tuple| tuple[:name] }
}

ROM.setup :memory

ROM.mappers do
  register :users, name_list: my_mapper
end

class Users < ROM::Relation[:memory]
end

rom = ROM.finalize.env

rom.repositories[:default].dataset(:users).insert id: 1, name: 'Jane'
rom.repositories[:default].dataset(:users).insert id: 2, name: 'John'

puts rom.relation(:users).as(:name_list).to_a.inspect
# ["Jane", "John"]

Plugin Interface

Last but not least - ROM now has a basic plugin interface. We already ported a couple of features to plugin infrastructure. You can start experimenting with it already:

require 'rom'

module MyLoggerPlugin
  def self.included(command)
    # do stuff
  end
end

ROM.plugins do
  register :logger, MyPublisherPlugin, type: :command
end

class CreateStuffCommand < ROM::Commands::Create[:memory]
  use :logger
end

Plugins can be provided only for a specific adapter and are grouped by relation, command and mapper types.

Roadmap: Towards 1.0.0

ROM is already a powerful toolkit for data mapping and a uniform interface to various different data sources.

In the immediate future, version 0.8.0 will bring major improvements to the Command API and—as always—a bunch of smaller bug fixes and enhancements.

We’ve started using Waffle to help define the scope of work for upcoming minor versions and the goals for the final 1.0.0 release. All issues that need to be completed prior to the final 1.0.0 release are labelled as "1.0.0" and will be done in one of the minor releases before the final 1.0.0 RC is announced.

As we work towards the 1.0.0 release, we'll continue to document all of the features on this website, as well as improve the API reference docs. If all goes well, we’re aiming for 1.0.0 to arrive in late summer (that’s around August or September for all of you folks who aren’t in the Northern Hemisphere!).

We're getting there! Thanks to everyone who’s helped out with testing, experimental features, bug reports, documentation and development so far. Try out 0.7.0, and let us know what you think.


ROM 0.6.1 Released

We’re pleased to announce that ROM 0.6.1 is now available, building on the improvements and API changes in 0.6.0.

This release includes updates of the following gems:

Auto-mapping of command results

The results of commands can now be mapped in the same way as relations, using as or map_with.

rom.command(:rel_name).as(:mapper_name)

New migrations interface

A database migration interface is now included in rom-sql. This is currently a thin wrapper around the excellent Sequel Migrations API.

You can use migrations with one or more repositories as follows:

ROM.setup(
  default: [:sql, 'sqlite::memory'],
  other: [:sql, 'postgres://localhost/test']
)

ROM.finalize

ROM::SQL.migration do
  change do
    create_table(:users) do
      primary_key :id
      String :name
    end
  end
end

# for a non-default repository
ROM::SQL.migration(:other) do
  # ...
end

The expected way to run migrations is to require the Rake task from rom/sql/rake_task, and provide a db:setup task that sets up the ROM connection.

require 'sqlite3'
require 'rom/sql/rake_task'

namespace :db do
  task :setup do
    ROM.setup(:sql, 'sqlite::memory')
    ROM.finalize
  end
end

Improvements to forms for Rails users

There are several new changes that rationalize and improve the usability of the Rails forms API:

  • An optional mappings declaration has been added, which applies named mappers to the results of form commands.
  • Form generators can generate a shared base form class for new/update forms.
  • input and validations blocks are now available in all descendants of ROM::Model::Form.

The following example shows what this looks like in action, with base attributes and validations shared by the child classes, which each declare their own command, mapper, and attribute inputs:

# app/forms/user_form.rb
class UserForm < ROM::Model::Form
  input do
    set_model_name 'User'

    attribute :name, String
    attribute :email, String
  end

  validations do
    relation :users

    validates :name, :email, presence: true
    validates :email, uniqueness: true
  end
end

# app/forms/new_user_form.rb
class NewUserForm < UserForm
  commands users: :create

  mappings users: :entity

  input do
    timestamps(:created_at)
  end

  def commit!
    users.try { users.create.call(attributes) }
  end
end

# app/forms/update_user_form.rb
class UpdateUserForm < UserForm
  commands users: :update

  mappings users: :entity

  input do
    timestamps(:updated_at)
  end

  def commit!
    users.try { users.update.by_id(id).set(attributes) }
  end
end

Lotus integration

And last but not least, rom-lotus is out shiny new integration with the Lotus Framework.

A very basic setup example:

# config/environment.rb
require 'lotus/setup'
require 'rom-lotus'

require_relative '../apps/web/application'

Lotus::Container.configure do
  mount Web::Application, at: '/'

  ROM::Lotus.setup(Web::Application) do |setup|
    setup.repositories[:default] = [:sql, "sqlite:///tmp/test.sqlite"]
  end
end

Expect to hear more about this as the integration evolves.

Upgrading to 0.6.1

Please let us know if you have any trouble with the upgrade. Contact us directly via Zulip or submit an issue on GitHub describing any problems you're having.

Onwards to 0.7

We’re planning to focus heavily on improvements to mappers and commands in 0.7, which should get us significantly closer to figuring out what the shape of these APIs will be in 1.0.

Thanks for your support!


  • 8 of 9