Repositories

An object that mediates between entities and the persistence layer. It offers a standardized API to query and execute commands on a database.

A repository is storage independent, all the queries and commands are delegated to the current adapter.

This architecture has several advantages:

As of the current version, Hanami only supports SQL databases.

Interface

When a class inherits from Hanami::Repository, it will receive the following interface:

A collection is a homogenous set of records. It corresponds to a table for a SQL database or to a MongoDB collection.

repository = BookRepository.new

book = repository.create(title: "Hanami")
  # => #<Book:0x007f95cbd8b7c0 @attributes={:id=>1, :title=>"Hanami", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:02:37 UTC}>

book = repository.find(book.id)
  # => #<Book:0x007f95cbd5a030 @attributes={:id=>1, :title=>"Hanami", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:02:37 UTC}>

book = repository.update(book.id, title: "Hanami Book")
  # => #<Book:0x007f95cb243408 @attributes={:id=>1, :title=>"Hanami Book", :created_at=>2016-11-13 16:02:37 UTC, :updated_at=>2016-11-13 16:03:34 UTC}>

repository.delete(book.id)

repository.find(book.id)
  # => nil

Private Queries

All the queries are private. This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.

Look at the following code:

BookRepository.new.where(author_id: 23).order(:published_at).limit(8)

This is bad for a variety of reasons:

There is a better way:

# lib/bookshelf/repositories/book_repository.rb
class BookRepository < Hanami::Repository
  def most_recent_by_author(author, limit: 8)
    books
      .where(author_id: author.id)
      .order(:published_at)
      .limit(limit)
  end
end

This is a huge improvement, because:

Hanami queries are based on gems from ROM project, namely rom-repository and rom-sql. The gem rom-sql is itself based on Sequel project.

Learn more on how to craft queries with ROM and Sequel.

Timestamps

To have a track of when a record has been created or updated is important when running a project in production.

When creating a new table, if we add the following columns, a repository will take care of keeping the values updated.

Hanami::Model.migration do
  up do
    create_table :books do
      # ...
      column :created_at, DateTime
      column :updated_at, DateTime
    end
  end
end
repository = BookRepository.new

book = repository.create(title: "Hanami")

book.created_at # => 2016-11-14 08:20:44 UTC
book.updated_at # => 2016-11-14 08:20:44 UTC

book = repository.update(book.id, title: "Hanami Book")

book.created_at # => 2016-11-14 08:20:44 UTC
book.updated_at # => 2016-11-14 08:22:40 UTC

When a database table has created_at and updated_at timestamps, a repository will automatically update their values.

Timestamps are on UTC timezone.

Legacy Databases

By default, a repository performs auto-mapping of corresponding database table and creates an automatic schema for the associated entity.

When working with legacy databases we can resolve the naming mismatch between the table name, the columns, with repositories defaults and entities attributes.

Let's say we have a database table like this:

CREATE TABLE t_operator (
    operator_id integer NOT NULL,
    s_name text
);

We can setup our repository with the following code:

# lib/bookshelf/repositories/operator_repository.rb
class OperatorRepository < Hanami::Repository
  self.relation = :t_operator

  mapping do
    attribute :id,   from: :operator_id
    attribute :name, from: :s_name
  end
end

While the entity can stay with the basic setup:

# lib/bookshelf/entities/operator.rb
class Operator < Hanami::Entity
end

The entity now gets the mapping we defined in the repository:

operator = Operator.new(name: "Jane")
operator.name # => "Jane"

The repository can use the same mapped attributes:

operator = OperatorRepository.new.create(name: "Jane")
  # => #<Operator:0x007f8e43cbcea0 @attributes={:id=>1, :name=>"Jane"}>

There are few caveats though (as there always are, with legacy databases):

Note that all attributes need to be mapped or you will get an error when booting up the application that will look like this:

class OperatorRepository < Hanami::Repository
  self.relation = :t_operator

  mapping do
    attribute :id, from: :operator_id
  end
end

  # => Hanami::Model::Error: key not found: :s_name

That happens because the Repository infers the schema for the table and mapping only affects the Hanami::Entity and commands that operate over it (like create and update).

A query, on the other hand, will still need to use the original attribute name:

class OperatorRepository < Hanami::Repository
  self.relation = :t_operator

  mapping do
    attribute :id,   from: :operator_id
    attribute :name, from: :s_name
  end

  def by_name(name)
    root.where(s_name: name)
  end
end

If you want to use the more semantic, aliased names (or with just a subset of attributes) you'll need to define both a schema and a mapping:

class OperatorRepository < Hanami::Repository
  self.relation = :t_operator

  schema do
    attribute :operator_id, Hanami::Model::Sql::Types::Int.meta(primary_key: true, alias: :id)
    attribute :s_name, Hanami::Model::Sql::Types::String.meta(alias: :name)
  end

  mapping do
    attribute :id,   from: :operator_id
    attribute :name, from: :s_name
  end

  def by_name(name)
    t_operator.where(name: name)
  end
end

Then you can use the aliased names on both queries and entities:

operator = OperatorRepository.new.create(name: 'Jane')
  # => #<Operator:0x007f8e43cbcea0 @attributes={:id=>1, :name=>"Jane"}>

OperatorRepository.new.by_name('Jane')
  # => #<Operator:0x007f3e43cdcea1 @attributes={:id=>1, :name=>"Jane"}>

Count

Count is a concept not generally available to all the databases. SQL databases have it, but others don't.

You can define a method, if you're using a SQL database:

class BookRepository < Hanami::Repository
  def count
    books.count
  end
end

Or you can expose specific conditions:

class BookRepository < Hanami::Repository
  # ...

  def on_sale_count
    books.where(on_sale: true).count
  end
end

If you want to use raw SQL you can do:

class BookRepository < Hanami::Repository
  # ...

  def old_books_count
    books.read("SELECT id FROM books WHERE created_at < (NOW() - 1 * interval '1 year')").count
  end
end

Looking for Lotus? We renamed the project and it's now called Hanami. Read the announcement.