Search code examples
elixirecto

Building changeset for entire association


I read about cast_assoc/3 here .But documentation looks confusing. I want to build single changeset for the entire association and execute it in one transaction for update. Here are my models ;

    defmodule User do      
     use Gallery.Web, :model


     schema "users" do   
       field(:name, :string)
       field(:occupation, :string)
       has_many(:paintings, Painting)
     end

    def changeset(struct, params \\ %{}) do
     struct
     |> cast(params, [ :name, :occupation ])
     |> validate_required([:name, :occupation]) 
    end
   end

   defmodule Painting do      
     use Gallery.Web, :model


     schema "paintings" do   
       field(:name, :string)          
       belongs_to(:users, User)
     end

    def changeset(struct, params \\ %{}) do
     struct
     |> cast(params, [ :name ])
     |> validate_required([:name]) 
    end
   end

This is the data i want to build a single changeset of

data= %User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   id: 4606,
   name: "Test",
   occupation: "Artist",
   paintings: [
     %Painting{
     __meta__: #Ecto.Schema.Metadata<:loaded, "paintings">,
     user_id: 4606,
     id: 1515,
     name: "philip"
     },
   %Painting{
    __meta__: #Ecto.Schema.Metadata<:loaded, "paintings">,
    user_id: 4606,
    id: 1516,
    name: "john"
    }
  ]
 }

Any suggestions?

Thanks


Solution

  • For changesets to work your data needs to be plain maps and not structs (as if you got it from params).

    If you want to just insert user with multiple paintings you need to:

    • get rid of structs
    • get rid of ids (in case of inserts they are created on the fly)
    • have a cast_assoc in user changeset

    Like this:

    data = %{
      name: "Test",
      occupation: "Artist",
      paintings: [
        %{
          name: "philip"
        },
        %{
          name: "john"
        }
      ]
    }
    
    %User{}
    |> User.changeset(data)
    |> Repo.insert
    

    If you want to also update stuff in this way, it gets more complicated. It is not clear if the list of paintings in data should update existing paintings in place, add new ones or delete all previous ones and replace them with those in data. I personally wouldn't recommend nested changesets for updates. https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3

    UPDATE AFTER CLARIFICATION:

    To update all the paintings in place you need to do two more things. You need to:

    • preload the paintings
    • have the painting id in data

    Like this:

    data = %{
      name: "Test",
      occupation: "Artist",
      paintings: [
        %{
          id: 1,
          name: "philip"
        },
        %{
          id: 2,
          name: "john"
        }
      ]
    }
    
    User
    |> Repo.get_by(id: user_id)
    |> Repo.preload(:paintings)
    |> User.changeset(data)
    |> Repo.update
    

    You don't need to use Multi. It will be one transaction. Using Repo module once usually indicates one database operation.

    All the magic happens in the paintings: [...]. According to docs you have four cases:

    • If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
    • If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
    • If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
    • If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)

    You are interested in case the third case for updating in place. If you don't pass all the paintings in data you may be also interested in fourth one.