ROM 4.0.0 released

After over six months of work, we're pleased to announce the release of rom-rb 4.0.0! This is a major release which brings many improvements and new features. One of the biggest priorities of this release was to solidify and improve automatic mapping capabilities, by extending core API with features that were previously implemented only in rom-repository. This means that the most advanced features, such as automatic mappers or inferring struct objects, are now part of core API, and it makes using rom-rb much simpler. Apart from this, we also added Association API to the core, which enables associations for all adapters, and you can define associations between different databases.

Here are some of the highlights of 4.0.0 release.

Automatic mapping

Starting with 4.0.0, rom-rb can infer mappers on-the-fly for all relations. It can generate mappers for "flat" relations, as well as combined or wrapped relations. You don't have to use any structs (aka models) to use this feature, it works with plain hashes too.

Let's say we have users and tasks relations:

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end
end

Now we can load data using these relations easily, including preloaded association data:

> users.first
=> {:id=>1, :name=>"Jane"}

> users.combine(:tasks).first
=> {:id=>1, :name=>"Jane", :tasks=>[{:id=>1, :user_id=>1, :title=>"Jane's task"}]}

> tasks.wrap(:user).first
=> {:id=>1, :user_id=>1, :title=>"Jane's task", :user=>{:id=>1, :name=>"Jane"}}

Auto-struct mapping

Relations can now automatically infer struct objects based on their schema information. This feature can be enabled via auto_struct setting and it is enabled by default, when you use relations through repositories. Let's tweak previous example to use this feature:

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end

  auto_struct true
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end

  auto_struct true
end

Now we will get convenient struct objects back:

> users.first
=> #<ROM::Struct::User id=1 name="Jane">

> users.combine(:tasks).first
=> #<ROM::Struct::User id=1 name="Jane" tasks=[#<ROM::Struct::Task id=1 user_id=1 title="Jane's task">]>

> tasks.wrap(:user).first
=> #<ROM::Struct::Task id=1 user_id=1 title="Jane's task" user=#<ROM::Struct::User id=1 name="Jane">>

Support for custom struct classes

This is probably the biggest enhancement, you can now configure your own struct_namespace where your own struct classes are defined, and there's no need to define attributes. This can be called "Active Record mode" (in a good way!). Resulting struct objects are still decoupled from the database, their structure is based on relation data, which can be projected anyhow you want. This means that we have dynamic struct objects, but without 1:1 mapping between your database schema and their attributes.

Let's say you decide to put your own struct classes under Entities module. This module can be configured as the struct_namespace, and rom mappers will automatically find classes, matching relation names. Here's an example:

module Entities
  class User < ROM::Struct
    def task_titles
      tasks.map(&:title)
    end
  end
end

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :tasks
    end
  end

  auto_struct true
  struct_namespace Entities
end

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end

  auto_struct true
  struct_namespace Entities
end

Now we will get instances of your own struct class:

> jane = users.combine(:tasks).first
> jane.task_titles
=> ["Jane's task"]

You can learn more about this feature in our updated docs

Standalone changesets

Changesets are now provided by a separate rom-changeset gem and they no longer need repositories. Relations are extended by :changeset plugin, which adds Relation#changeset method. This makes using changesets more straightforward, here's an example:

users.changeset(:create, name: "John").commit
# {:id=>2, :name=>"John"}

user_changeset = users.by_pk(2).changeset(:update, name: "John Doe")

user_changeset.diff?
# => true

user_changeset.diff
# => {:name=>"John Doe"}

user_changeset.commit
# {:id=>2, :name=>"John Doe"}

See Changeset documentation for more information.

...and more

There are dozens of other improvements and new features, to quickly summarize few more:

  • Experimental auto-migration feature known from DataMapper project - you will hear more about this soon!
  • You no longer need to define relations in repository classes
  • You can define custom association views with non-standard combine/join keys
  • Configuration uses an event bus now, which you can use to hook into setup process and enable additional features. Our plugin system uses it already.
  • New APIs have been added to rom-sql, including SQL::Relation#exists, SQL::Relation#each_batch, SQL::Relation#import and SQL::Relation#explain (for PG)
  • SQL conditions can be negated by using idiomatic ! operator, ie users.where { !admin.is(true) }

Release information and upgrading

This is a major release with breaking changes. Please refer to the upgrade guide for more information. As part of 4.0.0, following gems have been released:

If you're having problems with the upgrade, please seek for help on discussion forum.

Thank you :)

Thank you to all our contributors and supporters. Special thanks go to Nikita Shilnikov for his amazing work on making rom-sql better, implementing auto-migrations feature and helping with development of core APIs and addressing issues!

This has been the biggest effort so far, and it's probably the most important release we've had. We'll continue working on bug-fix and minor upgrades soon (there are already PRs opened with new features!), so stay tuned.

Check out rom-rb 4.0.0 and tell us what you think!


rom-repository 1.1.0 released

Today we've released a new version of rom-repository which ships with new features and a couple of bug fixes. This release focused on improving auto-mapping capabilities and making it more flexible when you want to use custom mappers.

Adjusting relation nodes when loading aggregates

It's now possible to adjust individual relation nodes when you're loading aggregates or composing relations manually. This feature allows you to apply additional restrictions, or use custom views, or even set custom mappers on-the-fly.

Let's say we want to load users with their priority tasks:

aggregate(:tasks).
  node(:tasks) { |tasks| tasks.where { priority < 3 }

This also works with deeply nested nodes:

aggregate(orders: :lines).
  node(orders: :lines) { |lines| lines.where(id: line_ids) }

Disabling auto-mapping to structs

By default all repositories map plain hashes to ROM::Struct objects, you can now disable this feature. This is useful in situations like mapping to JSON where intermediate objects are simply not needed, or when you want to use custom mappers that require hashes rather than structs.

You can disable mapping to structs using a repo class option:

class UserRepo < ROM::Repository[:users]
  auto_struct(false)
end

user_repo = UserRepo.new(rom)
user_repo.users.to_a
# [{:id=>1, :name=>"Jane"}]

or when instantiating a repo object:

user_repo = UserRepo.new(rom, auto_struct: false)

or at run-time per individual relation:

class UserRepo < ROM::Repository[:users]
  def user_hashes
    users.with(auto_struct: false).to_a
  end
end

user_repo = UserRepo.new(rom)
user_repo.user_hashes
# [{:id=>1, :name=>"Jane"}]

Improved support for wrapping

Just like combine, wrap now accepts association names. This simplifies loading nested data structures via joins. Here's a simple example:

class Tasks < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      belongs_to :user
    end
  end
end

class TaskRepo < ROM::Repository[:tasks]
  def with_user(id)
    tasks.wrap(:user).by_pk(id).one
  end
end

task_repo = TaskRepo.new(rom)

task_repo.with_user(id)
# #<ROM::Struct[Task] id=1 user_id=1 title="A task" user=#<ROM::Struct[User] id=1 name="Jane">>

Using custom mappers along with auto-mapping

You can now use auto_map option in map_with method which will apply auto-mapping before applying your own mappers. This is useful in cases where you want to use custom mappers and you want auto-mapping to handle complex structural transformations like merging multiple data sets into nested structures (which is what happens when you compose relations using aggregate or combine).

To enable auto-mapping with custom mappers, simply pass auto_map: true option, this way you don't have to worry about handling aggregates manually, as your mappers will be applied to already transformed data:

class UserRepo < ROM::Repository[:users]
  relations :tasks

  def custom_mapping
    aggregate(:tasks).
      map_with(:my_custom_mapper, auto_map: true)
  end
end

Release Details

This is a backward compatible upgrade, for more information check out the CHANGELOG. If you found any issues, or have trouble upgrading, please report it.


  • 3 of 9