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?

2 Likes

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.