Difficulty setting up around-step with dry-transaction

Hi there, following this guide using Rails 7 and Ruby 3.2 we are running into an exception on extending MyContainer:

`extend': wrong argument type Class (expected Module) (TypeError)

Our setup so far only contains 2 files:

# app/services/dry_service.rb
class DryService
  include Dry::Transaction(container: MyContainer)

  around :transaction, with: 'transaction'

  step :create_user
  step :notify_user

  private

  def create_user
  end

  def notify_user
  end
end

and

# app/services/my_container.rb
class MyContainer
  extend Dry::Container

  register "transaction" do |input, &block|
    result = nil

    begin
      ActiveRecord::Base.transaction do
        result = block.(Success(input))
        raise ActiveRecord::Rollback if result.failure?
        result
      end
    rescue ActiveRecord::Rollback
      result
    end
  end
end

What are we missing?

Try making MyContainer a module instead of a class.

@bkuhlmann thanks for reaching out! Changing MyContainer from class to module results in the same issue - the exception is raised on the extend on the 2nd line.

Yeah, no problem. Actually, I was too hasty in reading your code snippet originally. Try using extend Dry::Container::Mixin on Line 2. I believe that’s the syntax error that is tripping you up.

No worries I appreciate your help. Different exception now:

undefined method "Success" for MyContainer:Class (NoMethodError).

Did we miss some major step from the docs on how to setup and use dry-transaction? dry-transaction gem is supposed to work without any other dry-rb gems (except for its dependencies of course) on Rails 7, right?

So the error you are seeing now is not related to Dry Transaction, it’s with your container still. You need to include Dry Monads in order to resolve the Success no method error in your container. So a bit like this (truncated for brevity):

require "dry/monads"

class MyContainer
  include Dry::Monads[:result]
end

Still the same issue unfortunately.

Here’s a working example

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'activerecord', '~> 7'
  gem 'dry-transaction'
  gem 'sqlite3'
end

require 'active_record'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')

ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define version: 1 do
  create_table :users do |t|
    t.string :email, null: false
    t.datetime :deleted_at
    t.timestamps
  end
end

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

class User < ApplicationRecord
  validates :email, presence: true
end

require 'dry/transaction'

M = Dry::Monads

class ValidationError < StandardError
  attr_reader :result

  def initialize(result)
    @result = result
  end
end

module MyContainer
  extend Dry::Core::Container::Mixin

  register "transaction" do |input, &block|
    result = nil

    ApplicationRecord.transaction do
      block.(M::Success(input)).or do |failure|
        raise ValidationError, failure.value
      end
    end
  rescue ValidationError => err
    M::Failure(err.result)
  end
end

class DryService
  include Dry::Transaction(container: MyContainer)

  around :transaction, with: 'transaction'

  step :create_user

  def create_user(attrs)
    user = User.new(attrs)

    if user.valid? && user.save
      Success(user)
    else
      Failure[:validation, user]
    end
  end
end

case DryService.new.({ email: nil })
in M::Success(user)
  puts "Successfully created user for #{user.email}"
in M::Failure[type, User => user]
  warn "Error: #{type}: #{user.errors.full_messages.inspect}"
end

Based on some context clues in your example, I suspect you are working with an older version of Ruby and/or Dry::Transaction, so specifics there might change the code slightly.

There are a couple things I want to note:

Raising ActiveRecord::Rollback and then rescuing it outside the transaction would never work, because the entire point of this exception that it is silently handled by the transaction and not reraised.

If you want to trigger a rollback from an exception in this way, you need your own exception.

Second, I do want to point out how much simpler this flow becomes using dry-monad’s do-notation:

class DryService
  include Dry::Monads[:result, :do]

  def call(attrs)
    ActiveRecord::Base.transaction do
      yield create_user(attrs)
    end
  end

  private

  def create_user(attrs)
    user = User.new(attrs)

    if user.valid? && user.save
      Success(user)
    else
      Failure[:validation, user]
    end
  end
end

Thanks! So do-notation can be considered as a replacement, or at least an alternative to dry-tranaction?

Dry-Transaction was going to be EOL’d but based on feedback, kept around. But it’s safe to say it’s on the back burner.

I started with Dry-Transaction and moved to 100% Dry-Monads w/ do-notation; I can say with confidence that everything you did with transactions, you can do better with monads.

For instance, dependency injection isn’t currently compatible with it. It’s an older, more rigid implementation of ideas that have clearly come to fruition in other Dry gems.

2 Likes

Hi @alassek thanks for your input, while I was waiting for a response I also found do-notation of dry-monads, which looks really promising, currently testing it :slight_smile:

1 Like