What's the right way to set an attribute for a Hanami Struct (ROM::Struct)?

Hey :wave: ,
I ask this because in certain situations it may be necessary to set i.e. a relation property for an object without reloading it from the DB.

# relation
module MySlice
  module Relations
    class Orders < MySlice::DB::Relation
      schema :orders, infer: true do
        associations do
          has_many :add_ons
        end
      end
    end
  end
end

# struct
module MySlice
  module Structs
    class Order < MySlice::DB::Struct
    end
  end
end
# pseudo-code
transaction do 
   order = step create_order
   order = step create_addons(order)
end

def create_add_ons(order)
  # after add_on creation, I do not want to reload
  # the order with the included add_ons

  # Approach 1
  # The following should actually be possible with dry struct, but does not
  # cause the order-object was not loaded with combine(:add_ons)
  order.new(add_ons: created_add_ons)

  # Approach 2
  # feels cumbersome
  # Furthermore the Order struct has to be changed to make this work
  order.add_ons = created_add_ons
  order
end

Do you know another way?
Thanks

ROM structs are immutable structures, which means making a change to their attributes is copy-on-write.

This will not work, because the struct does not allow mutations.

order.add_ons = created_add_ons

This is close, but you discard the new order.

order.new(add_ons: created_add_ons)

Instead, do this:

order = order.new(add_ons: created_add_ons)

Thanks for your response.
Discarding the order variable is only a problem in pseudo code. It does not work even if it is not discarded. add_ons is a relation that can be loaded with combine. The order class here has no add_ons attribute, so the new order also contains no add_ons. Do you think this could be changed at ROM::Struct?

Okay I believe I understand your question now. You want to take one struct schema, an “order”, and produce an entirely new schema that contains “add_ons” which are a relational association. In ROM’s design, these are considered entirely different structs.

This should absolutely be possible, since this is literally what Repository does. I will have to think about it and look into the code though. I have not actually done this myself.

In the interests of treating these structs as generic DTOs in the system, I believe this is a pretty solid use-case that will likely come up again.

The process of turning a Relation dataset into a struct is very, very complicated. I don’t think it’s fruitful to attempt using that plumbing directly.

But in the end, all ROM structs are just dry-struct objects. So here’s how we might approach it from that direction:

order_with_addons = Class.new(order.class) { attribute :add_ons, Types::Array.of(ROM::Struct::AddOn) }
order = order_with_addons.new({ **order, add_ons: created_add_ons })

Thanks for taking a look at this.

Do you see a good way to integrate these features into hanami-struct, or should this be left in the user area?

I also believe that some people will come across this use case. An elegant solution would be great.

For now, to work around this, I load the created order combined with all relations, I want to set in the flow. This works quite well.

Perhaps @solnic can weigh in on how ROM was intended to work in this case, before we go so far as to build an abstraction for it.

I recommend thinking about changing data separately from reading data. If you want to write something to the database, you want to use a separate data representation. Using plain hashes with changesets should suffice.

Thanks @solnic for your input.

This is not about writing the data back to the DB. It is about the possibility of using a struct in such a way that data is added to it that is relevant in the further flow.

Using a hash may be a way out here, but the struct offers many advantages.

# pseudo-code
transaction do 
   order = step create_order
   order = step create_addons(order)
   # use enriched order in further flow
end

def create_addons(order)
  # create addons and return order with addons
  order.new(addons: created_addons)
end

If we were querying an order from the db, it would be simple:

order = orders_repo.orders.by_pk(id).combine(:addons).one

The question, as I understand it, is: what if we are creating these two records separately, but want to combine them into one struct after-the-fact?

It seems less than ideal that we should have to make another round-trip to the database when we already have the data, we just need to combine it outside the context of a dataset.

There is a further need for this because changesets don’t support combines presently.