achempion

achempion

Need help to refactor the resource creation code

Hi,

I’m trying to create a form with file field to upload a file. In order to store file into the proper folder I need to know resourse’s id in advance. So, I’ve split the creation transaction to two separate change-sets: changeset for the form fields and audio_changeset to upload the file.

When I call audio_changeset to update changeset and upload the file with invalid data, it changes the form state to update which changes form rendering as put action. This all happen inside the creating resource transaction. To overcome this and reset form status back to creating resource state I need to reset changeset.data.__meta__.state to nil.

It works but I think it has a better and prettier solution :slight_smile:

  def create_recording(attrs \\ %{}) do
    Repo.transaction(fn ->
      %Recording{}
      |> Recording.changeset(attrs)
      |> Repo.insert()
      |> case do
           {:ok, recording} ->
             Recording.audio_changeset(recording, attrs)
             |> Repo.update()
             |> case do
                  {:ok, recording_with_audio} -> recording_with_audio
                  {:error, changeset} ->
                    # make the form to a "new" state insted of "update"
                    meta = Map.put(changeset.data.__meta__, :state, nil)
                    data = Map.put(changeset.data, :__meta__, meta)

                    changeset
                    |> Map.put(:data, data)
                    |> Repo.rollback()
                end

           {:error, error} -> Repo.rollback(error)
      end
    end)
  end

Marked As Solved

fuelen

fuelen

try this:

  def create_recording(attrs \\ %{}) do
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:recording, Recording.changeset(%Recording{}, attrs))
    |> Ecto.Multi.update(:recording_with_audio, &Recording.audio_changeset(&1.recording, attrs))
    |> Repo.transaction()
    |> case do
      {:ok, %{recording_with_audio: recording}} -> {:ok, recording}
      {:error, _operation, changeset, _} -> Ecto.Changeset.apply_action(changeset, :insert)
    end
  end

Also Liked

david_ex

david_ex

One easy thing that will make this much more readable is

a
|> b()
|> c()
|> case do
  :foo -> nil
  :bar -> nil
end

instead of

case a |> b() |> c() do
  :foo -> nil
  :bar -> nil
end
StefanL

StefanL

You could use with statements if there are multiple failures possible.

However i think this does not fundamentally solves what you are trying to achieve. Maybe Ecto.Multi would be a better choice if you need to move files around as part of the transaction.

Its kind of hard to understand what is going on exactly in the snippet you posted. If it is one form, i guess you probably should have 1 changeset, also prepare_changes/2 allows you to run code in the transaction.

Using with instead of nested case statements could look like this: (i did not mess with the changeset)

    def create_recording(attrs \\ %{}) do
      Repo.transaction(fn ->
        with {:ok, recording} <- insert_recording(attrs),
             {:ok, with_audio} <- update_recording(recording) do
          with_audio
        else
          {:error, error} ->
            Repo.rollback(error)
        end
      end)
    end

    defp insert_recording(attrs \\ %{}) do
      %Recording{}
      |> Recording.changeset(attrs)
      |> Repo.insert()
    end

    defp update_recording(recording, attrs \\ %{}) do
      recording
      |> Recording.audio_changeset(attrs)
      |> Repo.update()
    end

If you are uploading a file and don’t want to depend on ids from the db, generate a random path for the uploaded file (eg uuid as filename). Maybe this helps you to get rid of the second changeset … You would probably still need a transaction to move/upload the file. Just be aware that storing a lot of files inside 1 directory is a performance killer.

achempion

achempion

final version

  def create_recording(attrs \\ %{}) do
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:recording, Recording.changeset(%Recording{}, attrs))
    |> Ecto.Multi.update(:recording_with_audio, &Recording.audio_changeset(&1.recording, attrs))
    |> Repo.transaction()
    |> case do
         {:ok, %{recording_with_audio: recording}} -> {:ok, recording}
         {:error, _operation, changeset, _} ->
           # make the form to a "new" state insted of "update"
           # https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/html.ex#L300
           meta = Map.put(changeset.data.__meta__, :state, nil)
           data = Map.put(changeset.data, :__meta__, meta)

           Map.put(changeset, :data, data)
           |> Ecto.Changeset.apply_action(:insert)
       end
  end

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

Other popular topics Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43657 311
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31194 112
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52774 488
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement