HanamiMastery

Integrate Hanami 2.0 with your Database using ROM

Episode #9

by Sebastian Wilgosz

Picture of the author

In the Hanami Mastery #002 I've shown you how to list articles in Hanami 2.0 application and in the third episode, how to prettify them using Bulma CSS framework. However, there is an issue with this.

Importance of persistence

I have this application running at the moment, so we can easily see the problem I'm talking about. Basically, whenever you refresh the page, the article details are changed.

Blog listingBlog listing

It happens, because I've cheated a bit in the previous episodes, first to keep the videos short and focused on the rendering part of the framework - but secondly because it was before the release of Hanami 2.0 alpha versions so it was unclear how ROM updates will affect the implementation.

Now, with ROM 6.0 ready and amazing Hanami 2.0 integrations in place, it's finally a time to finish it off, and showcase you the complete integration of ROM with the SQL database within a Hanami 2.0 application.

Marc Busqué & Todos app

I was able to pull this episode together, thanks to the awesome work of Hanami Core Team Member, Marc Busqué. He created a complete todos application, with ROM integration and all CRUD actions in place, showcasing how to replace the Hanami Actions with WebPipe. This is an amazing example of the elastic nature of Hanami components, which can be easily replaceable by whatever you wish.

He opensourced it on Github so I strongly encourage you to check it out. As always you can easily access it using resources links in this episode.

Faker & Vitor Oliviera

When I was focused just to show the rendering part of the Hanami application, I came up with a quick solution to define a dummy entity in the main slice, with the default parameters in place. Those entities can be easily created in any place of the application and I bravely created them inside of view objects.

To quickly generate a random entity to be displayed on the page, I used a faker gem.

If you don't know faker, it allows you to fill your objects with random data taken from different sets without an effort. I heavily use it for seeding data in my projects, and I appreciate the effort of the main author, Vitor Oliviera.

This guy has** 5 times more contributions in the last year than me**, which I see as pretty impressive. Feel free to check out his other projects he manages a few popular repositories.

However, let's go back to the topic.

Current buggy implementation

# slices/main/lib/main/entities/article.rb

require 'faker'

module Main
  module Entities
    class Article
      attr_accessor(
        :title,
        :excerpt,
        :author,
        :content,
        :thumbnail,
        :id,
        :published_on
       )

      def initialize(args = {})
        self.id = args[:id] || 1
        self.title = Faker::Hacker.say_something_smart
        self.excerpt = Faker::Lorem.paragraph(
          random_sentences_to_add: 4
        )
        self.content = Faker::Lorem.sentences(number: 50).join(' ')
        self.thumbnail = "https://loremflickr.com/800/460/cat?lock=#{id}"
        self.author = Author.new
        self.published_on = "1 Jan 2016"
      end
    end
  end
end

For each created article we also create a random author and assign it to the author-reader. When I open the author entity, you'll see a very similar data structure, where everything is just randomly filled in using faker helpers.

# slices/main/lib/main/entities/article.rb

require 'faker'

module Main
  module Entities
    class Author
      attr_accessor :first_name, :last_name

      def initialize(args = {})
        self.first_name = Faker::Name.first_name
        self.last_name = Faker::Name.last_name
      end
    end
  end
end

Initially, I just grabbed those entities in the view, and created a collection of randomly filled objects, and then exposed the result to the template.

# slices/main/lib/main/views/blog/articles/index.rb

module Main
  module Views
    module Blog
      module Articles
        class Index < View::Base
          expose :articles do
            (1..20).map do |i|
              ::Main::Entities::Article.new(id: i)
            end
          end
        end
      end
    end
  end
end

Seriously?Seriously?

Yes, seriously.

But now let's implement it using actual records fetched from the database, as one should expect.

The correct approach - ROM.rb in action.

Hanami has built-in integration with ROM. This is the default ORM of choice, and even if it can be replaced by anything, I don't see a reason to do so. It's powerful, fast, and easy to use. I talked about ROM with comparison to Activerecord in dedicated article, feel free to chech it out!

Create migrations

First, we need to create the necessary database tables, for articles and authors, and for that, I'll use the hanami migration generator.

hanami db create_migration create_authors
hanami db create_migration create_articles

This will create an empty migration file but with the correct timestamp automatically added to the file name.

ROM::SQL.migration do
  change do
  end
end

Now let's fill this migration in, and create an :authors table with the id as a primary key, first name, and the last name to be listed in articles later on.

# db/migrate/xxxx_create_authors.rb

ROM::SQL.migration do
  change do
    create_table(:authors) do
      primary_key :id
      column :first_name, String
      column :last_name, String
    end
  end
end

Now for the articles database, I'll need a bit more fields, basically the same as I had in the dummy entity before.

Therefore I need a title, excerpt, content, and the thumbnail URL, all types of string. Then I need the author reference, and the publication date information.

# db/migrate/xxxx_create_articles.rb

ROM::SQL.migration do
  change do
    create_table(:articles) do
      primary_key :id
      column :title, String
      column :excerpt, String
      column :content, String
      column :thumbnail, String
      foreign_key :author_id, :authors
      column :published_on, Date
    end
  end
end

Now I can RUN the migrations... and my tables are created.

hanami db migrate

NOTE: If you want to know more about migration DSL in Hanami, ROM migrations are based on Sequel, created by Jeremy Evans, one of Ruby Legends I would say. You can check the detailed Sequel migrations documentation here if you're interested more about this topic.

Relations

Now having that database tables in place, I'll create the Relations and Repositories for both resources.

Hanami allows you to have separate sets of persistence-related resources for each application slice, but because my application is so small, I'll create them in the global namespace.

Articles relation

First I will create the article relation, which inherits from ROM sql relation, and define the schema based on :articles table, setting a flag: infer to true. This will automatically set my attribute readers on the entity, based on the table definition!

# 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

NOTE: Keep in mind, that sandbox is my application name.

Then within the schema, I'll define the belongs_to association for the article's author.

Authors relation

Let's repeat the same thing for authors, with the difference, that author has many articles.

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

module Persistence
  module Relations
    class Authors < ROM::Relation[:sql]
      schema(:authors, infer: true) do
        associations do
          has_many :articles
        end
      end
    end
  end
end

WARNING: Please notice, that there is no module Sandbox in either of the relation files. It's because there is a known namespacing bug in the pre-2.0.0-alpha2 Hanami releases that should be fixed very soon.

Because we do entity definitions based on the tables definitions, there is no need to manually define entities at the moment and those can be removed completely!

Repositories

Now, let's add an article repository.

Update notice!

You'll encounter a problem with loading repositories due to file-loading improvements introduced in Hanami 2.0-alpha3. Check out episode #13 for detailed explanation!

In ROM relations are responsible for communicating with the database, to fetch the data. There you define queries specific for the DB you use, or if you wish - scopes definitions.

Repositories are database-agnostic and can use multiple relations to update and fetch resources from many databases if needed. This is why it's extremely easy to replace database adapters in Hanami while keeping the same interface and minimizing the required changes to be done across the application.

Articles repository

Let me defióhmwne the article repository now, so we can fetch and create resources in the database.

By adding the commands :create, we extend the default repository by the ability to create resources, which we can use to seed the data to our system.

# 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
      end
    end
  end
end

then I just need to define the all method, and inside I combine articles with their authors, returning the array of results at the end.

Authors repository

Now I'll also add the authors' repository. I don't need it for listing my articles, but It will be useful for seeding data into our database.

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

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

        def all
          authors.to_a
        end
      end
    end
  end
end

We have almost everything done now. Now let's tweak our actions and views.

Rendering the model

Previously I've instantiated the article collection inside of the view directly, but that's not the correct way to go. View object should only contain view-related logic, but all data should be passed into it from actions.

Therefore, let's remove the block from the expose method, and open the corresponding action.

# slices/main/lib/main/views/blog/articles/index.rb

module Main
  module Views
    module Blog
      module Articles
        class Index < View::Base
          expose :articles
        end
      end
    end
  end
end

Here I will inject the articles repository as a dependency, and add a name repo to the newly created reader.

Notice: Have you spotted, that the root key is prefixed with an application string? We can have several containers defined in the system, and the persistence dependencies are managed by the application container, and this is where this prefix comes from.

# slices/main/lib/main/actions/blog/articles/index.rb

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

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

Now I need to handle the incoming request, by rendering the corresponding view with the articles variable passed in. As a value, I'll call my repository, fetching all articles into it.

Because we have the articles variable exposed, there is nothing we need to do in templates.

Seeding data

This is all that's required to make our articles' listing work!

However, visiting the articles page in the browser now will show you an empty list of articles, because there are none saved in our database yet.

In the terminal, I can open hanami console to manually create records, but we already know faker and we know that we can do better. I'll use db/seeds.rb file to create all necessary resources in an automatic way.

To do so, I'll open the seeds file, and add the necessary insertion rules.

First I require the faker and then load the authors' repository from the Main slice container.

# db/seeds.rb
require 'faker'

authors = Main::Container['application.persistence.repositories.authors']

Then let me create a few completely random records.

authors.create(first_name: 'Seb', last_name: 'Wilgosz')
authors.create(first_name: 'Hanami', last_name: 'Mastery')
authors.create(first_name: 'Awesome', last_name: 'Subscriber')

First will be me, as an author, then the Hanami Mastery project, in case the author of the article would like to remain anonymous, and finally, You, my awesome subscriber, in case you'll ever subscribe to me and want to write a Hanami article on your own.

Now I need to fetch the IDS of all my authors, to randomly generate one for each newly created article.

author_ids = authors.all.to_a.map &:id
articles = Main::Container['application.persistence.repositories.articles']

20.times do |i|
  articles.create(
    **title: Faker::Hacker.say_something_smart,
    excerpt: Faker::Lorem.paragraph(random_sentences_to_add: 4),
    content: Faker::Lorem.sentences(number: 50).join(' '),
    thumbnail: "https://loremflickr.com/800/460/cat?lock=#{i}",
    author_id: author_ids.sample,
    published_on: "1 Apr 2021"**
  )
end

Now I need to get the articles repository in the same way and create a loop of maybe twenty random records in it.

Here is where I'll use faker, similar to what I did before in the entity model. I already saved this script aside, so let me paste it here.

Then let's run our seeds and run the server

hanami db seed
docker-compose up

We can now run the server and visit localhost:2300 to check out the result in the browser.

Final blog listing with persistence setFinal blog listing with persistence set

It looks pretty similar to what we had before, however, after refreshing the page, everything will stay the same.

Articles preview action

Now, when our list works well, let's quickly apply the same changes to the single article view.

# slices/main/lib/main/views/blog/articles/show.rb

module Main
  module Views
    module Blog
      module Articles
        class Show < View::Base
          expose :article
        end
      end
    end
  end
end

I remove the dummy article fetching logic from here and move it to the action

# slices/main/lib/main/actions/blog/articles/show.rb

module Main
  module Actions
    module Blog
      module Articles
        class Show < Main::Action
          include Deps[
            repo: 'application.persistence.repositories.articles'
          ]

          def handle(req, res)
            article = repo.find(req.params[:id])
            res.render view, article: article
          end
        end
      end
    end
  end
end

Again, I inject the repository dependency and handle the response, this time setting the article variable to be exposed. To fetch the article, I'll call the find method with the request parameters.

Then, in the repository, I'll define the find method, to behave exactly as we would expect:

# frozen_string_literal: true

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

I will combine the articles with the author, and find it by primary key, passing the given id as an argument. Then I'll ensure that only one record is returned.

Now after restarting the server page should work well. Oh, it seems I made a little mistake in the show action, so let me visit it very quickly. Yes, I used the render method on the action object, instead of the response. Now should be fine.

Hurray!

The article is persistent and does not change even after the page refresh, I can safely browse my publications and manage resources exactly as one would expect from a blog application.

Summary

That's all for today!

I hope you've enjoyed this episode, and if you want to see more content in this fashion, subscribe to this YT channel, newsletter and follow me on Twitter! As always, all links you can find the description of the video or in the https://hanamimastery.com.

Special Thanks!

I'd like to thank Sebastjan Hribar, Thomas Carr, and Useo. for supporting this project! Really appreciated! Also thanks to all my existing sponsors for their continuous support.

Any financial support allows me to spend more time on creating this content, promoting great open source projects, open-source heroes, and Hanami in General.

Thank you for reading, you're awesome! - and see you in the next Hanami Mastery episode, covering interesting topics related to Hanami and anything else happening in the Ruby world!

Also thanks to:

  • Jan Antonin Kolar- for a great cover image
  • Marc Busqué - for creating his sample todos application.

...and to the whole Hanami team for being nice and supportive at any stage of my activity.

Have a great day and happy coding!

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...

#28 Configure ROM from scratch
rom-rbhanamirelations

Hanami 2.0 comes without the persistence layer nor views preconfigured. It is useful then to know how to set up the best ORM available in the Ruby ecosystem.

I've wondered why Hanami uses sequel under the hood. There are some problems with ActiveRecord, but I've wanted to know exactly, what it is about. Here is the summary of my foundings.

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.