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

Setup

  1. $ bundle exec hanami generate model author
  2. create lib/bookshelf/entities/author.rb
  3. create lib/bookshelf/repositories/author_repository.rb
  4. create db/migrations/20171024081558_create_authors.rb
  5. create spec/bookshelf/entities/author_spec.rb
  6. create spec/bookshelf/repositories/author_repository_spec.rb
  7. $ bundle exec hanami generate model book
  8. create lib/bookshelf/entities/book.rb
  9. create lib/bookshelf/repositories/book_repository.rb
  10. create db/migrations/20171024081617_create_books.rb
  11. create spec/bookshelf/entities/book_spec.rb
  12. create spec/bookshelf/repositories/book_repository_spec.rb

Edit the migrations:

  1. # db/migrations/20171024081558_create_authors.rb
  2. Hanami::Model.migration do
  3. change do
  4. create_table :authors do
  5. primary_key :id
  6. column :name, String, null: false
  7. column :created_at, DateTime, null: false
  8. column :updated_at, DateTime, null: false
  9. end
  10. end
  11. end
  1. # db/migrations/20171024081617_create_books.rb
  2. Hanami::Model.migration do
  3. change do
  4. create_table :books do
  5. primary_key :id
  6. foreign_key :author_id, :authors, on_delete: :cascade
  7. column :title, String, null: false
  8. column :on_sale, TrueClass, null: false, default: false
  9. column :created_at, DateTime, null: false
  10. column :updated_at, DateTime, null: false
  11. end
  12. end
  13. end

Now we can prepare the database:

  1. $ bundle exec hanami db prepare

Basic usage

Let’s edit AuthorRepository with the following code:

  1. # lib/bookshelf/repositories/author_repository.rb
  2. class AuthorRepository < Hanami::Repository
  3. associations do
  4. has_many :books
  5. end
  6. def create_with_books(data)
  7. assoc(:books).create(data)
  8. end
  9. def find_with_books(id)
  10. aggregate(:books).where(id: id).as(Author).one
  11. end
  12. 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:

  1. repository = AuthorRepository.new
  2. author = repository.create_with_books(name: "Alexandre Dumas", books: [{title: "The Count of Montecristo"}])
  3. # => #<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}>]}>
  4. author.id
  5. # => 1
  6. author.name
  7. # => "Alexandre Dumas"
  8. author.books
  9. # => [#<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?

  1. author = repository.find(author.id)
  2. # => #<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}>
  3. author.books
  4. # => 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):

  1. author = repository.find_with_books(author.id)
  2. # => #<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}>]}>
  3. author.books
  4. # => [#<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.

Add and Remove

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

  1. # lib/bookshelf/repositories/author_repository.rb
  2. class AuthorRepository < Hanami::Repository
  3. # ...
  4. def add_book(author, data)
  5. assoc(:books, author).add(data)
  6. end
  7. def remove_book(author, id)
  8. assoc(:books, author).remove(id)
  9. end
  10. end

Let’s add a book:

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

And remove it:

  1. repository.remove_book(author, book.id)

Querying

An association can be queried:

  1. # lib/bookshelf/repositories/author_repository.rb
  2. class AuthorRepository < Hanami::Repository
  3. # ...
  4. def books_count(author)
  5. assoc(:books, author).count
  6. end
  7. def on_sales_books_count(author)
  8. assoc(:books, author).where(on_sale: true).count
  9. end
  10. def book_exists?(author, id)
  11. book_for(author, id).exists?
  12. end
  13. private
  14. def book_for(author, id)
  15. assoc(:books, author).where(id: id)
  16. end
  17. end

You can also run operations on top of these scopes:

  1. # lib/bookshelf/repositories/author_repository.rb
  2. class AuthorRepository < Hanami::Repository
  3. # ...
  4. def delete_on_sales_books(author)
  5. assoc(:books, author).where(on_sale: true).delete
  6. end
  7. end