joaquinalcerro

joaquinalcerro

Handling nested associations with cast_assoc

Hi everyone,

I finally had a bit of time to followup on this post: Best way to handle associations in single html form

In my previous post I just wanted to create a handle a nested association in the same html form. Specifically to create the parent and child in one go.

In this post I want to go a little bit further and try to explain to new comers how I manage to implement a full CRUD for this scenarios.

Please feel free to comment in this post how did you handle this scenario.

You can find all the code for this implementation in bitbucket here.

Scenario:
Handle a nested (has_many) association in the same html form.
Schema: Invoice --> has_many --> details

Key points:

  1. You are using one single page form to handle both parent and child associations.
  2. When creating the association for the first time you don’t have the parent’s id yet so you have to save the parent’s part fist then use its id to associate each child.
  3. I will be using drab library to add fields to the html form dynamically. So you will have to add it to your project.
  4. I will use cast_assoc to handle the update and delete of the nested association.

Create

To keep this post as short as possible, please refer to my previous post where I coverd the create action only. You can find it here.

Update

Please refer to the cast_assoc official documentation for further reference but I will enumerate some important points:

  1. cast_assoc requires you to preload the nested associations records

     def get_invoice!(id) do
       Repo.get!(Invoice, id)
       |> Repo.preload(:details)
     end
    
  2. Based in the id number cast_assoc will set the changeset action accordingly (insert, update, delete, replace) and the database will follow.

  3. cast_assoc will handle all the database insert, update and delete actions.

So how does cast_assoc works. Lets see the code:

def update_changeset(%Invoice{} = invoice, attrs) do
invoice
  |> cast(attrs, [:provider])
  |> validate_required([:provider])
  |> cast_assoc(:details, with: &Multipartform.Documents.Detail.update_changeset(&1, &2, invoice))
end

Note that cast_assoc/3 takes 3 arguments: the changeset, the nested association as atom and a function. cast_assoc will invoque the function for each child record. The function must return a changeset of Details for each child record that is received in the attrs[“details”].

Also note that this function has three arguments. The first is the %Details{} struct and the second is the corresponding attributes in attrs[“details”] based on the id. So cast_assoc matches the id’s and calls the function with the arguments. Lets see the code of this function:

  @doc false
  def update_changeset(%Detail{invoice_id: nil}, attrs, invoice) do
    Ecto.build_assoc(invoice, :details)
    |> cast(attrs, [:tax, :code, :desc, :up, :invoice_id])
    |> validate_required([:tax, :code, :desc, :up, :invoice_id])
  end
  
  @doc false
  def update_changeset(%Detail{} = detail, attrs, _invoice) do
    detail
    |> cast(attrs, [:tax, :code, :desc, :up, :invoice_id, :delete])
    |> validate_required([:tax, :code, :desc, :up, :invoice_id])
    |> mark_for_delete()
  end

  @doc false
  defp mark_for_delete(changeset) do
    if get_change(changeset, :delete) do
      %{ changeset | action: :delete }
    else
      changeset
    end
  end

Two important thing to note:

  1. We are pattern matching on the first argument of the function header (Detail Struct). This argument is the record we preloaded. If there is no matching id means that the record found is new and this argument is initialized with nil values.
  2. When the invoice_id is nil in the struct means that the record is new and we have to build the association between the invoice and the detail.

Delete

To delete a nested association you just have to delete the entire record in the html form so it won’t be part of the attrs[“details”]. cast_assoc will delete the record form the database. This is a straight forward operation but because it is a destructive operation the recommended way is to mark the record for delete and change the changeset action to delete. This lets you do something else with the data insted of just deleting it.

Note that for each record we check the changeset for the key “delete” in the changes and if so we change the changeset action to delete.

I hope this helps explanation helps.

Best regards

Most Liked

joaquinalcerro

joaquinalcerro

So cast_assoc takes a function, in this case, Multiplatform.Documents.Detail.update_changeset/3. This functions must return a changeset and will be used to match the preloaded record from the database against the params posted in the html form.

I use the capture operator “&” to facilitate the reading. &1 refers to the first argument and &2 to the second. The first argument will be the data record loaded from the database and the second will be the data form the params posted. Both are passed to the function and returns a changeset with the proper action added. The action of a changeset might be:

action() :: nil | :insert | :update | :delete | :replace | :ignore

Thats why you have the following function header with three arguments:

def update_changeset(%Detail{} = detail, attrs, _invoice) do

Depending in the changeset’s action, cast_assoc will create, update, delete the record in the database or just ignore it.

Hope this clarifies it.

emanuelturis

emanuelturis

Thank you, doing update_changeset(&1, &2…) and additional arguments allowed me to pass data to the changeset in a cast_assoc. Great explanation!!

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
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
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New

We're in Beta

About us Mission Statement