Using ROM 5.3.0 Repository with Hanami 2.0

Hello,

I am trying to build a web-app prototype with Hanami 2.0 and as part of that I am trying to utilize ROM Repositories. Below shown is the relevant code snippet for a question I have:

# startup_template/slices/admin/repository.rb

# auto_register: false
# frozen_string_literal: true

require 'rom-repository'

module Admin
  class Repository < ROM::Repository
  end
end

# startup_template/slices/admin/repositories/doctor_repository.rb
module Admin
  module Repositories
    class DoctorRepository < ::Admin::Repository
      commands :create, update: :by_pk, delete: :by_pk

    end
  end
end

I was trying to use that repository from hanami console but I encountered an error. Please see below the console output.

startup_template$ HANAMI_ENV=test bundle exec hanami console
startup_template[test]> Admin::Repositories::DoctorRepository.new
KeyError: key not found: :container
from /home/jignesh/.rvm/gems/ruby-3.1.2@startup-template-hanami-2-0/gems/rom-repository-5.3.0/lib/rom/repository/class_interface.rb:53:in 'fetch'
startup_template[test]>

Checking the source code at rom/class_interface.rb at v5.3.0 · rom-rb/rom · GitHub
the error is being raised by options.fetch(:container) and the solution is to pass ROM container to Admin::Repositories::DoctorRepository.new which works as can be seen in console output shown below:

startup_template[test]> Hanami.app.start(:persistence)
=> StartupTemplate::Container
startup_template[test]> rom_container = container['persistence.rom']
startup_template[test]> Admin::Repositories::DoctorRepository.new(rom_container)
=> #<Admin::Repositories::DoctorRepository struct_namespace=ROM::Struct auto_struct=true>
startup_template[test]>

But I was trying to override the class method new in my base class Admin::Repository so as to do something like following

module Admin
  class Repository < ROM::Repository

    def self.new
      rom_container = Hanami.app['persistence.rom']
      super(rom_container)
    end
  end
end

but the overridden new method doesn’t get invoked and I always end up seeing the following error

KeyError: key not found: :container
from /home/jignesh/.rvm/gems/ruby-3.1.2@startup-template-hanami-2-0/gems/rom-repository-5.3.0/lib/rom/repository/class_interface.rb:53:in 'fetch'

implying the new method defined at rom/class_interface.rb at v5.3.0 · rom-rb/rom · GitHub
is getting invoked.

I tried simulating the class and module structure in a standalone ruby script like

module ROM
  class Repository
    module ClassInterface
      def new(opts={})
        opt_1 = opts.fetch(:opt_1)
        puts ">>>>>>> ROM::Repository::ClassInterface new"
      end
    end
  end
end


module ROM
  class Repository
    extend ClassInterface

  end
end

module Admin
  class Repository < ROM::Repository
    def self.new
      puts ">>>>> Admin::Repository new"
      super
    end
  end
end

module Admin
  module Repositories
    class DoctorRepository < ::Admin::Repository

    end
  end
end

Admin::Repositories::DoctorRepository.new

and running that script the overridden version of new method does get invoked as can be seen in the output below

jignesh@j-home-pc:~/Desktop$ ruby test-3.rb
>>>>> Admin::Repository new
test-3.rb:5:in `fetch': key not found: :opt_1 (KeyError)
	from test-3.rb:5:in `new'
	from test-3.rb:25:in `new'
	from test-3.rb:39:in `<main>'

So is there something special going on with the inheritance in my Hanami-based classes I shown above which is preventing the overridden new method in Admin::Repository from getting invoked or the behaviour is being caused in the manner ROM Repository related classes are defined?

Thanks.

Welcome! What you are trying to accomplish here is intended to be done with Dependency Injection. See this Hanami guide for more information

Here’s the short answer:

module Admin
  class Repository < ROM::Repository
    include Deps[container: "persistence.rom"]
  end
end

Hi @alassek,

Thanks for your response. Regarding your suggestion of including Deps in Admin::Repository initially I did that only but I encountered the error I shared about in my 1st post i.e. following:

startup_template[test]> Admin::Repositories::DoctorRepository.new
KeyError: key not found: :container
from /home/jignesh/.rvm/gems/ruby-3.1.2@startup-template-hanami-2-0/gems/rom-repository-5.3.0/lib/rom/repository/class_interface.rb:53:in `fetch'
startup_template[test]> 

And to resolve which I attempted an alternate approach of overriding ROM::Repository.new method like following

module Admin
  class Repository < ROM::Repository

    def self.new
      rom_container = Hanami.app['persistence.rom']
      super(rom_container)
    end
  end
end

but as shared that overridden new method doesn’t get invoked and despite that overridden version in place I always end up seeing that same error KeyError: key not found: :container.

As per documentation at V2.0: Container and components | Hanami Guides for Deps it says following:

In the above example, the Deps mixin takes each given key and makes the relevant component from the app container available within the current component via an instance method.

So if we define include Deps[container: "persistence.rom"] in Admin::Repository then it should make available an instance method container under Admin::Repository and its sub-classes but to initialize a Repository class it needs to be instantiated like following as documented at ROM - Quick Start

user_repo = UserRepo.new(rom)

where rom passed to UserRepo.new should be an instance returned by ROM.container.

Now what I want to do is avoid manually passing that container instance to each of my concrete repository classes (like Admin::Repositories::DoctorRepository) that should extend from parent class Admin::Repository.

In other words if I do Admin::Repositories::DoctorRepository.new then it should automatically pick the container made available in parent class Admin::Repository. But using Deps it is not working because Admin::Repository extends from ROM::Repository and ROM::Repository has its class method new defined in ROM::Repostiory::ClassInterface which accepts a ROM container as its first argument or through an option named :container.

So to instantiate a ROM::Repository sub-class I need to pass the ROM container to its new method but I want to avoid that in repositories in my Hanami 2.0 app and to achieve that I tried to override the class method new in my custom parent class Admin::Repository but as informed for some reason the overridden version is not getting invoked.

I hope this time I have conveyed with more clarity on what I am trying to achieve and where I am stuck.

Thanks.

Maybe a little off-topic, but I can share my setup with Hanami 2 and ROM 5.3 that works. It seems to be a little different than yours:

Basically I have this class:

# lib/my_app/repository.rb

require "rom-repository"

module MyApp
  class Repository < ROM::Repository::Root
    include Deps[container: "persistence.rom"]
  end
end

… and every repository in slices inherits from this one, so for example:

class Account::Repositories::Account < MyApp::Repository[:accounts]
  commands :create
  # [...]
end

With that setup I don’t even need to boot persistence manually - it just boots when I first reference the repository. Hope this helps.

Apologies for the confusion here, I don’t think the Repository base class will work with injection, at least I can’t get it to do so. As I and @katafrakt have done, it looks like ROM::Repository::Root is the desirable base class here.

So try this:

module Admin
  class Repository < ROM::Repository::Root
    include Deps[container: "persistence.rom"]
  end

  module Repositories
    class DoctorRepository < Repository[:doctors]
      commands :create, update: :by_pk, delete: :by_pk
    end
  end
end

The [:doctors] part refers to a registered relation name in ROM, so you should see it as Hanami.app["persistence.rom"].relations[:doctors]

Here’s a working test script:

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'sqlite3'
  gem 'rom-sql'
  gem 'dry-core'
  gem 'dry-auto_inject'
end

require 'rom'
require 'rom/sql'

rom_config = ROM::Configuration.new(:sql, 'sqlite::memory')
gateway    = rom_config.default_gateway

migration = gateway.migration do
  change do
    create_table :users do
      primary_key :id
      column :name, String, null: false
      column :email, String, null: false
    end
  end
end

migration.apply(gateway.connection, :up)

class Users < ROM::Relation[:sql]
  schema(infer: true)
end

rom_config.register_relation(Users)

container = Dry::Core::Container.new
container.register("persistence.rom", ROM.container(rom_config))

Deps = Dry::AutoInject(container)

class Repository < ROM::Repository::Root
  include Deps[container: "persistence.rom"]
  def find(id) = root.by_pk(id).one
end

class UserRepo < Repository[:users]
  commands :create, update: :by_pk, delete: :by_pk
end

repo = UserRepo.new
repo.create(name: "Joe Example", email: "joe@example.com")

puts repo.find(1).inspect
# => #<ROM::Struct::User id=1 name="Joe Example" email="joe@example.com">
1 Like

Thanks @alassek and @katafrakt for your help on this. So here is an update. Based on your suggestions I modified my slices/admin/repository.rb to look like following:

require 'rom-repository'

module Admin
  class Repository < ROM::Repository::Root
    include Deps[container: "persistence.rom"]

  end
end

Then tried accessing repository from console but encountered error Dry::Core::Container::KeyError: key not found: "persistence.rom" (see below)

startup_template$ HANAMI_ENV=test bundle exec hanami c

startup_template[test]> dr = Admin::Repositories::DoctorRepository.new
Dry::Core::Container::KeyError: key not found: "persistence.rom"
from /home/jignesh/.rvm/gems/ruby-3.1.2@startup-template-hanami-2-0/gems/dry-core-1.0.0/lib/dry/core/container/resolver.rb:32:in `block in call'

startup_template[test]> 

Now it is interesting that why persistence.rom is unavailable despite the fact I have my persistence provider defined (see below)

config/providers/persistence.rb

# frozen_string_literal: true

Hanami.app.register_provider :persistence, namespace: true do
  prepare do
    require 'rom'

    config = ROM::Configuration.new(:sql, target["settings"].database_url)

    register "config", config
    register "db", config.gateways[:default].connection
  end

  start do
    config = target["persistence.config"]

    config.auto_registration(
      target.root.join('lib/startup_template/persistence'),
      namespace: 'StartupTemplate::Persistence'
    )

    register "rom", ROM.container(config)
  end
end

One possibility can be that it is not prepared by default and indeed that is the case checking the app container keys (see below)

startup_template[test]> Hanami.app.keys
=> ["settings", "notifications"]
startup_template[test]> 

So I prepared and started the persistence provider manually like below

startup_template[test]> Hanami.app.prepare(:persistence)
=> StartupTemplate::App

startup_template[test]> Hanami.app.keys
=> ["settings", "notifications", "persistence.config", "persistence.db"]

startup_template[test]> Hanami.app.start(:persistence)
=> StartupTemplate::Container

startup_template[test]> Hanami.app.keys
=> ["settings", "notifications", "persistence.config", "persistence.db", "persistence.rom"]
startup_template[test]> 

Now that the provider is available in the container I again tried instantiating the repository but again the same error

startup_template[test]> dr = Admin::Repositories::DoctorRepository.new
Dry::Core::Container::KeyError: key not found: "persistence.rom"
from /home/jignesh/.rvm/gems/ruby-3.1.2@startup-template-hanami-2-0/gems/dry-core-1.0.0/lib/dry/core/container/resolver.rb:32:in `block in call'

startup_template[test]> 

Then it clicked to me that my repository is under slices folder which as per the documentation at V2.0: Slices | Hanami Guides says that a slice can have its own container. So I checked the key’s in slice’s container like below:

startup_template[test]> Admin::Slice.keys
=> []
startup_template[test]> 

So Admin slice container doesn’t contain any keys. So I booted it as shown below and then again checked its keys (ref: V2.0: Slices | Hanami Guides) (see below)

startup_template[test]> Admin::Slice.boot
=> Admin::Slice
startup_template[test]> Admin::Slice.keys
=> ["repositories.doctor_repository", "inflector", "logger", "notifications", "rack.monitor", "routes", "settings"]
startup_template[test]> 

So as can be seen app container’s persistence.rom is still not available in the slice container and this can be the reason for the error when trying to instantiate the repository defined in the slice. So following the documentation at V2.0: Slices | Hanami Guides I modified my default generated config/app.rb by adding following

config.shared_app_component_keys += [ "persistence.rom" ]`

after which the file content looks like:

# frozen_string_literal: true

require "hanami"

module StartupTemplate
  class App < Hanami::App
    config.shared_app_component_keys += [ "persistence.rom" ]
  end
end

Then I restarted Hanami console and tried instantiating the repository and it worked!

startup_template$ HANAMI_ENV=test bundle exec hanami c
startup_template[test]> dr = Admin::Repositories::DoctorRepository.new
=> #<Admin::Repositories::DoctorRepository struct_namespace=ROM::Struct auto_struct=true>
startup_template[test]> 

So the take away here is that the Hanami 2.0 guide illustrates features by putting all code under app and lib folder and for the code under those folders app container’s all components should be available without any additional configuration but code added under slices folder if that needs to access app containers components then they should be made available through config.shared_app_component_keys in config/app.rb.

Thanks again for the help on getting this resolved.

@alassek Regarding following in Using ROM 5.3.0 Repository with Hanami 2.0 - #5 by alassek

I don’t think the Repository base class will work with injection, at least I can’t get it to do so. As I and @katafrakt have done, it looks like ROM::Repository::Root is the desirable base class here.

I was curious to know why it had to be that way so I explored relevant source code and following are my findings

TL;DR:

When using ROM::Repository::Root as the base class for repositories in a Hanami 2.0 app, the container that is made available through Hanami Deps
that is passed as an option, against key container, to ROM::Repository.new method.

Details

Let’s say we have a base repository class like below in a Hanami 2.0 app:

require 'rom-repository'

module Admin
  class Repository < ROM::Repository::Root
    include Deps[container: "persistence.rom"]

  end
end

Note: The code references below are the from the most recent commit, at the time of writing this, in tree Commits · rom-rb/rom · GitHub

ROM::Repository::Root extends ROM::Repository class.

ROM::Repository defines following option (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository.rb#L90)

option :container, allow: ROM::Container

That option is a class method made available by ROM::Initializer (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/core/lib/rom/initializer.rb#L17)
It defines a with method (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/core/lib/rom/initializer.rb#L37).

That with method when gets invoked with new_options passed, following should execute (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/core/lib/rom/initializer.rb#L41)

self.class.new(#{seq_names}**options, **new_options)

The options method used is an instance method and it is defined at https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/core/lib/rom/initializer.rb#L62.
That should return a Hash which should contain following key-value pair container: <container object injected through Hanami Deps>. So at runtime
following kind of code should execute

self.class.new(#{seq_names}, { container: container })

Now since ROM::Initializer is included using extends in class ROM::Repository so when above shown self.class.new should be invoked it should invoke ROM::Repository’s
new class method which is defined in ROM::ClassInterface (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository/class_interface.rb#L52)
which is also included using extends in class ROM::Repository (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository.rb#L58).

So when ROM::Repository::Root[:<relation_name>].new executes it should end up invoking ROM::ClassInterface’s new method with following args nil, { container: container }.

So when we do Admin::Repository[:doctors].new the final invocation can be something like ROM::Repository::Root[:doctors].new(nil, { container: container }).

Then the super(**options, container: container) at https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository/class_interface.rb#L60 should I guess invoke the new method of root ancestor in ancestor chain which I guess should be Class.new which inturn should finally invoke ROM::Repository::Root#initialize
(see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository/root.rb#L59-L62). The super there should invoke ROM::Repository#initialize (see https://github.com/rom-rb/rom/blob/e230c12283b6f3701827aaea64da951713113701/repository/lib/rom/repository.rb#L107) and the super
there should inturn invoke any other initialize method that should be available in the ancestors chain of ROM::Repository.

Note: I may be wrong in interpreting any particular chain in runtime invocation chain described above but I have gut feeling that I am right on the overall workflow involved. So please feel to correct me on any of my findings above.

So in my understanding that is the workflow regarding how ROM::Repository::Root based subclasses gets instantiated without explicitly needing passing ROM container to the new method
like illustrated at ROM - Quick Start.

But when ROM::Repository based subclasses are to be instantiated ROM container must be explicitly passed to the new method.

Finally one thing still remains unanswered for me is that when I defined my base repository extending ROM::Repository why my overridden new method (see below; ref: Using ROM 5.3.0 Repository with Hanami 2.0) didn’t got invoked?

module Admin
  class Repository < ROM::Repository

    def self.new
      rom_container = Hanami.app['persistence.rom']
      super(rom_container)
    end
  end
end

There must be “something” which prevented that from happening but that “something” I am unable to figure out. So anybody ending up on this post finds himself/herself capable to solve that mystery for me I would really be grateful to him/her.

Thanks.

I’m glad to see you got this working! Reading back on your posts, I realize you did specify that this was happening in a slice, which I missed originally.

Obviously, this adds a bit of complexity, but hopefully you can see the value: you are able to write a shared component used by multiple slices, but each instance could have a completely different DB connection injected by the slice context. If you don’t need this kind of dependency isolation, then you can just put everything in the top-level app slice and not think about it.

1 Like

Tim Riley, one of the primary authors of Hanami, is building an app with it at decafsucks/decafsucks.

Note that he is writing his repo in exactly the same way.

Hanami will have much better integration of this in the near future; they are targeting Q1 2023 for persistence and view support.

3 Likes