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:
- Add gems.
- Set up ENV variables.
- Update
config/settings.rb
. - Create
config/providers/persistence.rb
. - Add
bin/hanami
to enablehanami 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 doingcreate
andmigrate
in succession.reset
- Same as doingdrop
,create
, andmigrate
.sample_data
- Run the script you create atdb/sample_data.rb
to load sample data.seed
- Run the script you create atdb/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.