ROM 0.9.0 Released
We are pleased to announce the release of ROM 0.9.0! This is a big release which focuses on internal clean-up in the core library as a preparation for 1.0.0. For those of you hungry for new features - you won't be disappointed. As part of this release we are introducing new adapters, new gems extracted from rom and the long awaited high-level interface for ROM called rom-repository.
For notes about upgrading to ROM 0.9.0 please refer to Upgrade Guides.
Gem updates summary:
- rom 0.9.0 CHANGELOG
- rom-sql 0.6.0 CHANGELOG
- rom-yaml 0.2.0 CHANGELOG
- rom-csv 0.2.0 CHANGELOG
- rom-rails 0.5.0 CHANGELOG
- rom-lotus 0.2.0 CHANGELOG
- rom-roda 0.2.0 CHANGELOG
New gems:
- rom-repository 0.1.0 - a higher-level interface with auto-mapping and additional relation plugins
- rom-mapper 0.1.1 - standalone mapper objects extracted from rom
- rom-model 0.1.1 - extracted from rom-rails, includes
Attributes
andValidator
extensions - rom-support 0.1.0 - a bunch of small extensions reused across all rom gems
New adapters:
- rom-couchdb - an adapter for CouchDB
- rom-http - an abstract HTTP adapter useful for implementing concrete adapters for HTTP APIs
- rom-rethinkdb - an adapter for RethinkDB
Repository
Probably the most significant addition coming with this release is rom-repository. Using lower-level APIs and configuring mappers manually is tedious in most of the cases that's why Repository was introduced.
Repository interface is very simple and built on top of Relation and Mapper API. It allows you to easily work with relations and have results automatically mapped to struct-like objects. There are a couple of neat plugins that ship with this gem which make relation composition ridiculously simple.
Repositories work with all adapters which means you can combine data from different data sources.
Here's an example repository class:
class UserRepository < ROM::Repository::Base
relations :users, :tasks
def with_tasks(id)
users.by_id(id).combine_children(many: tasks)
end
end
user_repo.with_tasks.to_a
# [#<ROM::Struct[User] id=1 name="Jane" tasks=[#<ROM::Struct[Task] id=2 user_id=1 title="Jane Task">]>, #<ROM::Struct[User] id=2 name="Joe" tasks=[#<ROM::Struct[Task] id=1 user_id=2 title="Joe Task">]>]
Please refer to Repository Guide for the rationale and more information.
Multi-Environment Support
Initially, ROM supported its setup process through a global environment object. This was a good start that worked well with frameworks like Rails that expect globally accessible objects; however, we're pushing towards removing global state as much as possible.
For that reason in ROM 0.9.0 you can configure the environment as a standalone object, which comes with the benefit of being able to have more than one environment. Why would you want to have many environments? For example for database sharding, or separating components within your application where data comes from different sources and you want to keep them isolated.
Here's an example of a multi-environment setup:
class Persistence::Command::CreateUser < ROM::Commands::Create[:sql]
relation :users
register_as :create
end
class Persistence::Query::Users < ROM::Relation[:sql]
dataset :users
end
command_env = ROM::Environment.new
command_env.setup(:sql, [:postgres, 'postgres://command_host/my_db')
command_env.register_relation(Persistence::Command::CreateUser)
command_container = command_env.finalize.env
command_container.command(:users) # access to defined commands
query_env = ROM::Environment.new
query_env.setup(:sql, [:postgres, 'postgres://query_host/my_db')
query_env.register_relation(Persistence::Query::Users)
query_container = query_env.finalize.env
query_container.relation(:users) # access to defined relations
Global setup process still works, but please refer to upgrade guide if you are using ROM standalone without any framework integration.
Gateway Configuration Support
A new interface for configuring individual adapter gateways has been added. For now the only customization you can make is configuring how relation inferrence should work:
# disable inferring relations from schema
ROM.setup(:sql, [
:postgres, 'postgres://localhost/db', infer_relations: false
])
# cherry-pick which relations should be inferred
ROM.setup(:sql, [
:postgres, 'postgres://localhost/db', inferrable_relations: [:users, :tasks]
])
# disallow inferrence for specific relations
ROM.setup(:sql, [
:postgres, 'postgres://localhost/db', not_inferrable_relations: [:some_table]
])
This feature is useful when you have a big database and you don't want to use ROM to deal with all of your relations.
Extracted Standalone Mappers
You can now install rom-mapper as a standalone gem and use the powerful mapping DSL:
require 'rom-mapper'
class PostMapper < ROM::Mapper
attribute :title, from: 'post_title'
wrap :author do
attribute :name, from: 'post_author'
attribute :email, from: 'post_author_email'
end
end
post_mapper = PostMapper.build
post_mapper.call([
{ 'post_title' => 'Hello World', 'post_author' => 'Jane', 'post_author_email' => 'jane@doe.org' }
])
# [{:title=>"Hello World", :author=>{:name=>"Jane", :email=>"jane@doe.org"}}]
Mappers are very powerful, make sure to check out the Mapper Guides.
All Relations Are Lazy
Before 0.9.0, ROM had a separate layer for decorating your relations with a lazy-proxy wrapper. This has caused some confusion and unnecessary complexity, as the relations you defined were not the same thing that the #relation()
method returned. It also turned out that implementing rom-repository was more difficult than it should have been.
That's why in ROM 0.9.0 all relations have lazy interface. It means that every relation method you define is auto-curried:
class Users < ROM::Relation[:sql]
def by_name(name)
where(name: name)
end
end
# assuming your container is called `rom`
users = rom.relation(:users)
user_by_name = users.by_name # returns auto-curried relation
user_by_name['Jane'].one! # call later on to apply the required argument
Adapter Query DSL is Public
Starting from ROM 0.9.0, the query interface exposed by individual adapters is public, but it is not recommended to use it directly in your application. Relations should be used to encapsulate data access properly and query DSLs should not leak to the application layer.
# this is considered as a smell
users.where(name: "Jane")
# that's the proper way™
users.by_name("Jane")
Extracted Model Extensions
A couple of useful extensions have been extracted from the rom-rails gem into rom-model. These are standalone components that are based on Virtus and ActiveModel. We have an ambitious plan to rewrite it in the future on top of more powerful tools. Please refer to rom-model README for more information.
Right now you can use Attributes
and enhanced Validator
objects with nice support for embedded validations:
class AuthorInput
include ROM::Model::Attributes
attribute :name, String
attribute :email, String
end
class PostInput
include ROM::Model::Attributes
attribute :title, String
attribute :author, AuthorInput
end
class PostValidator
include ROM::Model::Validator
validates :title, presence: true
embedded :author do
validates :name, :email, presence: true
end
end
input = PostInput[{ title: 'Hello World', author: { name: 'Jane', email: 'jane@doe.org' } }]
validator = PostValidator.new(input)
validator.valid?
HTTP Adapter
The new, abstract rom-http
adapter is a fantastic addition to the growing list of ROM adapters. It gives you a solid foundation for building a custom adapter which needs to talk via HTTP protocol. It's pretty flexible, and works like any other rom adapter - which means that you can use either the lower-level relation and mapping APIs or set it up with rom-repository
and auto-mapping.
require 'json'
require 'http'
class Users < ROM::Relation[:http]
dataset :users
def by_id(id)
with_path(id.to_s)
end
end
rom = ROM::Environment.new
rom.setup(:http, {
uri: 'http://jsonplaceholder.typicode.com',
request_handler: ->(dataset) {
HTTP.headers(dataset.headers).public_send(
dataset.request_method,
"#{dataset.uri}/#{dataset.name}/#{dataset.path}",
params: dataset.params
)
},
response_handler: ->(response, dataset) {
Array([JSON.parse(response.body)]).flatten
}
})
rom.register_relation(Users)
container = rom.finalize.env
container.relation(:users).by_id(1).to_a
# => GET http://jsonplaceholder.typicode.com/users/1 [ Accept: application/json ]
Support Campaign
As announced a couple of weeks ago, we're running a campaign for sustainable development. We already have people who've decided to donate - thank you so much for your support.
With ROM 0.9.0 we're close to the first stable 1.0.0 release, but there's still a lot to be done. Please consider supporting this great effort.
Please also remember that ROM is a project open for contributions and currently we have 24 repositories under our GitHub organization. There are many adapters looking for maintainers, there are many smaller tasks to do in core libraries, framework integrations and other extensions. Please get in touch if you're interested in contributing <3.
Reporting Issues and Support
All repositories now have their own issue trackers enabled on GitHub. If you find a bug, or have problems using ROM, please report an issue for a specific project. If you're not sure which project it relates to, just report it in the main rom issue tracker, and we'll move it to the right place if needed.
For any random questions and support requests you can talk to us on zulip.
Last but not least - we're looking for help in setting up a Discourse instance on DigitalOcean to make it simpler for people to discuss things as an alternative to gitter.
We've got a discourse instance up and running :)
ROM 1.0.0 - See You at ROSSConf!
In case you missed it, ROM is part of the second edition of ROSSConf in Berlin, where you'll have a chance to contribute to the project. We have a crazy plan to release 1.0.0 during the event or at least close all the remaining issues and get an RC out of the door. :)
We'll be working on the list of issues scheduled for 1.0.0, thus it is important to get as much feedback as possible from you.
Please try out ROM 0.9.0. Let us know your thoughts. Report issues, ideas, comments, anything that can help in specifying what should be done for 1.0.0 will be grately appreciated.
Thanks! <3