Hanami 1.3 -> Hanami 2.0

I’ve started working on upgrading our projects from Hanami 1.3 → 2.0 and I’ll document my journey as I progress.

  1. This post will be updated as the time goes
  2. When (Not IF :sweat_smile: ) I’ll encounter some blockers, I’ll ask here for help in this thread.
  3. At the end I’ll compose a blog post (and guides chapter?) out of our findings here.

Any contribution to discussions appreciated :slight_smile:

Upgrade Steps

  1. Get Rid of Hanami Model → Switch to ROM 5.0
  2. Upgrade Ruby 2.7 → 3.1
  3. Upgrade all DRY Libraries
  4. Update Hanami → Hanami 2

Please let me know if you have more ideas, or need further help!

Resources

2 Likes

My first trouble:

I have JSONB columns in my entities. Before, reading them by Hanami::Model, resulted in a hash. After switching to ROM, the column read from DB returns JSON.

# Before (Hanami::Model)
message.payload
# => { author: "Jsmith", content: "My message" }

# After (ROM 5.0)

# frozen_string_literal: true

module Entities
  class Message < ROM::Struct
  end
end

# frozen_string_literal: true

module Persistence
  module Relations
    class Messages < ROM::Relation[:sql]
      schema(:messages, infer: true)

      auto_struct true
    end
  end
end

class MessageRepository < Rom::Repository[:messages]
end

MessageRepository.new.messages.first.payload
# =>  "{\"author\":\"Jsmith\",\"content\":\"My message\"}"

Question: How I should configure a Mapper for all JSONB columns in my system at once?

1 Like

Enable the pg_json extension from Sequel.

I’m not using Hanami yet but my dry-system app has this:

rom_config = ROM::Configuration.new(
  :sql,
  target[:settings].database_url,
  extensions: %i[pg_array pg_json]
)

Nothing specific needs to be done within ROM itself, in my experience.

1 Like

Thanks! Unfortunately did not work :(. Still when I read the payload, I get JSON instead Hash

Here’s an example script that works for me

#!/usr/bin/env ruby

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  gem 'pg'
  gem 'sequel_pg', require: 'sequel'
  gem 'rom', '~> 5'
  gem 'rom-sql'
end

require 'open3'

o, e, s = Open3.capture3("psql -c 'CREATE DATABASE rom_example'")
s.success? ? puts(o) : abort(e)

o, e, s = Open3.capture3('psql rom_example', stdin_data: <<~SQL)
  CREATE schema public;

  CREATE TABLE public.messages (
    id bigint NOT NULL,
    payload jsonb NOT NULL default '{}'
  );
SQL

s.success? ? puts(o) : abort(e)

require 'sequel'
require 'rom'
require 'rom/sql'

ROM::SQL.load_extensions :postgres

config = ROM::Configuration.new(:sql, "postgres://localhost:5432/rom_example", extensions: %i[pg_json])

config.relation(:messages) do
  schema(infer: true)
  auto_struct true
end

rom = ROM.container(config)
relation = rom.relations[:messages]

attrs = { id: 1, payload: { author: "John Smith", content: "My message" } }
relation.changeset(:create, attrs).commit

message = relation.where(id: 1).one
puts "MESSAGE PAYLOAD: #{message.payload.inspect}"

rom.gateways[:default].disconnect
system "psql -c 'drop database rom_example'"
$ ./example.rb
Fetching gem metadata from https://rubygems.org/.......
Resolving dependencies...
Using bundler 2.3.3
Using dry-initializer 3.1.1
Using method_source 1.0.0
Using pg 1.4.3
Using transproc 1.1.1
Using sequel 5.59.0
Using coderay 1.1.3
Using dry-inflector 0.3.0
Using ice_nine 0.11.2
Using concurrent-ruby 1.1.10
Using dry-equalizer 0.3.0
Using pry 0.14.1
Using sequel_pg 1.15.0
Using dry-core 0.8.1
Using dry-container 0.10.1
Using dry-logic 1.2.0
Using dry-types 1.5.1
Using dry-struct 1.4.0
Using rom-core 5.2.6
Using rom-changeset 5.2.3
Using rom-repository 5.2.2
Using rom 5.2.6
Using rom-sql 3.5.0
Expanded display is used automatically.
Null display is "¤".
CREATE DATABASE
Expanded display is used automatically.
Null display is "¤".
CREATE TABLE
MESSAGE PAYLOAD: {"author"=>"John Smith", "content"=>"My message"}
Expanded display is used automatically.
Null display is "¤".
DROP DATABASE
1 Like

@alassek Thank you! It actually worked! It appears, I loaded extensions and plugins wrongly.

I experienced more problems, as the Sequel::Postgres::JSONHash returns stringified keys, while the rest of attributes symbolized and it can’t be stringified via: Hanami::Utils::Hash.deep_stymbolize(attrs)

I’ve handled that. However, I got stucked with other advanced query magic with Sequel and ROM mix :(.

OLD Code from Hanami 1 (ROM 3.X)

# foos_by_users
before = Date.today.to_time.strftime('%Y-%m-%dT%H:%M:%S.%LZ')

foos
  .select { `data->>'user_id'`.as(:user_id) }
  .select_append { `MAX(data->>'featured_at')`.as(:featured_at) }
  .where(Sequel.lit("data->>'featured_at' <= ?", before))
  .group(:user_id)
  .order(nil)
  .pluck(%i(user_id featured_at))
  .to_h

Question: How to translate this to ROM 5?

Running above returns the error:

KeyError:
 :user_id attribute doesn't exist in foos schema

In new ROM syntax, select blocks should contain array instead of single objects.

foos
  .select do
    [
      `data->>'user_id'`.as(:user_id)
      `MAX(data->>'featured_at')`.as(:featured_at)
    ]
  end
   # ...

But still, .group(:user_id) does not see the attribute.

I’ve tried also:

.group(`data->>'user_id'`)

but this returns the undefined method error

     NoMethodError:
       undefined method `qualified_projection' for [:user_id, :featured_at]:Array

This looks like the pluck happens before grouping, or whatever.

cc: @solnic

I have a particular interest in SQL ASTs so this was an amusing diversion, although the software engineer in me wonders if this might be simpler to just implement as a database view.

This is complex so I’m going to take this a piece at a time.

SQL literals

There’s nothing wrong with using literals this way but there are Ruby interfaces for what you’re doing.

.select { data.pg_jsonb.get_text('user_id').as(:user_id) }
.select_append { function(:MAX, data.pg_jsonb.get_text('featured_at')).as(:featured_at) }
.where { data.pg_jsonb.get_text('featured_at') <= Date.today.to_time }

The pg_jsonb interface is less than ideal but it gets the job done.

GROUP BY

Method parameters are schema attributes, blocks are schema attributes and virtual columns

.group { user_id }

Pluck

Pluck is implemented as select + map. You’re already selecting so just try map here

.map(%i[user_id featured_at])
1 Like

@swilgosz Relation#pluck is supposed to be used to return values from specific columns, it doesn’t support projected columns. You can probably skip pluck and just do .to_hash(:user_id, :featured_at).

1 Like

Thank you!

I needed to add set Sequel.lit for grouping:

.groupSequel.lit(`data->>'user_id'`)

But the pluck accepting list of symbols instead of array was another issue. Thanks for the quick reply!

There shouldn’t be any need for using this. There are PG JSONB helpers that you can use in blocks like showed above.

1 Like

Thanks, will play with it more.

BTW: I found that PG:: JSON returns stringified hash while the rest of Entities is accessible via symbolized hash. What would be the best way ro unify this?

I have written the custom type on top of PG::JSONB, but it requires me to define all JSONB columns in relations manually.

Wondering if there is better approach

It seems I cannot edit the Main post in the thread anymore (probably too old), so I’ll add the resources in the comments.

How do you think about migration of views and templates ?

Hi, this thread is the closest thing I found to a guide for upgrading from Hanami 1.3.5 to 2.

The link on the official guides page (https://guides.hanamirb.org/) is broken. It points to: https://guides.hanamirb.org/upgrade-notes/v200 which doesn’t exist: guides/content/v1.3/upgrade-notes at main · hanami/guides · GitHub.

I’ve got a pet project I’m working on in my spare time that’s on Hanami 1.3.5. I’d be grateful if someone could point me to where I should start to upgrade it to 2.0? Is there any blog post or documentation?

As far as I know, I am the only person that spends effort on figuring things out on big projects at the moment.

Top level path for you to follow:

  1. Upgrade ruby to 2.7.7
  2. Lock hanami at 1.3.4 (1.3.5 does not introduce any new features but prevents installing ruby 3)
  3. Replace Hanami-Model with ROM 3 (or ROM 5 directly if project is small)
  4. Update all dry gems
  5. Install hanami overrides for ruby 3 support, version 0.0.3 and Hanami-Utils overrides
  6. Remove all ruby 3 deprecation warnings.
  7. Update Ruby to 3.0
  8. Then switch to Hanami 2 (this is yet by me to figure out, I’m currently on point 5 in our applications in Ascenda.)

More detailed guides will be released as soon as I’ll finish the complete first upgrade in Ascenda.

1 Like

Thank you for this, I’ll try following that.

Do you maybe have a WIP upgrade guide somewhere? I’d be interested in contributing to it as I’m trying to upgrade.

It currently doesn’t look good for Hanami to not have a an upgrade guide almost 4 months after the release. Especially with the link on the official guides page broken. :slight_smile:

1 Like

Let me fix this link first. I will post our team notes as a document you can contribute to after the weekend

I agree, let’s join efforts to speed it up:). I have not yet freed up a dedicated day for OSS/content creation, still on my way to do so but things are progressing in the right direction:)

I’d be very happy to help! The project is a pet project so I work on it when I have time which is not often but I am hoping to update it to 2.0 over the next 2 months and I’ll be very happy to amend and extend a guide.

I can also start it from scratch but if there is anything already written it’s going to go much faster if I contribute to that. :slight_smile:

@swilgosz you mentioned some notes you might share and that you already upgraded your models to ROM? Would it be possible to see the notes? I’ve picked up the upgrade of my (small) side project and I’m struggling a bit to switch from Hanami models to ROM since I haven’t used ROM on its own before.

1 Like