script
script

Reputation: 2167

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

Upvotes: 1

Views: 535

Answers (1)

tkowal
tkowal

Reputation: 9289

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.

Upvotes: 1

Related Questions