Associations

An association is a logical relationship between two entities.

As of the current version, Hanami supports associations as an experimental feature only for the SQL adapter.

Design

Because the association is made of data linked together in a database, we define associations in repositories.

Explicit Interface

When we declare an association, that repository does NOT get any extra method to its public interface. This because Hanami wants to prevent to bloat in repositories by adding methods that are often never used.

When we define an association, the repository doesn't get any extra public methods.

If we need to create an author, contextually with a few books, we need to explicitly define a method to perform that operation.

Explicit Loading

The same principle applies to read operations: if we want to eager load an author with the associated books, we need an explicit method to do so.

If we don't explicitly load that books, then the resulting data will be nil.

No Proxy Loader

Please remember that operations on associations are made via explicit repository methods. Hanami does NOT support by design, the following use cases:

Please remember that author.books is just an array, its mutation won't be reflected in the database.

Types Of Associations

Has Many

Also known as one-to-many, is an association between a single entity (Author) and a collection of many other linked entities (Book).

% bundle exec hanami generate migration create_authors
      create  db/migrations/20161115083440_create_authors.rb
# db/migrations/20161115083440_create_authors.rb
Hanami::Model.migration do
  change do
    create_table :authors do
      primary_key :id

      column :name,       String,   null: false
      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end
% bundle exec hanami generate migration create_books
      create  db/migrations/20161115083644_create_books.rb
# db/migrations/20161115083644_create_books.rb
Hanami::Model.migration do
  change do
    create_table :books do
      primary_key :id
      foreign_key :author_id, :authors, on_delete: :cascade, null: false

      column :title,      String,   null: false
      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end
% bundle exec hanami db prepare
% bundle exec hanami generate model author
      create  lib/bookshelf/entities/author.rb
      create  lib/bookshelf/repositories/author_repository.rb
      create  spec/bookshelf/entities/author_spec.rb
      create  spec/bookshelf/repositories/author_repository_spec.rb

% 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

Let's edit AuthorRepository with the following code:

# lib/bookshelf/repositories/author_repository.rb
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(id: id).as(Author).one
  end
end

We have defined explicit methods only for the operations that we need for our model domain. In this way, we avoid to bloat AuthorRepository with dozen of unneeded methods.

Let's create an author with a collection of books with a single database operation:

repository = AuthorRepository.new

author = repository.create_with_books(name: "Alexandre Dumas", books: [{title: "The Count of Montecristo"}])
  # => #<Author:0x007f811c415420 @attributes={:id=>1, :name=>"Alexandre Dumas", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC, :books=>[#<Book:0x007f811c40fe08 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Montecristo", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC}>]}>

author.id
  # => 1
author.name
  # => "Alexandre Dumas"
author.books
  # => [#<Book:0x007f811c40fe08 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Montecristo", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC}>]

What happens if we load the author with AuthorRepository#find?

author = repository.find(author.id)
  # => #<Author:0x007f811b6237e0 @attributes={:id=>1, :name=>"Alexandre Dumas", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC}>
author.books
  # => nil

Because we haven't explicitly loaded the associated records, author.books is nil. We can use the method that we have defined on before (#find_with_books):

author = repository.find_with_books(author.id)
  # => #<Author:0x007f811bbeb6f0 @attributes={:id=>1, :name=>"Alexandre Dumas", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC, :books=>[#<Book:0x007f811bbea430 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Montecristo", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC}>]}>

author.books
  # => [#<Book:0x007f811bbea430 @attributes={:id=>1, :author_id=>1, :title=>"The Count of Montecristo", :created_at=>2016-11-15 09:19:38 UTC, :updated_at=>2016-11-15 09:19:38 UTC}>]

This time author.books has the collection of associated books.


What if we need to add or remove books from an author? We need to define new methods to do so.

# lib/bookshelf/repositories/author_repository.rb
class AuthorRepository < Hanami::Repository
  # ...

  def add_book(author, data)
    assoc(:books, author).add(data)
  end
end

Let's add a book:

book = repository.add_book(author, title: "The Three Musketeers")

And remove it:

BookRepository.new.delete(book.id)

Prev: Models - PostgreSQL
Next: Models - Use Your Own ORM
Looking for Lotus? We renamed the project and it's now called Hanami. Read the announcement.