We rewrote hanami-model from scratch with an engine based on ROM.

The result is impressive: it's faster and more robust.

Features

Database Automapping

But there is more, all the mapping boilerplate is gone, now using entities and repositories is much easier.

% bundle exec hanami generate model book
      create  lib/bookshelf/entities/book.rb
      create  lib/bookshelf/repositories/book_repository.rb
      create  spec/bookshelf/entities/book_spec.rb
      create  spec/bookshelf/repositories/book_repository_spec.rb

This is the generated entity:

class Book < Hanami::Entity
end

While this is the generated repository:

class BookRepository < Hanami::Repository
end

Then we generate the migration:

% bundle exec hanami generate migration create_books
      create  db/migrations/20161113154510_create_books.rb

Let's edit the migration with the following code:

Hanami::Model.migration do
  change do
    create_table :books do
      primary_key :id
      column :title,      String
      column :created_at, DateTime
      column :updated_at, DateTime
    end
  end
end

Now we need to prepare the database to use it:

% bundle exec hanami db prepare

We're ready to use our repository:

% bundle exec hanami console
irb(main):001:0> book = BookRepository.new.create(title: "Hanami")
=> #<Book:0x007f95ccefb320 @attributes={:id=>1, :title=>"Hanami", :created_at=>2016-11-13 15:49:14 UTC, :updated_at=>2016-11-13 15:49:14 UTC}>

Learn more about new repositories, entities usage.

Entities Data Integrity

When using a SQL database, an entity setups an internal schema, which is derived automatically from the table definition.

Imagine to have the books table defined as:

CREATE TABLE books (
    id integer NOT NULL,
    title text,
    created_at timestamp without time zone,
    updated_at timestamp without time zone
);

This is the corresponding entity Book.

# lib/bookshelf/entities/book.rb
class Book < Hanami::Entity
end

Let's instantiate it with proper values:

book = Book.new(title: "Hanami")

book.title      # => "Hanami"
book.created_at # => nil

The created_at attribute is nil because it wasn't present when we have instantiated book.


It ignores unknown attributes:

book = Book.new(unknown: "value")

book.unknown # => NoMethodError
book.foo     # => NoMethodError

It raises a NoMethodError both for unknown and foo, because they aren't part of the internal schema.


It can coerce values:

book = Book.new(created_at: "Sun, 13 Nov 2016 09:41:09 GMT")

book.created_at       # => 2016-11-13 09:41:09 UTC
book.created_at.class # => Time

An entity tries as much as it cans to coerce values according to the internal schema.


It enforces data integrity via exceptions:

Book.new(created_at: "foo") # => ArgumentError

If we use this feature, in combination with database constraints and validations, we can ensure a strong degree of data integrity for our projects.

Learn more about Entities custom schema.

Associations (experimental)

Hanami finally ships with associations. We postponed this feature for long time mainly because we didn't want to replicate ActiveRecord API. That is a complex beast that took years to get stable. Its proxy loader design (post.comments to load comments) doesn't fit Hanami vision of explicitness.

To design an API that is both explicit and reduces the boilerplate is a hard challenge. But today we can share with you a preview of this experimental feature.

class AuthorRepository < Hanami::Repository
  associations do
    has_many :books
  end

  def create_with_books(data)
    assoc(:books).create(data)
  end

  def find_with_books(id)
    aggregate(:books).where(authors__id: id).as(Author).one
  end
end

We can create with a single SQL command the parent and the children:

repository = AuthorRepository.new

author = repository.create_with_books(name: "Alexander Dumas", books: [{title: "The Count of Monte Cristo" }])
  # => #<Author:0x007f8a08130968 @attributes={:id=>1, :name=>"Alexander Dumas", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC, :books=>[#<Book:0x007f8a0812b260 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Monte Cristo", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC}>]}>

The default find, doesn't preload the associated records:

found = repository.find(author.id)
  # => #<Author:0x007f8a07971ce0 @attributes={:id=>1, :name=>"Alexander Dumas", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC}>

found.books
  # => nil

It requires an explicit preload operation:

found = repository.find_with_books(author.id)
  # => #<Author:0x007f8a040a6cf8 @attributes={:id=>1, :name=>"Alexander Dumas", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC, :books=>[#<Book:0x007f8a040a5970 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Monte Cristo", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC}>]}>

found.books
  # => [#<Book:0x007f8a040a5970 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Monte Cristo", :created_at=>2016-11-14 20:52:24 UTC, :updated_at=>2016-11-14 20:52:24 UTC}>]

Learn more about associations.

PostgreSQL Types

hanami-model now supports natively most common PostgreSQL data types such as: UUID, Array, JSON(B)

Hanami::Model.migration do
  up do
    execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'

    create_table :project_files do
      column :id, 'uuid', null: false, default: Hanami::Model::Sql.function(:uuid_generate_v4)
      column :name, String
    end
  end

  down do
    drop_table :project_files
    execute 'DROP EXTENSION IF EXISTS "uuid-ossp"'
  end
end
ProjectFileRepository.new.create(name: "source.rb")
  # => #<ProjectFile:0x007ff29c4b9740 @attributes={:id=>"239f8e0f-d764-4a76-aaa7-7b59b5301c72", :name=>"source.rb"}>

Learn more about PostgreSQL data types

Misc

  • Hanami is only compatible with Ruby (MRI) 2.3+

  • There are several breaking changes, please check the upgrade notes

Upgrade Notes

Please have a look at the upgrade notes for v0.9.0.

Acknowledgements

A special thanks goes to the ROM team for their support during the hard work of integration between the two frameworks. Thank you Piotr Solnica, Nikita Shilnikov, Andy Holland, and Tim Riley for your help.

Contributors

We're grateful for each person who contributed to this release. These lovely people are:

  • Alfonso Uceda
  • Anton Davydov
  • Bruz Marzolf
  • Grachev Mikhail
  • Ivan Lasorsa
  • Jakub Pavlík
  • James Hamilton
  • Kyle Chong
  • Lucas Allan
  • Marion Duprey
  • Maxim Dzhuliy
  • Pascal Betz
  • Russell Cloak
  • Sean Collins
  • Tomas Craig
  • Trung Lê

Thank you all!