V2.2: Overview


Hanami’s persistence layer is based on the Ruby Object Mapper (ROM) project. ROM may be a radically different approach to persistence than what you’re familiar with, but don’t let that scare you. ROM is designed to provide clear separation of responsibilities. The hardest part is shifting your perspective to think in a new paradigm.

Above all else ROM favors:
  • Explicitness over “magic” whenever possible
  • Speed, because performance is a feature
  • Flexibility in your domain layer’s design

ROM: Principles & Design

While traditional Object-Relational Mapping comes from a strictly Object-Oriented approach, ROM combines the best parts of Functional Programming and OOP that play to Ruby’s inherent strengths as a language. Instead of homogenizing all datastores into a lowest-common-denominator API, ROM embraces the diversity of storage engines and the powerful features they can provide.

Relations

Relations represent a collection of information in your persistence layer, and its relationships to other collections. They contain a schema, associations, and a dataset.

Relations model how you query the persistence layer for information. With a SQL database, this means constructing SQL queries. But importantly, ROM does not assume your backend technology; you could be querying a YAML file, a document-based storage engine, or an HTTP endpoint as well. Hanami does not officially support doing this yet, but it is possible.

The default adapter used by Hanami::DB::Relation is the ROM::SQL adapter. The adapter defines the top-level semantics of how the storage engine works, and how to connect to it. ROM::SQL is based on the Sequel library.

Schemas

The schema defines the shape of the incoming data and how it should be coerced into Ruby types. Most of the time, you will see this inferred from the database directly:

class Users < Hanami::DB::Relation
  schema :users, infer: true
end

However, if you need to make any alterations to the defaults you can explicitly declare schema with or without inference.

class Users < Hanami::DB::Relation
  schema :users, infer: true do
    attribute :email, Types::Email
  end
end

ROM::SQL provides a wide array of data types for SQL engines, but you can provide your own based on dry-types. In this example, Types::Email would be user-defined.

For more on Schemas, see the relations guide.

Associations

Associations define the relationships between individual Relations.

class Users < Hanami::DB::Relation
  schema(infer: true) do
    associations do
      has_many :users_tasks
      has_many :tasks, through: :users_tasks
    end
  end
end

For more on associations, see the relations guide.

Datasets

Datasets define how the underlying data is fetched by default. ROM defaults to selecting all attributes in the schema, but this is simple to override.

class Users < Hanami::DB::Relation
  schema(:users, infer: true)

  dataset do
    select(:id, :name).order(:name)
  end
end

The dataset can be thought of as the default state of the query; adding query conditions builds up the query from there.

The output of Dataset queries are plain Ruby hashes, which are consumed by a Repository (and automatically converted to Structs there).

For more on Datasets, see the relations guide.

Repositories

Repositories are the primary public interface of the persistence layer. They exist to bridge the gap between business objects and database objects.

Because Relation queries are highly dependent on the shape of the persistence layer, the Repository encapsulates this responsibility with a permanent, public API.

Consider the case of a users table that originally used emails as the identity of the user.

class UserRepo < Hanami::DB::Repo
  def find(email) = users.where(email:).one
end

Let’s say the requirement has changed, to use usernames as the principal identity instead. Without a Repository, every place in your codebase that queries a User would need to accommodate this change. But here, we can do:

class UserRepo < Hanami::DB::Repo
  def find(username) = users.where(username:).one
end

If the rest of your business logic treats the identity as an opaque string, then you’re done. The encapsulation afforded by Repository restricts the knowledge of the persistence layer from where it does not belong.

For more on Repositories, see ROM: Repositories

Structs

The final output of a Repository is a Struct.

A Hanami struct is immutable, and contains no business logic. They are extensible for adding presentation logic:

module Main
  module Structs
    class User < Hanami::DB::Struct
      def full_name
        "#{given_name} #{family_name}"
      end

      def mailbox
        "#{full_name} <#{email}>"
      end
    end
  end
end

But you don’t need to define every struct ahead of time, only to extend its functionality. If you don’t define a Struct class, Structs will be generated on-demand in the appropriate namespace.

A Struct is not a permanent abstraction of a piece of data: it is a momentary projection of the data you requested. This means that instead of a User model that fills every role you need of a user, you could project user data as a Credential for authentication, a Role for authorization, a Visitor for displaying their identity on the page. Every projection can serve a specific purpose, and contain exactly the information you need and nothing more.

More importantly, the form your structs take in the application layer can change independently of the persistence layer. Your database tables can change without impacting the structs; the application structs can change without impacting the database. All you have to do is manage the changing relationships to produce the same output.