First Beta of ROM 1.0.0 Has Been Released

Exactly a year ago ROM 0.3.0 was released after the reboot of the project was announced, it was a complete rewrite that introduced the new and simplified adapter interface. Since then we've come a long way, the community has been growing, rom-rb organization on Github has now 28 projects including 16 adapters.

The core API, which also includes adapter interface, is stabilizing. This means we are coming very close to releasing the final version of rom 1.0.0 and today I'm very happy to announce that its first beta1 was released. Please notice this is the release of the core rom gem - adapters and other extensions remain unstable (in the sem-ver sense).

To ease testing we also released beta versions of minor upgrades of other rom gems so that you can install them from rubygems rather than relying on github sources. Overall the release includes following updates:

Changes In Setup API

Setting up ROM has been refactored and specific responsibilites have been broken down into smaller, and explicit, objects. Unfortunately it is a breaking change as ROM.setup method is gone. If you're using ROM with rails, things should continue to work, but if you have a custom rom setup additional actions are required.

This was an important change that resulted in a cleaner API, removed complex logic that used to rely on inheritance hooks to automatically register components and reduced the amount of global state that ROM relies on.

We kept convenience in mind though and introduced a feature that uses dir/file structure to infer components and register them automatically but without the complexity of relying on inheritance hooks.

Here's an example of a step-by-step setup with explicit registration:

# instead of ROM.setup:
config = ROM::Configuration.new(:sql, 'postgres://localhost/rom')

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

config.register_relation(Users)

# creates rom container with registered components
container = ROM.container(config)

Here's an example of a setup that infers components from dir/file names:

config = ROM::Configuration.new(:sql, 'postgres://localhost/rom')

# configure auto-registration by providing root path to your components
# namespacing is turned off
config.auto_registration("/path/to/components", namespace: false)

# assuming there's `/path/to/components/relations/users.rb`
# which defines `Users` relation class it will be automatically registered

# creates rom container with registered components
container = ROM.container(config)

You can also use auto-registration with namespaces, which is turned on by default:

config = ROM::Configuration.new(:sql, 'postgres://localhost/rom')

# configure auto-registration by providing root path to your components
# namespacing is turned on by default
config.auto_registration("/path/to/components")

# assuming there's `/path/to/components/relations/users.rb`
# which defines `Relations::Users` class it will be automatically registered

# creates rom container with registered components
container = ROM.container(config)

For a quick-start you can use an in-line style setup DSL:

rom = ROM.container(:sql, 'postgres://localhost/rom') do |config|
  config.use(:macros) # enable in-line component registration

  config.relation(:users) do
    def by_id(id)
      where(id: id)
    end
  end
end

rom.relation(:users) # returns registered users relation object

Command API Improvements

Probably the most noticable improvement/feature is the addition of the command graph DSL. The command graph was introduced in 0.9.0 and it allowed you to compose a single command that will be able to persist data coming in a nested structure, similar to nested_attributes_for in ActiveRecord, but more flexible.

This release introduces support for update and delete commands in the graph as well as a new DSL for graph definitions. Here's an example:

# assuming `rom` is your rom container and you have `create` commands
# for :users and :books relations
command = rom.command # returns command builder

# define a command that will persist user data with its book data
create_command = command.create(user: :users) do |user|
  user.create(:books)
end

# call it with a nested input
create_command.call(
  user: {
    name: "Jane",
    books: [{ title: "Book 1" }, { title: "Book 2" }]
  }
)

It also supports update (delete works in the same way):

# assuming `rom` is your rom container and you have `update` commands
# for :users and :books relations
command = rom.command # returns command builder

# define a command that will restrict user by its id and update it
user_update = command.restrict(:users) { |users, user| users.by_id(user[:id]) }

update_command = command.update(user: user_update) do |user|
  # define an inner update command for books
  books_update = user.restrict(:books) do |books, user, book|
    books.by_user(user).by_id(book[:id])
  end

  user.update(books: books_update)
end

# call it with a nested input
update_command.call(
  user: {
    id: 1,
    name: "Jane Doe",
    books: [{ id: 1, title: "Book 1" }, { id: 2, title: "Book 2" }]
  }
)

As a bonus, you are free to use all types of commands in the same graph and have complete freedom in defining how specific relations must be restricted for a given command.

New Command Result API

Starting from 1.0.0 you can check whether a command result was successful or not:

create_command = command.create(user: :users) do |user|
  user.create(:books)
end

result = create_command.call(
  user: {
    name: "Jane",
    books: [{ title: "Book 1" }, { title: "Book 2" }]
  }
)

result.success? # true if everything went fine, false otherwise
result.failure? # true if it failed, false otherwise

Relation API Extensions

Early version of rom-repository introduced a couple of plugins that now have become part of the core rom gem. They are opt-in and the adapter developers must decide whether or not it makes sense to enable them for adapter relations.

View

Relation view plugin is a DSL for defining relation views with an explicit header definition. It is typically useful for reusable relation projections that you can easily compose together in repositories.

Here's a simple example:

class Users < ROM::Relation[:sql]
  view(:listing, [:id, :name, :email]) do |*order_args|
    select(:id, :name, :email).order(*order_args)
  end
end

rom.relation(:users).listing(:name, :id)

This plugin plays major role in relation composition as it defines the header up-front, which allows repositories to generate mappers automatically, which is very convenient. It is also a nice way of specifying re-usable relation projections which some times may indicate where using an actual database view (assuming your db supports it) could simplify your queries.

Key Inference

This simple plugin provides default value for a foreign-key in a relation. It is used for generating relation views used for composition in the repositories.

You can use it too:

rom.relation(:users).foreign_key # => `:user_id`

Defining Default Datasets

It is now possible to not only specify the name of a relation dataset, but also configure it using a block, when you do that your relation will be initialized with whatever that blocks returns, it is executed in the context of the dataset object:

class Users < ROM::Relation[:sql]
  dataset(:people) do
    select(:id, :name).order(:id)
  end
end

Mapper Extension

There's one new feature in the mapper DSL where you can map values from multiple attributes into a new attribute:

class MyMapper < ROM::Mapper
  attribute :address, from: [:city, :street, :zipcode] do |city, street, zipcode|
    "#{city}, #{street}, #{zipcode}"
  end
end

Release Plan

Please try out the beta releases and provide feedback. Once we are sure that it works for everybody we'll be able to push the first RC and hopefully follow-up with the final 1.0.0 release shortly after the RC. Other gems that are now released as betas will be bumped to final versions and depend on rom 1.0.0 final.

The final release also means a major update of rom-rb.org along with a new set of documentation, guides and tutorials. This is still a work in progress and needs help, please get in touch if you're interested in helping out.

Once rom 1.0.0 is out there will be major focus on rom-sql and rom-repository. There's a plan to improve query DSL in rom-sql and provide full CRUD interface for repositories that should be handy for simple applications.

If you see any issues, please report them in the individual issue trackers on Github or main rom if you are not sure which gem it relates to.


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:

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 and Validator 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


  • 6 of 9