Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing many-to-many (and other) relationships with Hanami

Tags:

hanami

I searched the docs for how to implement relationships among entities (eg, one-to-many, many-to-many, etc), but didn't find any examples.

So I tried a reasonable guess. Here's my attempt at implementing a Person who can be tagged with Tags:

require 'moocho_query'
require 'hanami/model'
require 'hanami/model/adapters/file_system_adapter'

class Person
  include Hanami::Entity
  attributes :name, :age, :tags
end

class Tag
  include Hanami::Entity
  attributes :name
end

class PersonRepository
  include Hanami::Repository
end

class TagRepository
  include Hanami::Repository
end

Hanami::Model.configure do

  adapter type: :file_system, uri: './file_db'

  mapping do

    collection :people do
      entity Person
      repository PersonRepository

      attribute :id, Integer
      attribute :name, String
      attribute :age,  Integer
      attribute :tags, type: Array[Tag]
    end

    collection :tags do
      entity Tag
      repository TagRepository

      attribute :id, Integer
      attribute :name, String
    end

  end

end.load!


me = Person.new(name: 'Jonah', age: 99)
t1 = Tag.new(name: 'programmer')
t2 = Tag.new(name: 'nice')
me.tags = [t1, t2]
PersonRepository.create(me)

This fails on the load! call, with the following error:

/Users/x/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/hanami-utils-0.7.0/lib/hanami/utils/class.rb:90:in `load_from_pattern!': uninitialized constant (Hanami::Model::Mapping::Coercers::{:type=>[Tag]}|
{:type=>[Tag]}) (NameError)
        from /Users/jg/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/hanami-model-0.6.0/lib/hanami/model/mapping/attribute.rb:80:in `coercer'
        from /Users/jg/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/hanami-model-0.6.0/lib/hanami/model/mapping/attribute.rb:53:in `load_coercer'

What is the correct way to implement relationships in Hanami?

like image 613
Jonah Avatar asked Jan 30 '16 16:01

Jonah


2 Answers

As of version 0.7.0, there is no way to implement relationships between the entities. This is why there is no how-to in the documentation as well.

Out of curiousity, I had inquired this using a tweet which can be taken as an official word on entity relationships.

As a work around, in Hanami, entities are simply objects that you are persisting to the database which means an entity's persistence details can vary from it's schema.

I would suggest having a tags method on the Person object. Inside this method, you can retrieve the person's tags. Something like this:

def self.tags
  TagRepository.query do
    where(id: [tag-id-1, tag-id-2, ... , tag-id-n])
  end.all
end

Though you will need to persist the tag ids associated with the person to the database as an attribute of the person or using a join table.

Do know that this implementation will have the n+1 query problem.

like image 130
Noman Ur Rehman Avatar answered Sep 28 '22 16:09

Noman Ur Rehman


I know it's an old question, but I'll leave this answer in case somebody stumbles here:
Hanami added many-to-many support on version 1.1 http://hanamirb.org/guides/1.1/associations/has-many-through/

Basic 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 story
create  lib/bookshelf/entities/story.rb
create  lib/bookshelf/repositories/story_repository.rb
create  db/migrations/20171024085712_create_stories.rb
create  spec/bookshelf/entities/story_spec.rb
create  spec/bookshelf/repositories/story_repository_spec.rb

% bundle exec hanami generate model comment
create  lib/bookshelf/entities/comment.rb
create  lib/bookshelf/repositories/comment_repository.rb
create  db/migrations/20171024085858_create_comments.rb
create  spec/bookshelf/entities/comment_spec.rb
create  spec/bookshelf/repositories/comment_repository_spec.rb

Migrations

Users table:

# 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

Stories table:

# db/migrations/20171024085712_create_stories.rb
Hanami::Model.migration do
  change do
    create_table :stories do
      primary_key :id

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

      column :text, String, null: false

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

Comments table:

# db/migrations/20171024085858_create_comments.rb
Hanami::Model.migration do
  change do
    create_table :comments do
      primary_key :id

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

      column :text, String, null: false

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

Repositories

User repository:

# lib/bookshelf/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  associations do
    has_many :stories
    has_many :comments
  end
end

Story repository:

# lib/bookshelf/repositories/story_repository.rb
class StoryRepository < Hanami::Repository
  associations do
    belongs_to :user
    has_many :comments
    has_many :users, through: :comments
  end

  def find_with_comments(id)
    aggregate(:user, comments: :user).where(id: id).map_to(Story).one
  end

  def find_with_commenters(id)
    aggregate(:users).where(id: id).map_to(Story).one
  end
end

Comment repository:

# lib/bookshelf/repositories/comment_repository.rb
class CommentRepository < Hanami::Repository
  associations do
    belongs_to :story
    belongs_to :user
  end
end

Usage

user_repo = UserRepository.new
author = user_repo.create(name: "Luca")
# => #<User:0x00007ffe71bc3b18 @attributes={:id=>1, :name=>"Luca", :created_at=>2017-10-24 09:06:57 UTC, :updated_at=>2017-10-24 09:06:57 UTC}>

commenter = user_repo.create(name: "Maria G")
# => #<User:0x00007ffe71bb3010 @attributes={:id=>2, :name=>"Maria G", :created_at=>2017-10-24 09:07:16 UTC, :updated_at=>2017-10-24 09:07:16 UTC}>

story_repo = StoryRepository.new

story_repo.create(user_id: author.id, text: "Hello, folks")
# => #<Story:0x00007ffe71b4ace0 @attributes={:id=>1, :user_id=>1, :text=>"Hello folks", :created_at=>2017-10-24 09:09:59 UTC, :updated_at=>2017-10-24 09:09:59 UTC}>

story = story_repo.find_with_comments(story.id)
# => #<Story:0x00007fd45e327e60 @attributes={:id=>2, :user_id=>1, :text=>"Hello folks", :created_at=>2017-10-24 09:09:59 UTC, :updated_at=>2017-10-24 09:09:59 UTC, :user=>#<User:0x00007fd45e326bc8 @attributes={:id=>1, :name=>"Luca", :created_at=>2017-10-24 09:06:57 UTC, :updated_at=>2017-10-24 09:06:57 UTC}>, :comments=>[#<Comment:0x00007fd45e325930 @attributes={:id=>1, :user_id=>2, :story_id=>2, :text=>"Hi and welcome!", :created_at=>2017-10-24 09:12:30 UTC, :updated_at=>2017-10-24 09:12:30 UTC, :user=>#<User:0x00007fd45e324490 @attributes={:id=>2, :name=>"Maria G", :created_at=>2017-10-24 09:07:16 UTC, :updated_at=>2017-10-24 09:07:16 UTC}>}>]}>

story.comments
# => [#<Comment:0x00007fe289f2d618 @attributes={:id=>1, :user_id=>2, :story_id=>2, :text=>"Hi and welcome!", :created_at=>2017-10-24 09:12:30 UTC, :updated_at=>2017-10-24 09:12:30 UTC, :commenter=>#<User:0x00007fe289f2c420 @attributes={:id=>2, :name=>"Maria G", :created_at=>2017-10-24 09:07:16 UTC, :updated_at=>2017-10-24 09:07:16 UTC}>}>]
like image 45
Rodrigo Vasconcelos Avatar answered Sep 28 '22 14:09

Rodrigo Vasconcelos