V1.3: Has One


Also known as one-to-one, is an association between an entity (User) associated to one child entity (Avatar).

Setup

$ bundle exec hanami generate model user
      create  lib/bookshelf/entities/user.rb
      create  lib/bookshelf/repositories/user_repository.rb
      create  db/migrations/20171024083639_create_users.rb
      create  spec/bookshelf/entities/user_spec.rb
      create  spec/bookshelf/repositories/user_repository_spec.rb

$ bundle exec hanami generate model avatar
      create  lib/bookshelf/entities/avatar.rb
      create  lib/bookshelf/repositories/avatar_repository.rb
      create  db/migrations/20171024083725_create_avatars.rb
      create  spec/bookshelf/entities/avatar_spec.rb
      create  spec/bookshelf/repositories/avatar_repository_spec.rb

Edit the migrations:

# db/migrations/20171024083639_create_users.rb
Hanami::Model.migration do
  change do
    create_table :users do
      primary_key :id

      column :name, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end
# db/migrations/20171024083725_create_avatars.rb
Hanami::Model.migration do
  change do
    create_table :avatars do
      primary_key :id

      foreign_key :user_id, :users, null: false, on_delete: :cascade

      column :url, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

Now we can prepare the database:

$ bundle exec hanami db prepare

Basic usage

Let’s edit UserRepository with the following code:

# lib/bookshelf/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  associations do
    has_one :avatar
  end

  def create_with_avatar(data)
    assoc(:avatar).create(data)
  end

  def find_with_avatar(id)
    aggregate(:avatar).where(id: id).map_to(User).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 UserRepository with dozen of unneeded methods.

Let’s create an user with an avatar single database operation:

repository = UserRepository.new

user = repository.create_with_avatar(name: "Luca", avatar: { url: "https://avatars.test/luca.png" })
  # => #<User:0x00007fa166ac8550 @attributes={:id=>1, :name=>"Luca", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC, :avatar=>#<Avatar:0x00007fa166ac35c8 @attributes={:id=>1, :user_id=>1, :url=>"https://avatars.test/luca.png", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC}>}>

user.id
  # => 1

user.name
  # => "Luca"

user.avatar
  # => #<Avatar:0x00007fa166ac35c8 @attributes={:id=>1, :user_id=>1, :url=>"https://avatars.test/luca.png", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC}>

What happens if we load the user with UserRepository#find?

user = repository.find(user.id)
  # => #<User:0x00007fa166aa3a70 @attributes={:id=>1, :name=>"Luca", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC}>
user.avatar
  # => nil

Because we haven’t explicitly loaded the associated record, user.avatar is nil. We can use the method that we have defined on before (#find_with_avatar):

user = repository.find_with_avatar(user.id)
  # => #<User:0x00007fa166a71048 @attributes={:id=>1, :name=>"Luca", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC, :avatar=>#<Avatar:0x00007fa166a70328 @attributes={:id=>1, :user_id=>1, :url=>"https://avatars.test/luca.png", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC}>}>

user.avatar
  # => #<Avatar:0x00007fa166a70328 @attributes={:id=>1, :user_id=>1, :url=>"https://avatars.test/luca.png", :created_at=>2017-10-24 08:44:27 UTC, :updated_at=>2017-10-24 08:44:27 UTC}>

This time user.avatar has the associated avatar.

Add, Remove, Update, and Replace

You can perform operations to add, remove, update, and replace the avatar:

# lib/bookshelf/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  # ...

  def add_avatar(user, data)
    assoc(:avatar, user).add(data)
  end

  def remove_avatar(user)
    assoc(:avatar, user).delete
  end

  def update_avatar(user, data)
    assoc(:avatar, user).update(data)
  end

  def replace_avatar(user, data)
    assoc(:avatar, user).replace(data)
  end
end