Problem composing ecto changesets

I’m working on a chat-type application. There are two relevant structs %Message{} and %MessagePart{}, a Message has many MessagePart's. I am working on the ability to update a Message and change the order and content of the contained message parts.

I was hoping todo this in multiple steps which look something like this:

changeset = Message.update_changeset(%Message{}, params)
parts = get_change(changeset, :message_parts)
parts = parse_and_add_add_links(parts)
put_assoc(changeset, :parts, parts)

However when replacing an existing MessagePart with a new one I get the error:

(RuntimeError) cannot replace related %MessagePart{} because it already exists and it is not currently associated with the given struct. Ecto forbids casting existing records through the association field for security reasons. Instead, set the foreign key value accordingly

Does this mean that I need to accomplish this in two separate Repo calls? One that replaces the existing data and another that updates it? Or am I forced to generate the entire changeset all in one go instead of composing them again after the initial generation and then calling put_assoc.

Is it possible to implement the message parts as an embedded schema and to store them in a jsonb blob so that you would only have to update a single message row containing all of the parts?

Does Message.update_changeset call cast_assoc ?

If so, maybe the additional logic could be part of the changeset function in the MessagePart module?

I’d rather not change the database schema at this point. Plus we often want to filter/search based on the message parts so it makes sense for them to be their own table (and have their schema enforced by the database)

Yes it does call cast_assoc. Although my hesitation in putting all the logic in the MessagePart.changeset is that right now there is a parsing phase which I’d prefer to only do once for performance (it’s a little slow and regex based right now). But the parsing parses the body of all the message parts and outputs a list of embedded links and mentions which are then deduped and associated directly with the message itself (not the individual message parts).

So I could do the parsing phase on the params before passing the into Message.update_changeset but up until now I’ve only done minimal param munging before sending the data into a changeset. But this is the direction I’m currently leaning towards, but I’m still very open to any alternative ideas.

Just a blind question – have you tried fragmenting your procedure into several steps and wrapping them in a transactioned Ecto.Multi?

Yeah this is already one step in a larger Ecto.Multi