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! 🌸