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
@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
#!/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
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.
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