Seeking Opportunities
Contact Me

Full Stack Hanami, Part 1

Not dogma, just what I'm learning and thinking about right now.
Comments and feedback are welcome on Mastodon.

"If you're thinking without writing, then you just think you're thinking."
Leslie Lamport

If you’re anything like me, then I know you’re chomping at the bit to use Hanami 2. Hanami 2.1.0.beta1 brought us view integration and beta2 now has asset management in place, but persistence is still missing. It’s hard to get the feel for a framework without being able to build functioning projects with it, and functioning projects need persistence. Well if that was all that was holding you back, then your wait is over! Let’s walk through adding ROM-rb now so you can get started with Hanami 2!

Note: If you want to get up and running quickly, Brooke Kuhlmann’s Hanamismith project is a great choice. It’s a command line tool to jumpstart Hanami projects with persistence baked right in. I took the more vanilla approach I describe here.

These guides are derived from the examples contained in the Hanami docs, Tim Riley’s Decaf Sucks project, and Brooke Kuhlmann’s Hemo demo application. I am grateful to those authors for providing such excellent examples to follow!

What We Will Cover

There’s a lot of material here, so I intend to break it up into three articles. There will be:

  • Part 1 - Adding ROM. Covers the gems we need and how to get them working with Hanami 2.
  • Part 2 - Working with ROM. Covers relations, repos, and database chores (creation, migrations, etc.).
  • Part 3 - Testing. Set up tooling to use your database in RSpec.

In This Article

These are the exact steps we will follow to get ROM working in our project:

  1. Add gems.
  2. Set up ENV variables.
  3. Update config/settings.rb.
  4. Create config/providers/persistence.rb.
  5. Add bin/hanami to enable hanami db CLI tasks.

Prerequisites and Provisos

This walk-through assumes you are able to setup and run a Hanami 2.1.X app on your machine. You can use this guide to add persistence to an existing Hanami app or spin up a new one. The examples here will use an app titled “Fullstack.” You can use the following commands to start fresh and follow along. I’ll be using Ruby 3.2, but any recent version should work.

> hanami new fullstack
> cd fullstack
> echo '3.2' > .ruby-version

We’ll be adding ROM-rb for persistence. If you have a different preference, you may be able to use this article as a guide for setting up your preferred tool, but the details will vary. We’ll be using Postgres as the relational database backing ROM, so you need to have Postgres installed, along with the libpq-dev package if you’re on Linux. This package is needed to compile the pg gem. I believe the same is true on Macs, but I have no idea what is needed on Windows. ROM is compatible with databases other than Postgres, but their usage is not covered here.

Finally, you need to create a Postgres user and password that will be used by your Hanami app, as well as the project database itself. It’s not a good idea to use root or your own username. While this is not a Postgres tutorial, I think these instructions for creating a user, password, and database after a fresh install are still valid. We’ll cover creating our databases using Hanami CLI commands, so you don’t need to create them just yet.

With that out of the way, let’s get started!

1. Add Gems

First, we’ll add our dependencies. There are six gems that we’ll need, four under general dependencies and two under the :test group. Open your project gemfile and add the following lines as indicated.

# Gemfile

# ... general dependencies ...

# persistence
gem "pg"
gem "rom"
gem "rom-sql"
gem "sequel"

# ... group dependencies ...

group :test do
  # ... existing dependencies ...

  # database
  gem "database_cleaner-sequel"
  gem "rom-factory"
end

The first four gems added here are the four amigos that provide database persistence. The pg gem is a general-purpose Ruby Postgres client library. Next is the rom gem, along with the rom-sql adapter to provide a “SQL-specific query DSL” (per the ROM docs). Finally, we have sequel, a powerful “database toolkit for Ruby,” that is the foundation upon which ROM is built.

Under the :test group, we have database_cleaner-sequel and rom-factory. These gems will be used in Part 3 of this series, which covers using the database in testing.

Now we bundle install and verify that the gems and their dependencies install correctly.

2. Set Up ENV Variables

This next part is just bookkeeping of a sort: we’ll tell Hanami how to connect to Postgres. To do this, we need to provide a URI connection string. As the Postgres docs show, the connection string contents are very flexible. But at the end of the day we need to provide four things:

  • username,
  • password,
  • host connection, and
  • database name.

This is usually done with a connection string that looks like this, where the user name and password are the ones you created above, and the hostspec is set to localhost for local development:

postgres://<user-name>:<password>@[hostspec][/dbname]

So, what do we do with the connection string? We are going to use it to set a DATABASE_URL environment variable. In fact, two of them. Hanami uses the dotenv gem, which allows us to set environment variables for our various environments using .env files. We are going to create two files, one for development and one for test. These files will provide identical connection strings (as defined above), save for the database name. You will have to create these files in the root directory of your project if they do not yet exist.

# .env.development

DATABASE_URL=postgres://<user-name>:<password>@localhost/fullstack_development
# .env.test

DATABASE_URL=postgres://<user-name>:<password>@localhost/fullstack_test

The DATABASE_URL environment variable will now be available to our app, and will automatically switch between development and test according to the value of HANAMI_ENV when the app is started.

3. Update config/settings.rb

This step will be even easier than the last one. We are going to pull the DATABASE_URL into our app by assigning it to a globally available “setting.” You can read more about Hanami settings here. If you just created your app, config/settings.rb should look like this:

# config/settings.rb

module Fullstack
  class Settings < Hanami::Settings
    # Define your app settings here, for example:
    #
    # setting :my_flag, default: false, constructor: Types::Params::Bool
  end
end

You should now edit this file to look like this:

# config/settings.rb

module Fullstack
  class Settings < Hanami::Settings
    # Define your app settings here, for example:
    #
    # setting :my_flag, default: false, constructor: Types::Params::Bool

    setting :database_url, constructor: Types::String
  end
end

The added line tells Hanami to extract DATABASE_URL from the environment variables and assign it to the “settings” key of our app container. That means we can now invoke the Hanami console:

> bundle exec hanami console

and type

fullstack[development]> Hanami.app["settings"].database_url

And it should return the connection string we provided in our .env.development file. Because we added this setting to config/settings.rb, it is now required. The app initialization will now fail if a value for DATABASE_URL is not provided as an environment variable.

Let’s talk about the second, named argument that we passed to the setting method: constructor: Types::String. This argument tells Hanami to attempt to coerce the value provided for DATABASE_URL into a string. If this coercion fails, then the app initialization will also fail. This ensures that we never attempt to pass anything other than a string to Postgres.

4. Create config/providers/persistence.rb

In order to understand this next step, I want to pause here to review some aspects of how Hanami works under the hood.

Hanami Component Management System

Hanami implements a component management system based on containers. The default container in all Hanami apps is Hanami.app, which we saw in the previous section. All of the classes available in a container are registered under “keys” based on the inflection of their Ruby class names. In the previous section, we saw that the Settings class is made available in the Hanami.app container under the key settings. As with our database URL connection string, registration makes our classes available anywhere in our app.

One way to see all of the classes available in Hanami.app, is to display them in the Hanami console, like so:

 bundle exec hanami console

fullstack[development]> Hanami.app.boot
=> Fullstack::App

fullstack[development]> Hanami.app.keys
=> ["settings", "notifications", "routes", "inflector", "logger", "rack.monitor"]

The keys you see here are the default classes available in all Hanami apps. If you are not familiar with the Hanami console, the “fullstack” you see in the CLI prompt is the name of our app. The name you selected for your app will appear in your console prompt. Also, [development] indicates that the app was started in the development environment, as opposed to test or production.

Hanami provides two standard ways to register classes in a container. The first is via autoloading: upon booting, Hanami will automatically register all classes defined in the app directory or its sub-directories. You can read more in the docs at the previous link and here. The second way is to manually register classes by creating “providers.” Hanami will include any providers we put in the config/providers/ directory when autoloading. In order to make persistence available throughout our app, our next step is to create a provider for ROM-rb.

Creating a Provider for Rom

This will be the most intricate code we need to add. Let’s jump right in.

 1 # /config/providers/persistence.rb
 2 
 3 # frozen_string_literal: true
 4 
 5 Hanami.app.register_provider(:persistence, namespace: true) do
 6   prepare do
 7     require "rom-changeset"
 8     require "rom/core"
 9     require "rom/sql"
10 
11     rom_config = ROM::Configuration.new(:sql, target["settings"].database_url)
12 
13     rom_config.plugin(:sql, relations: :instrumentation) do |plugin_config|
14       plugin_config.notifications = target["notifications"]
15     end
16 
17     rom_config.plugin(:sql, relations: :auto_restrictions)
18 
19     register "config", rom_config
20     register "db", rom_config.gateways[:default].connection
21   end
22 
23   start do
24     rom_config = target["persistence.config"]
25 
26     rom_config.auto_registration(
27       target.root.join("app"),
28       namespace: Hanami.app.namespace.to_s
29     )
30 
31     register "rom", ROM.container(rom_config)
32   end
33
34   stop do
35     target["persistence.rom"].disconnect
36   end
37 end

The two code blocks shown here, prepare and start, are “lifecycle methods.” Hanami provides for three such methods: prepare, start, and stop. The prepare method is for setup and configuration, start is for invoking the component at runtime, and stop is for any code that needs run when the component is stopped (for teardown, clean-up, or other such housekeeping). Let’s walk through the code.

5- Establish persistence as a key in the Hanami.app container.

#prepare method

7-9- Required libraries should be loaded in the prepare block. I specified these three files following the examples in Decaf Sucks and Hemo.

11- Create a new ROM configuration instance using the ROM sql adapter. Note the use of the target method, provided by Hanami, to access settings from within our provider to extract the database_url.

13-15- Wire up ROM to forward notifications to Hanami’s notification class, again accessed using the target method.

17- This convenience directive tells ROM to automatically generate methods that can be used to filter datasets using any table attribute with an index created on it. For example, if your table has an indexed email field, ROM would automatically generate a #by_email method on the resulting relation.

19- Register the rom_config instance as config under the persistence key.

20- Register the ROM connection as db under the persistence key.

#start method

24- Extract the rom_config from the persistence key. This is required because variables created in the prepare method are not available in the start method.

26-29- Configure ROM to autoload our relations. We’ll look at each of these arguments separately.

27- This argument tells ROM where to find our relation classes for autoregistration. Following the example of DecafSucks, we will be placing our /relations directory directly in /app. See the Hemo app for a counter example where relations are saved under /lib/hemo/persistence/relations.

28- This argument tells ROM that your relation namespace is the same as your app namespace. That is, if your app is called “fullstack”, then ROM will namespace relations under Fullstack::Relations. See the Hemo app for a counter example where the namespace Hemo::Persistence::Relations is used.

31- Finally, we register the ROM connection object as rom under the persistence key.

#stop method

35- The only job of the this method is to close the database connection when the app is shutdown with the Hanami.shutdown command. This is used, for example, by the puma server to close the database connection before forking new worker processes.

Now, finally, if we return to the Hanami console, boot our app with Hanami.app.boot, and check Hanami.app.keys, we will see persistence.config, persistence.db, and persistence.rom all listed there. This is all we need to access ROM from anywhere in our app.

5. Add bin/hanami to enable hanami db CLI tasks.

As our final task, we are going to expose the db commands in the Hanami CLI to manage database tasks. We will be adding this script, taken directly from Decaf Sucks, to our project as bin/hanami. Note that there is no .rb extension on this file. Here is the script.

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

require "bundler/setup"
require "hanami/cli"

# Hanami 2.0 does does not officially include database integration. However, much of the required
# work is already done and included in the gem.
#
# This CLI shim activates the database commands so we can manage our app database.

Hanami::CLI.tap do |cli|
  cli.register "db create", Hanami::CLI::Commands::App::DB::Create
  cli.register "db create_migration", Hanami::CLI::Commands::App::DB::CreateMigration
  cli.register "db drop", Hanami::CLI::Commands::App::DB::Drop
  cli.register "db migrate", Hanami::CLI::Commands::App::DB::Migrate
  cli.register "db setup", Hanami::CLI::Commands::App::DB::Setup
  cli.register "db reset", Hanami::CLI::Commands::App::DB::Reset
  cli.register "db rollback", Hanami::CLI::Commands::App::DB::Rollback
  cli.register "db sample_data", Hanami::CLI::Commands::App::DB::SampleData
  cli.register "db seed", Hanami::CLI::Commands::App::DB::Seed
  cli.register "db structure dump", Hanami::CLI::Commands::App::DB::Structure::Dump
  cli.register "db version", Hanami::CLI::Commands::App::DB::Version
end

Hanami::CLI::Bundler.require(:cli)

cli = Dry::CLI.new(Hanami::CLI)
cli.call

As you can see, all this script does is register a series of existing commands in the Hanami-CLI gem so that we can use them. To make this script executable, you must first execute chmod +x bin/hanami from a command prompt in the root directory of your app.

To setup your development and test databases, you can now use the following commands:

> bin/hanami db setup
> HANAMI_ENV=test bin/hanami db setup

Most of the commands added by this script are probably familiar to you. You can get a feel for many of them from the Hanami 1.3 docs, although they may be slightly different in Hanami 2.

To summarize some of the non-intuitive commands:

  • setup - Same as doing create and migrate in succession.
  • reset - Same as doing drop, create, and migrate.
  • sample_data - Run the script you create at db/sample_data.rb to load sample data.
  • seed - Run the script you create at db/seeds.rb to seed the database.
  • structure_dump - “Dump” the database structure to a file, db/structure.sql.
  • version - Return the current schema “version,” as represented by the name of the most recent migration applied.

And that’s it! We’re now ready to use persistence in our Hanami 2 app! Ready to move on to Part 2? Before we do, maybe we should do something to confirm that all of this configuration is working. Maybe we should add a test!

BONUS: Test That Persistence is Available

This spec will not test that the database is working, just that our configuration is correct and that our ROM provider is functioning. To do this, create the following spec file in spec/providers (you will have to create this directory).

# spec/providers/persistence_spec.rb

RSpec.describe "Persistence" do
  it "can start the persistence provider" do
    expect { Hanami.app.start(:persistence) }.not_to raise_error
  end
end

There’s not much to go over here. This spec just starts the app and attempts to access the :persistence key in the Hanami.app container. If this does not raise an error, then our provider is able to start up correctly and get loaded into the container management system.

To run your specs, use:

> bundle exec rake spec
  or
> bundle exec rake

If everything is green, congratulations! We can now move one to Part 2, using ROM.