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.