Earlier this year we introduced our view layer with Hanami 2.1. After some intensive months of work, we’re back again and ready to complete the stack! With today’s release of Hanami 2.2.0.beta1, we’re delighted to offer you a preview of our database layer, as well our new tool for organising business logic.

Introducing our database layer

Hanami’s database layer is based on Ruby Object Mapper (ROM), a mature and flexible data persistence toolkit for Ruby.

Our goal for Hanami 2.2 is to provide the world’s best ROM experience, one that feels thoroughly at home within Hanami apps. We want to make it easy for you to get started and enjoy the benefits that come from your separating persistence logic from your business concerns. At the same time, we also want to make sure you can use any ROM feature without having to eject yourself from Hanami’s standard integration.

With 2.2, we believe we’ve achieved this. Let’s take a look at how it has come togther.

When you generate a new app, you’ll have a ready-to-go DATABASE_URL set in your .env. Our default for new apps is SQLite, with Postgres also supported, and MySQL is coming soon.

You can create a migration with bundle exec hanami generate migration, and fill it in:

ROM::SQL.migration do
  change do
    create_table :posts do
      primary_key :id
      column :title, :text, null: false
    end
  end
end

Now you can migrate your database with hanami db migrate.

After this, you can generate a new relation: hanami generate relation posts. Relations describe your low-level data sources. Here, this means your database tables. Relations are also your place to add reusable, chainable methods that you can use as the building blocks for expressive, higher-level queries. Add something simple to get started:

module MyApp
  module Relations
    class Posts < MyApp::DB::Relation
      schema :posts, infer: true

      use :pagination
      per_page 20

      def order_by_latest
        order(self[:id].desc)
      end
    end
  end
end

Relations are the primary component of your app’s database layer. While you can interact with them directly, it’s better to build a repo. With repos, you get to build your very own persistence API, so you can better manage how your data is accessed across your app. You can build a post repo with hanami generate repo posts. Here you can define a method to return your latest posts, built from the methods in your relation:

module MyApp
  module Repos
    class PostRepo < MyApp::DB::Repo
      def latest(page:)
        posts.order_by_latest.page(page).to_a
      end
    end
  end
end

You can include this repo as a dependency of any other class in your app, which is how you can access your data wherever you need. In a view, for example:

module MyApp
  module Views
    module Posts
      class Index < MyApp::View
        include Deps["repos.post_repo"]

        expose :posts do |page: 1|
          post_repo.latest(page:)
        end
      end
    end
  end
end

Repo methods return structs: plain old value objects, just data, with no live connection back to the database. This means you can be confident in passing them all around your app, knowing things like accidental n+1 queries are a thing of the past.

You can customize these structs by creating your own classes. Make one for your posts with hanami generate struct post. Inside these classes, you can access any of the attributes selected in your repo’s corresponding database query.

module MyApp
  module Structs
    class Post < MyApp::DB::Struct
      def excited_title
        "#{title}!"
      end
    end
  end
end

With relations, repos, structs and more, you now have a home for every piece of your data logic, and the foundation for a database layer that can evolve to meet even the most demanding needs.

Your DB is our command

You can manage the full lifecycle of your database thanks to this complete set of new CLI commands:

  • hanami db create
  • hanami db drop
  • hanami db migrate
  • hanami db prepare
  • hanami db seed
  • hanami db structure dump
  • hanami db structure load
  • hanami db version

Slice it your way

It wouldn’t be a new Hanami feature if it didn’t come with first-class support for slices, our built-in tool for modularising apps.

With Hanami 2.2’s new database layer, you can choose to:

  • Share a database, but have each slice provide their own relations, so they can choose how much of the database to expose (the default)
  • Use a dedicated database for certain slices (as easy as creating a SLICE_NAME__DATABASE_URL env var)
  • Share a single set of relations across all slices, for a simpler, blended development experience
  • Or any combination of the above!

See this forum post to learn more about these various slice formations. What we’ve delivered in this beta matches exactly the proposal in this post.

Introducing operations

With Hanami 2.2 we’re debuting dry-operation. With dry-operation, you have a streamlined tool for organising your business logic into flexible, composable objects made from flows of internal steps.

dry-operation is the long-awaited successor to the venerable dry-transaction gem, and I’m deeply grateful to Marc Busqué for building it. Even though it’s not quite fully released, we’re including it as a preview via a GitHub source in your new app’s Gemfile.

Creating an operation is as easy as hanami generate operation posts.create_post. Operations can be built from multiple steps, with each returning a Result:

module MyApp
  module Posts
    class CreatePost < MyApp::Operation
      include Deps["repos.post_repo"]

      def call(attributes)
        validation = step validate(attributes)

        post = post_repo.create(validation.to_h)

        Success(post)
      end

      private

      def validate(attributes)
        # Validate attributes here.

        # Return a `Failure` and execution above will short-circuit
        #   Failure(errors: ["not valid"])

        # Return a `Success` and execution will continue with the value unwrapped
        #   Success(attributes)
      end
    end
  end
end

Every operation returning a Success or Failure is great for consistency (every caller is required to consider both sides), but also for expressiveness. You can now turn to pattern matching on results in your actions, for example:

module MyApp
  module Actions
    module Posts
      class Create < MyApp::Action
        include Deps["posts.create_post"]

        def handle(request, response)
          result = create_post.call(request.params[:post])

          case result
          in Success(post)
            response.redirect_to routes.path(:post, post.id)
          in Failure(validation)
            response.render(view, validation:)
          end
        end
      end
    end
  end
end

Operations are natively integrated with Hanami’s database layer, providing transaction do ... end to ensure database changes are written together, and that an intervening Failure will automatically abort the transaction.

With operations built from flows of steps, but also themselves able to be chained into higher-level flows, you have a powerful new building block for business logic in your Hanami apps.

We need your help!

This is a major step for Hanami, so we need your help with testing.

We’ve already updated our getting started guides to walk you through your first Hanami 2.2 app, database layer included. Please give this a try, then let us know how you go.

What’s next? A release candidate, then 2.2.0.

We want this to be the one and only beta release for 2.2.

The work we have remaining is relatively minor, so our next step from here will be a release candidate, and your last chance for testing before 2.2.0 stable.

What’s included?

Today we’re releasing the following gems:

  • hanami v2.2.0.beta1
  • hanami-assets v2.2.0-beta.1 (npm package)
  • hanami-assets v2.2.0.beta1
  • hanami-cli v2.2.0.beta1
  • hanami-controller v2.2.0.beta1
  • hanami-db v2.2.0.beta1
  • hanami-reloader v2.2.0.beta1
  • hanami-router v2.2.0.beta1
  • hanami-rspec v2.2.0.beta1
  • hanami-utils v2.2.0.beta1
  • hanami-validations v2.2.0.beta1
  • hanami-view v2.2.0.beta1
  • hanami-webconsole v2.2.0.beta1

For specific changes in this release, please see each gem’s own CHANGELOG.

How can I try it?

> gem install hanami --pre
> hanami new my_app
> cd my_app
> bundle exec hanami dev

Contributors

Thank you to these fine people for contributing to this release!

Thank you

Thank you as always for supporting Hanami! We can’t wait to hear from you about this beta! 🌸