Data mapping and validation - in practice

Hello everyone,

Hoping to get some help/guidance with some questions relating to practically implementing using embedded schemas as mappers for the UI, as described here: Data mapping and validation — Ecto v3.12.5

The article explains the concept well & it makes a lot of sense to me, but i dont think the examples give much insight to some of the complexities involved.

  1. The example with the accounts / profile, the transaction to insert the data doesnt appear to link it at all i.e. the accounts and profiles are being inserted entirely isolated from each other into their respective database tables. In practice the profile table should link to the user_id / key of the accounts table. I believe this can be done through a multi, or cast_assoc or something similar / combination of the two via a transaction.
  2. Following insertion, managing the response when the dataset that was inserted does not match the embedded schema. Validations can be done on the changset prior to massaging/inserting the data accordingly, but should there be errors on the insert relating to constraints etc how do you return those errors to the UI? Is the intention to write some wrapper functions to take the errors from the {:error, changeset} & add them to the changeset being used to generate the UI form for rendering the output errors/messages?
    I still need to look at how the errors are stored in changsets for associations but on the face this seems like it could be quite complex.

Is this a common practice, & how have others handled this in their web applications?

Thanks a lot !!

Daniel

3 Likes

Sorry, my answer addresses only a part of your question and it’s speculative. But I thought I’d like to share my thoughts anyway (and maybe get some feedback).

The link (or reference in the database) might be missing by intention. This way one could keep the accounts and the profiles contexts decoupled from each other. The accounts context wouldn’t need to know about the profiles context at all. The profile would just have some account id value and the connection would have to be resolved by a service-like layer/module that depends on both contexts. For the business logic of the profiles context one only needs an account id value, not an account entity, so changes in the accounts context (or the accounts schema) wouldn’t affect the profiles context.

1 Like

Thanks for the perspective Jan :slightly_smiling_face:

I think thats fair & i can see where that might be appropriate - Regardless whether same context or not, in this siutation should the inputs from one table (i.e. ID/key) be required for the insert of another, what is everyones go to pattern for this, using a multi or using cast/put_assoc?

Point 2 is the piece im less clear on for sure, i’d like to avoid an ‘anti-pattern’ & hear ideas/examples from experience on how this should be approached ideomatically for liveview forms/changsets

Thanks again

Daniel

Yes. The expectation here would be that you map the errors back. I’d argue that’s unavoidable complexity and given this really is about db constraints only still limited in scope. There’s not much ecto can do here given the mapping between schemas is arbitrary.

1 Like

Hey all,

figured i’d loop back now i’ve had a bit time tonight to mess around with this, below essentially how i seem to be able to handle this succesfully;

to avoid creating a new function/repeating similar logic for each new insert with different tables/associations, using multi to insert one table at a time & inherit the relevant fields from the previous insert. This way if constraint errors are encountered its directly in the failed_value of the Ecto.Multi.failure() thats returned by the transaction & not nested like you’d see if you used put/cast_assoc.

on the liveview side, i literally rebuilt the changeset from the params then used a function to take the errors returned from the multi & add them back to the original changset alongside setting valid? to false & action to validate

i created a throwaway project to mess around with purely for testing this & made the code fairly verbose with prints etc but below some snippets to illustrate. When applying this to my new project i’ll create a new module with helper functions like used here to be used across the board.

Thanks

Daniel

insert function;

def register_user_multi(attrs) do
    %Register{}
    |> Register.changeset(attrs)
    |> register_multi_insert()
    |> Repo.transaction()
  end

  defp register_multi_insert(changeset) do
    user = Register.to_user(changeset)
    profile = Register.to_profile(changeset)

    Ecto.Multi.new()
    |> Ecto.Multi.insert(:user, user)
    |> Ecto.Multi.insert(:profile, fn %{user: user} ->
      user_id = user.id
      put_change(profile, :user_id, user_id)
    end )
  end

liveview save function;

def handle_event("save", %{"user" => user_params}, socket) do
    case Accounts.register_user_multi(user_params) do
      {:ok, _} ->
        changeset = Accounts.change_user_registration(%Register{}, user_params)
        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}

      {:error, failed_operation, failed_value, changes_so_far} ->
        IO.inspect(failed_operation)
        IO.inspect(failed_value)
        IO.inspect(changes_so_far)
        changeset = Accounts.change_user_registration(%Register{}, user_params)
        changeset_with_errors = bubble_errors(changeset, failed_value)
        IO.inspect(changeset_with_errors)
        {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset_with_errors)}
    end
  end

bubble errors function:

defp bubble_errors(original_changeset, failed_value) do
    errors = failed_value.errors
    ce = %{original_changeset | errors: errors}
    cv = %{ce | action: :validate}
    %{cv | valid?: false}
  end