How do you handle multiple changes without using multiple database requests?

How do y’all handle multiple changes without using multiple database requests. This is something I came up with but I am wondering if there’s already a solution I just haven’t found.

I made a fancy diagram but I am not allowed to post it. :frowning:

The Draft module implements an optimistic UI pattern where changes are buffered in memory and persisted as a single, atomic unit.

Component Description
%Draft{} The Draft stores the original data in source and keeps track of mutations in the changes field.
Draft.Diff Compares the difference between the original source (initial state) and changes (current state).
Draft.Validations Use Ecto Changesets to validate data before they are applied to the changes map. Batched operations are checked as a group, if any fail, the entire batch is rejected with errors collected for the UI.
Draft.Commit Atomic persistence, translates the list of ordered mutations into an Ecto.Multi. Handles tmp_id resolution for newly inserted entities.

I apologize if this seems rude, I’m just trying to gauge what it is that you’re trying to solve or get advice on , but the shape of what you describe seems basically the same as a changeset in ecto, or any of the other parsing or validation or schema libraries such as Drops, or Zoi, or …

If it’s something that needs to be persisted before it’s committed to the main entity, then I have a concept of it in the database and it’s just a matter of whether I store a map of some embedded schema, or something more formal.

If it does not need to be persisted or a process dying like a liveview refreshed for example, then I just store it in the socket or whatever mechanism of process state I’m using and updated or recompute it with every new set of changes until it’s ready to be applied. I tend to only use an extra changeset for very simple one or two step flows, as it’s way too easy to reuse a changeset twice which is a No-No. Usually if it’s just a little bit bigger than that and I want to protect myself from messing up. I will receive the form, turn it into a changeset if it’s valid, and if they don’t want to apply it yet, then I will just capture the params they sent or the cast attributes and then just append when they want to add another thing. So I basically I don’t keep the changeset. I just use it to make a decision and then I throw it away or keep it only for calling to_form.

Once I get into UI “wizard” territory or some sort of state machine, I make a custom reducer module and then decide how to save and restore the state if I need to.

No need to apologize; I’m a big boy. :smile:

Context: User Interface/User Experience

Constraint: I do not want to rely on a javascript framework. I don’t want to jump onto another stack when things get tough.

The UI is a drag and drop program editor. It’s a full page 7 day grid. I don’t want to send a database request to store every single change.

  • Drag-and-drop: Workouts can be dragged within and between day columns.
  • Sessions: Workouts can be combined into sessions - A, B, C, D, etc.
  • Workout Card Editor: A popover appears in order to edit the workout.
  • Workout Forms: The popover is its own form to create or edit a workout.

As you can imagine a UI like this can have multiple changes, deletions, and additions.

Everything else works, it’s just the UI is buggy as a fallen decomposing tree. At first I was storing changes as deltas:

{:add_exercise, %{"workout_id" => 1, "session" => "A", "day" => 1, "exercise_id" => 3, ...}} 

But now I am just diffing between original source and changes.

Oh dang, yeah that is a very complex problem space.

I can see how just doing deltas would be difficult, especially if the delta you have is not clearly mapped to an action or set of actions that you have to disambiguate such as dragging a workout from one day to another and combining it into a session of several others and potentially then editing them.

I think if I were approaching this I would have a hard a time determining when I stop and persist since you’re at the mercy of the combinations that they want to do and arbitrarily deciding " when this really complicated thing happens we recompute the next state and persist" could arbitrarily limit flexibility.

So I think I would probably either have a library of actions that can be done atomically then clever ux to make it apparent what is and is not possible at any given point in time…

OR I would do that same ux work but let the decision of when to do something about an action that a user takes be separated from the part of the application that ensures what the next actions can be or what the current state of the world is.

That’s a circuitous way of saying if this was my whole product or a significant portion of it, I would probably do an event sourced approach, and likely do CQRS. Based on the current state/aggregate that would determine the kinds of things they could do.

I’m imagining that if they dragged a workout from one day to another. You would look at the current state/aggregate of their calendar to determine what to show on the UI as a hint, but the thing you would emit would be an event saying they dragged an existing item to a new time slot, maybe with some metadata or a more specific event to denote that there was nothing seen in that time slot to the user so you that you can preserve intent in case some event popped into that time right around when they released it.

It would be processed, and a command or commands could be emitted that would later in the projector move it or delete the old and make a new one in the same transaction. The benefit also is that depending on how you structure the pieces, and aggregates, you could have an aggregate that represents a partially completed, edit or merge or whatever of the example from before where you drag it to an existing slot on another day in a way that means “combine these into a workout session but let me edit it first”. With an aggregate representing available time slots you could model that the time slot is not available or is pending an edit but is not finalized until the finalizing event happens. This would be a separate aggregate from the one that would be used to model the series of edits to that workout session itself.

I’m intentionally making something more complicated to show with flexibility, but if I were doing this I would probably go with the “persist on every action” pain (plus I imagine all of the optimistic writes and fallback logic needed if there was multiple calendars involved) while I figured out the right aggregates and events.

I’ve used commanded and liked it but I will say it is difficult to wrap your head around if you are new to CQRS, and I’ve only used it in production a couple of times. Once where the problem did not need it but did need basic event sourcing, and another time where it was absolutely vital and would not have worked without it.

There are also some other good event sourcing libraries, but for me it’s the separation of concerns and the ability to model the true problem that would make CQRS appealing

Sorry for the voice to text wall of text.