ROM 3.0 Released

We're happy to announce the release of rom 3.0.0, a big release which comes with the first stable versions of rom-sql 1.0.0 and rom-repository 1.0.0. Changes and improvements in rom core gem focused mostly on extending functionality of relation Schema API and Command API, as well as removing all deprecated core APIs. The biggest highlight are new features in rom-sql and rom-repository.

Extended Schemas

Starting from rom 3.0.0 all relations, regardless of the adapter, have schemas. It means that Schema is now a 1st class API available in all adapters.

Schemas now use adapter-specific attribute types, which allowed us to implement all kinds of new features in rom-sql that make building complex queries much simpler. We also improved schema inference and added support for more PostgreSQL types like enum, point or inet. Furthermore, it's now possible to use inference along with explicit attribute definitions, which is useful in cases where inferrer doesn't support some custom column type, or when you simply want to customize your schema.

Advanced projections

Relation schemas are always available, they keep track of the current attributes that relation tuples will include. This is a huge improvement, since previously schemas were only the representation of canonical relations (defined by your actual database schema). Projections go through schemas, and they adjust their attributes automatically. This gives us complete information about data that any relation can return.

In rom-sql schema attributes are extended with SQL-specific features, which allows queries like this:

class Users < ROM::Relation[:sql]
  schema(infer: true)

  def duplicated_emails
    select { [email, int::count(id).as(:count)] }.
      group(:email).
      order(:email)
  end
  # SELECT "email", COUNT("id") AS "count"
  #  FROM "users"
  #  GROUP BY "email"
  #  ORDER BY "email"
end

You can use both blocks or refer to attributes directly through Relation#[] method which returns schema attributes identified by their canonical names:

class Users < ROM::Relation[:sql]
  schema(infer: true)

  def duplicated_emails
    select(self[:email], self[:id].func { int::count(id).as(:count) }).
      group(:email).
      order(:email)
  end
  # SELECT "email", COUNT("id") AS "count"
  #   FROM "users"
  #   GROUP BY "email"
  #   ORDER BY "email"
end

Resulting relation views include complete information about their current schema:

users.duplicated_emails.schema.attributes
# [
#   #<ROM::SQL::Attribute[NilClass | String] name=:email source=ROM::Relation::Name(users)>,
#   #<ROM::SQL::Function[Integer] func=#<Sequel::SQL::Function @name=>"COUNT", @args=>[#<ROM::SQL::Attribute[Integer] primary_key=true name=:id source=ROM::Relation::Name(users)>], @opts=>{}> alias=:count>
# ]

This plays a major role in automatic mapping in repositories, as they can define structs with all attribute type information provided by relations.

Support for SQL functions

The count function we used in the previous example probably caught your attention—this is a new feature which allows you to use any SQL function with arbitrary arguments, including relation attributes, or anything that can be dumped into a valid SQL. The syntax is always:

type_annotation::function_name(*arguments)

It returns a ROM::SQL::Function object which works like any other schema attribute, and you can qualify it or provide an alias, or use a boolean expression with various operators. Functions work with select, order and where. The only difference is that select requires a type annotation, and the other methods don't.

Here's another example:

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

  def busy_people
    select(:id, :name, tasks[:id].func { int::count(id).as(:task_count) }).
      left_join(tasks).
      group(:id).
      having { count(id.qualified) > 1 }
  end
  # SELECT "users"."id", "users"."name", COUNT("tasks"."id") AS "task_count"
  #   FROM "users"
  #   LEFT JOIN "tasks" ON ("users"."id" = "tasks"."user_id")
  #   GROUP BY "users"."id"
  #   HAVING (count("users"."id") > 1) ORDER BY "users"."id"
end

Maybe you noticed that we passed tasks relation object to left_join—this is another new feature.

Improved joins

You can now pass relation objects to join, left_join and right_join, and, assuming you configured associations, your relation will do the work for you to join the correct table with join conditions already set. Furthermore, it will be automatically qualified. Here's what it means:

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

  def with_user
    # "join(:users, user_id: :id).qualified" becomes:
    join(users)
  end
end

There's also a new method to simplify finding all associated tuples through associations, called Relation#assoc, it uses configured associations to prepare a joined relation for you. Let's say we want to find all user priority tasks:

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

  def priority_tasks(user_ids)
    assoc(:tasks).
      select(:id, :title, self[:name].qualified.as(:user))
      where(priority: 1, user_id: user_ids)
  end
end

users.priority_tasks([1, 2, 3])
# SELECT "tasks"."id", "tasks"."user_id", "tasks"."title", "users"."name" AS "user"
#   FROM "tasks"
#   INNER JOIN "users" ON ("users"."id" = "tasks"."user_id")
#   WHERE ("priority" = 1) AND ("user_id" IN(1, 2, 3))
#   ORDER BY "tasks"."id"

Custom association views

Another interesting feature is the ability to extend default association relations with a custom view. This is useful in cases where you would like to add more attributes to the resulting relation, change order etc.

Let's say we have users with accounts, and would like to include position from the join table and order accounts by that column:

class Users < ROM::Relation[:sql]
  schema(infer: true) do
    associations do
      has_many :accounts, through: :users_accounts, view: :ordered
    end
  end
end

class Accounts < ROM::Relation[:sql]
  schema(infer: true)

  view(:ordered) do
    schema do
      append(relations[:users_accounts][:position])
    end

    relation do
      order(:position)
    end
  end
end

We use View DSL in this case, as it provides schema information up-front, before a relation is even initialized. It's one of the core features in rom that allows defining composable relations. Associations are based on this feature, but you can use it without associations too.

Bi-directional coercions

In the first version of Schema API, canonical attribute types were used by commands exclusively. Starting with rom 3.0.0 you can also define read attributes, which will be used by relations when they read their tuples.

Let's say we have address JSONB column, and we want a custom address object back that uses JSONB attributes:

Address = Struct.new(:country, :city, :street, :zipcode)

class Users < ROM::Relation[:sql]
  AddressType = Types.Constructor(Address) { |value| Address.new(*value) }

  schema(infer: true) do
    attribute :address, Types::PG::JSONB, read: AddressType
  end
end

users.select(:address).to_a
# [{:address=>#<struct Address country="Poland", city="Krakow", street="Street 1"}]

For now this is very explicit, in the near future we'll add various convention-based improvements, so that specifying read types is more concise.

Improved repositories

We added support for transactions, custom commands in changesets, run-time changeset mapping with custom blocks, associating data via changesets, asking for custom objects when committing a changeset... check out updated docs to learn more, and here are some of the highlights.

Comitting changesets

Changesets are now standalone objects, with a new Changeset#commit method which allows you to store them conveniently in your database.

Here's a simple example of a changeset which saves a new user:

# assuming we have a repo like that:
class UserRepo < ROM::Repository[:users]
end

user_repo.changeset(name: "Jane", email: "jane@doe.org").commit
# => {:id=>1, :name=>"Jane", :email=>"jane@doe.org"}

Committing changesets via repositories

Changesets simply return whatever your database returned, but you can commit them via repositories that will convert raw data to rom structs:

# assuming we have a repo like that:
class UserRepo < ROM::Repository[:users]
  command :create
end

new_user = user_repo.changeset(name: "Jane", email: "jane@doe.org")

user_repo.create(new_user)
# => #<ROM::Struct[User] id=1 name="Jane" email="jane@doe.org">

Powerful data transformations

Changeset now support custom data transformations, with many builtin functions provided by transproc gem. You can define your custom changeset classes and specify how data must be transformed before we can pass it to the underlying database command:

class NewUser < ROM::Changeset::Create[:users]
  map do
    unwrap :address, prefix: true
  end
end

new_user = user_repo.changeset(NewUser).data(
  name: "Jane",
  address: {
    city: "Krakow", country: "Poland", street: "Street 1", zipcode: "1234"
  }
)

new_user.commit
# => {:id=>1, :name=>"Jane", address_city: "Krakow", address_country: "Poland", address_street: "Street 1", address_zipcode: "1234"}

You can also pass an argument to .map and in that case you can use arbitrary code to perform a transformation. Check out docs to learn more.

Support for nested aggregates

If you specified your associations in relations, you can use a simplified interface for fetching aggregates through repositories. For example if you have users with tasks, and tasks have tags, and you want to load a user aggregate with more levels of nesting, you can now do this:

class UserRepo < ROM::Repository[:users]
  relations :tasks, :tags
end

user_repo.aggregate(tasks: :tags)

Check out Repository#aggregate API docs for more information.

Detailed release information

As part of this release following gems have been published:

Please check out rom-rb.org as it was updated with more documentation!

Upgrading

Please do read rom-sql 1.0.0 upgrade guide as it includes useful information. Making the transition should not be difficult, many applications (including big ones), have been already upgraded during beta/RC testing, and it was a smooth process.

If you have problems with the upgrade, please report an issue or ask for help on the discussion forum.

Thank you!

This is a long post, and it barely covers ~20% of what was improved or added, it was a huge effort to get here and I would like to thank all of the contributors, for their PRs, reported issues, and testing beta/rc releases early!

Special thanks go to (in no particular order):

  • Nikita Shilnikov, for his fantastic work on schema inferrers, helping a lot with rom-support removal by putting together dry-core and porting all rom gems to use dry-initializer
  • Sergey Kukunin for his help with transproc 1.0.0 (yes, we released that too!) and helping with rom-repository mapping pipeline
  • Andrew Kozin for his work on dry-initializer which now plays major role in many core objects in rom projects and allowed us to get rid of rom-support

What happens next?

We have big plans for future releases, but hopefully we'll manage to provide more frequent, incremental improvements now that rom-sql and rom-repository are stable. More details will be revealed soon, stay tuned!

I hope you'll find this release useful, if you have problems or any kind of feedback, please report issues or just talk to us.

If you happen to attend RubyConf AU next week, be sure to say hi! :)


ROM 2.0.0 Released

We’re happy to announce the release of rom 2.0 with a big list of improvements and new features! This release is a huge milestone for the project as its infrastructure is now complete - this means that all core APIs and high-level extensions are in place, and from now on it’s a matter of expanding functionality with new features.

Let’s take a quick look at the release highlights!

Relation Schemas

This new feature allows you to define a relation schema with attribute names and types.

By defining a relation schema you establish the canonical representation of the data provided by a relation. This gives you type-safe commands out-of-the-box and allows you to define custom types for low-level database coercions.

On top of that, every adapter can extend Schema API with its own features, and that’s how the new version of rom-sql adds support for defining associations.

Here’s an example:

class Users < ROM::Relation[:users]
  schema do
    attribute :id, Types::Serial
    attribute :name, Types::String

    associations do
      has_many :tasks
      belongs_to :group
    end
  end
end

You can learn more about relation schemas in the user documentation. For more information about associations check out SQL docs.

Command support in repositories

The new version of rom-repository adds support for database commands. This is a convenient API for common create/update/delete operations. It means there’s no need to define custom command classes for common operations anymore. Here’s an example:

class UserRepo < ROM::Repository[:users]
  commands :create, update: :by_pk, delete: :by_pk
end

# create
user = user_repo.create(name: "Jane")

# update
user_repo.update(user.id, name: "Jane Doe")

# delete
user_repo.delete(user.id)

Check out the user documentation for more information.

Repository changesets

Another new feature is the Changeset API, provided by rom-repository. This is a new way to handle data changesets via repository commands, similar to Elixir’s Ecto.

Changesets can be created via the Repository#changeset interface, here’s an example of “an update changeset”:

class UserRepo < ROM::Repository[:users]
  commands :create, update: :by_pk
end

user = user_repo.create(name: 'Jane', email: 'jane@doe.org')

changeset = user_repo.changeset(user.id, name: 'Jane Doe')

changeset.diff? # true
changeset.diff # {name=>"Jane Doe"}

user_repo.update(user.id, changeset)

This is a very fresh addition, please try it out and let us know what you think! You can learn more about it in the user documentation.

Native Upsert support for PostgreSQL >= 9.5

We added a new command type to rom-sql called Postgres::Upsert, this allows you to use native upsert statements on PostgreSQL. The repositories don't support it yet, but you can register commands manually and they will be available within the repositories.

Here's a full example of a setup with an Upsert command:

require 'rom-repository'
require 'rom-sql'
require 'rom/sql/commands/postgres'

conf = ROM::Configuration.new(:sql, 'postgres://localhost/rom_example')

conf.default.connection.create_table? :quotes do
  primary_key :id
  String :quote, unique: true, null: false
  Integer :likes, default: 1
end

class UpsertQuote < ROM::SQL::Commands::Postgres::Upsert
  relation :quotes
  register_as :create_or_update
  result :one

  conflict_target :quote
  update_statement likes: Sequel.+(:quotes__likes, 1)
end

conf.register_command(UpsertQuote)

class QuoteRepo < ROM::Repository[:quotes]
  def create_or_update(quote)
    command(:quotes)[:create_or_update].call(quote)
  end
end

rom = ROM.container(conf)

quote_repo = QuoteRepo.new(rom)

quote = quote_repo.create_or_update(quote: 'Such quote')

puts quote.inspect
# {:id=>1, :quote=>"Such quote", :likes=>1}

quote = quote_repo.create_or_update(quote: 'Such quote')

puts quote.inspect
# {:id=>1, :quote=>"Such quote", :likes=>2}

New website design & new documentation

Thanks to the awesome work of Angelo Ashmore, we’ve got a shiny new website design. We also updated the user docs and improved API docs coverage for most important rom gems.

We’re looking for help with adding guides, please check out the issues if you’re interested in contributing a guide.

Hanami and Trailblazer co-operation!

We started working closely with the Hanami and Trailblazer communities, looking into how our projects can help each other. As a result, we’ll be working on a rom-based backend for hanami-model, and integrating rom with Reform from Trailblazer.

These are really exciting times, we believe our co-operation will help us build a stronger ruby ecosystem!

What happens next?

We’d like to finish stable versions of rom-sql and rom-repository later this year. The core rom gem is already at version 2.0, but the main adapter and repositories are still unstable. There will be a lot of effort going into improving the query DSL in rom-sql, there are a lot of Sequel features that we could leverage and make the adapter even more powerful.

Depending on the feedback, we’ll also focus on expanding functionality of existing features and bug-fixing. Please report any issues on GitHub for individual rom projects that you use.

Gem updates summary

Please see the upgrade guide for more information about upgrading.

As part of this release following gems have been released:


  • 4 of 9