HanamiMastery

A sneak-peak into dependency loading with Hanami and dry-container

Episode #13

by Sebastian Wilgosz

Picture of the author

In this episode I'll show you a few ways to debug dependency loading via dry-container in Hanami applications, by using one of my recent setbacks.

Repository pattern

In episode 9, I showcased a complete integration of persistence layer in Hanami 2.0 applications, to deliver blog articles data read from my DB straight into the browser.

However, there was a mistake hidden in this tutorial that started to haunt me right after the release of the next Hanami Alpha version, as it caused incompatibility issues with what I've shown in the video.

There is a risk that some of my content becomes outdated quickly until official Hanami 2 is released but it's a risk I had consciously taken to help existing Hanami developers start playing with this project earlier.

info

However, to minimize such situations, I am currently releasing more videos about DRY libraries/, which are a bit more safe for me to cover.

While this is a normal risk when releasing tutorials about alpha versions, or rather about anything what happens in web development, this particular bug was caused because of my incomplete understanding of the Repository Pattern in ruby!

So, right after realizing that I did some research and to my surprise, I was not the only one who didn't get the concept in full shape.

In this article I'll explain where the problem came from and we will dig a little bit into loading dependencies using application containers.

Repository vs Relations

I will make a detailed explanation of a repository pattern and how it's done in Hanami as part of my Hanami Mastery PRO very soon, but for now, let's focus on the dependency injection part.

As Hanami uses ROM as the ORM of choice, all patterns and the conceptions of the persistence in Hanami are directly related to ROM concepts. This is cool, as it reduces the overhead of learning the intermediate DSL, and we can just rely directly on amazing ROM documentation to do anything it provides.

Therefore in Hanami, we have Relations and Repositories.

What I did in the initial integration of ROM was placing repositories next to relations in the general lib folder (lib/sandbox/peristence/repositories) and this was the core problem.

Repository

A repository is a bridge between the persistence layer and application logic in your application.

For example, you can have multiple slices in your application accessing the same database table!

Multi-Slice DB connectionMulti-Slice DB connection

In this case, each of our slices would have a separate repository, providing a clean interface to fetch only data the specific context is interested in.

Therefore repositories belong to the business part of your application and it is recommended that you define multiple repositories in different slices, which use the same relations to receive some data.

Relations

Relations, on the other hand, are definitely a part of the persistence logic.

In short, those are more or less, definitions of your table schemas and possible associations, to reflect what you have in your database.

# /lib/sandbox/persistence/relations/articles.rb

module Persistence
  module Relations
    class Articles < ROM::Relation[:sql]
      schema(:articles, infer: true) do
        associations do
          belongs_to :authors, as: :author
        end
      end
    end
  end
end

You may define very common queries there but queries specific for the given use case will definitely be placed within repositories.

The improved Hanami file structure

In occasion of a work on Hanami 2 Alpha3 release Tim Riley did an extraordinary contribution to dry-system improving it in a way that made possible flattened directory structure. You may read more about this amazing feature he added in his OSS Update article for September 2021.

Then several improvements had been applied, and as a result, repository loading started to be more strict and dumb resilient, ensuring that people will place them in a proper place by default.

Having that said, let's fix my implementation!

Moving repository to a slice

I have here my repository placed in a general lib folder, inside of my app's and persistence namespaces.

# /lib/sandbox/persistence/repositories/articles.rb

module Sandbox
  module Persistence
    module Repositories
      class Articles < Repository[:articles]
        commands :create

        def all
          articles.combine(:author).to_a
        end

        def find(id)
          articles.combine(:author).by_pk(id).one!
        end
      end
    end
  end
end
# /lib/sandbox/persistence/repositories/authors.rb

module Sandbox
  module Persistence
    module Repositories
      class Authors < Repository[:authors]
        commands :create

        def all
          authors.to_a
        end
      end
    end
  end
end

I will now move those into the main slice, simplifying the namespaces a bit by making use of the flattened file structure provided by dry-system now.

# /slices/main/repositories/articles.rb

module Main
  module Repositories
    class Articles < Repository[:articles]
      commands :create

      def all
        articles.combine(:author).to_a
      end

      def find(id)
        articles.combine(:author).by_pk(id).one!
      end
    end
  end
end
# /slices/main/repositories/authors.rb

module Main
  module Repositories
    class Authors < Repository[:authors]
      commands :create

      def all
        authors.to_a
      end
    end
  end
end

Notice that now my repositories don't have the persistence module anymore and my app namespace had been replaced by the name of slice.

Finally let me visit my actions and fix the repository loading.

# frozen_string_literal: true

module Main
  module Actions
    module Articles
      class Index < Main::Action
        include Deps[
          repo: 'repositories.articles'
        ]

        def handle(req, res)
          res.render view, articles: repo.all
        end
      end
    end
  end
end

As I skipped the persistence folder, I can now remove it also from here.

Then as I moved it to the current slice, I don't need the application namespace anymore neither.

Now my code is much cleaner and makes much more sense.

And more importantly, it works without issues!

List of articles loaded from DBList of articles loaded from DB

Container debugging tips.

It all seems very nice and easy, but it's because I exactly knew how my repositories are loaded, and under what keys they'll appear after the update.

But what if you actually want to figure that out?

Loading dependencies by using a container is an extremely powerful thing.

It can speed up tests suits like crazy, and allow for dependency injection without a hassle.

However, if you don't know the exact key your dependency is accessible by, it may be a bit confusing at the beginning to use it, so it is useful to know how to find loadable components.

Lazy loading dependencies

In the hanami console, I have the access to the container variable right away and it returns the general application container where all our configuration stuff lays in.

hanami console
container
# => Sandbox::Container

To preview all the dependencies loaded by the container, I can use the #keys method on it.

container.keys
# => ["notifications"]

However, it returns only one key for now - "notifications"!

In the development environment, all files are by default lazy-loaded by the container, which means, they are only loaded when they're needed.

This is why, if I'll ask the container what is loaded, it returns a minimal number of dependencies used when starting the console.

As soon as I will try to access for the first time the dependency not loaded yet, the container will load it and instantiate properly, automatically resolving all objects required by it!

container.keys
# => ["notifications"]
container['settings']
# => #<Sandbox::Settings:0x00007ff87a380318 @config=#<Dry::Configurable::Config values={:database_url=>"sqlite://./db/sandbox.sqlite", :session_secret=>"change-me", :precompiled_assets=>false}>>
container.keys
# => ["notifications", "settings"]

This feature allows the hanami console to be always extremely quick and the same applies to different test runs, no matter how much your app grows.

However, it's often useful to preview all loadable deps to quickly find what we need.

Load all dependencies on-demand using #finalize!

I can force the container to load everything at once by calling #finalize! method ending it with a bang.

container.finalize!
# => Sandbox::Container

Now checking the loaded dependencies again will result in all settings properly loaded.

container.keys
# => [
# "notifications",
# "settings",
# "assets",
# "persistence.config",
# "persistence.db",
# "persistence.rom",
# "rack_monitor",
# "routes_helper",
# "view.context",
# "inflector",
# "logger",
# "rack_logger"
# ]

With this you receive a complete control over loading dependencies, however here you still can't see your repositories, can you?

Slice-specific containers

It happens because each slice has their own container, independent of each other.

With this you may have different processes on separate pods running different parts of your system without loading all your codebase to the memory which is another huge benefit.

To access the main slice container, I just need to call it.

main_container = Main::Container
# => Main::Container

To launch the Hanami console, nothing from the Main slice is needed, so the container returns empty dependencies loaded.

main_container.keys
# => []

I can load necessary objects on-demand, or force all dependencies to be loaded at once, exactly like with the general application container.

sandbox[development]> main_container['repositories.articles']
=> #<Main::Repositories::Articles struct_namespace=Main::Entities auto_struct=true>
sandbox[development]> main_container.keys
# => ["repositories.articles"]
sandbox[development]> main_container.finalize!
# => Main::Container
sandbox[development]> main_container.keys
# => ["repositories.articles",
#  "application.notifications",
#  "application.settings",
#  "application.assets",
#  "application.persistence.config",
#  "application.persistence.db",
#  "application.persistence.rom",
#  "application.rack_monitor",
#  "application.routes_helper",
#  "application.view.context",
#  "application.inflector",
#  "application.logger",
#  "application.rack_logger",
#  "view.parts.article",
#  "actions.articles.drafts",
#  "actions.articles.published",
#  "actions.home.show",
#  "repositories.authors",
#  "views.articles.drafts",
#  "views.articles.published",
#  "views.home.show"]

As you can see, slice's container has access to everything from the application container, and to access it you just need to prefix the required dependency with application prefix.

For example, if I want to use logger in my action all I need to do in my file would be:

include Deps['application.logger']]

Finalizing All container at once with

There may be a situation, where you want to just load everything from all slices and just don't care. You can easily do it by manually booting your application. As my application is named Sandbox, my code would look like this:

Main::Container.keys
# => []
Sandbox::Application.boot
# => Sandbox::Application.boot
Main::Container.keys
# => ["repositories.articles",
#  "application.notifications",
#  "application.settings",
#  "application.assets",
#  "application.persistence.config",
#  "application.persistence.db",
#  "application.persistence.rom",
#  "application.rack_monitor",
#  "application.routes_helper",
#  "application.view.context",
#  "application.inflector",
#  "application.logger",
#  "application.rack_logger",
#  "view.parts.article",
#  "actions.articles.drafts",
#  "actions.articles.published",
#  "actions.home.show",
#  "repositories.authors",
#  "views.articles.drafts",
#  "views.articles.published",
#  "views.home.show"]

Summary

dry-system and dry-container are extremely powerful tools and if you understand how to work with them, you'll never look back.

Because of the amazing control they provide and loading always a minimal set of files to perform a single task, test-driven development becomes enjoyable again! You may have always fast test suits during the development, extremely performant rake tasks, and optimized pods for the production environment.

That's all for today, I hope you enjoyed this episode and that as always, you'll get learnings from my mistakes.

If you want to see more content in this fashion, Subscribe to my YT channel, Newsletter and follow me on Twitter!

Thanks

I want to especially thank my recent sponsors, Andrzej Krzywda, Sebastjan Hribar, and Useo for supporting this project, really apreciate it!

By helping me with a few dollars per month creating this content, you are helping the open-source developers and maintainers to create amazing software for you!

And remember, if you want to support my work even without money involved, the best you can do is to like, share and comment on my episodes and discussions threads. Help me add value to the Open-Source community!

As usual, here you can find two of my previous episodes! thank you all for supporting my channel, you are awesome, and have a nice rest of your day!

Additionally,

  • Ante Hamersmit for a great cover image!
Do you know great Ruby gems?

Add your suggestion to our discussion panel!

I'll gladly cover them in the future episodes! Thank you!

Suggest topicTweet #suggestion

May also interest you...

#15 Dependency Injection in Ruby - GOD Level! Meet dry-system! (Part 2)
dry-rbdry-systemdependency-injection

Dependency injection brings you great power, but comes with its own headaches. If you can get rid of them, You're left with the power only. In Ruby, with dry-system, it's possible. Here is how!

This episode is a special release, when Seb interviews Tim Riley and Luca Guidi from the Hanami Core Team, asking them questions collected from the community! This way we enjoy reaching our important milestone of releasing 50th Hanami Mastery episode!

There are plenty of popular ways to handle pagination in Ruby apps. But did you know, that ROM supports Pagination out of the box? And so Hanami does? In this episode, I'll show you how to quickly implement pagination from scratch for your Hanami projects.

#30 ROM - Mapping is everythingPRO

Understand data mapping in ROM, on all levels! In this episode we go through examples of simple to complex data mapping with ROM, with real usecases for each one.

Coffee buy button
Trusted & Supported by
DNSimple

1 / 2
Open Hanami Jobs

We use cookies to improve your experience on our site. To find out more, read our privacy policy.