Why do changest functions have two parameters?

The following was generated automatically by ecto in the User module:

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:uid_nf])
    |> validate_required([:uid_nf])
    |> unique_constraint(:uid_nf)
  end

What is the point of having two separate parameters (user and attrs) and then casting those to user? Most examples I see online just end up calling changeset with an empty User struct, like this: Mvsg.Interaction.changeset(%Interaction{}, %{uid_nf: 1}).

Also, I can put the data on the first parameter instead of the second, and insertion into the database still works:

iex(14)> cs1 = User.changeset(%Mvsg.User{uid_nf: 37}, %{})
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Mvsg.User<>,
 valid?: true>
iex(15)> cs2 = User.changeset(%Mvsg.User{}, %{uid_nf: 38})
#Ecto.Changeset<
  action: nil,
  changes: %{uid_nf: 38},
  errors: [],
  data: #Mvsg.User<>,
  valid?: true
>
iex(16)> Repo.insert(cs1)                                 
[debug] QUERY OK db=11.3ms
INSERT INTO `users` (`uid_nf`,`inserted_at`,`updated_at`) VALUES (?,?,?) [37, ~N[2019-08-06 16:58:42], ~N[2019-08-06 16:58:42]]
{:ok,
 %Mvsg.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   id: 7,
   inserted_at: ~N[2019-08-06 16:58:42],
   uid_nf: 37,
   updated_at: ~N[2019-08-06 16:58:42]
 }}
iex(17)> Repo.insert(cs2)                                 
[debug] QUERY OK db=11.6ms
INSERT INTO `users` (`uid_nf`,`inserted_at`,`updated_at`) VALUES (?,?,?) [38, ~N[2019-08-06 16:58:44], ~N[2019-08-06 16:58:44]]
{:ok,
 %Mvsg.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   id: 8,
   inserted_at: ~N[2019-08-06 16:58:44],
   uid_nf: 38,
   updated_at: ~N[2019-08-06 16:58:44]
 }}

Looking at the changests, it seems the only difference is that cs1 stores the new data in cs1.data while cs2 stores it in cs2.changes.

It’s for updating, you pass in the existing struct. A new struct is just empty. Generally you want to pass the attributes in and not in the struct, that way the user validation can be done via the changeset functions. I.E. it’s for security.

4 Likes

Oh right, thanks. So the validations are only applied to the attributes parameter?

Correct, anything in the existing struct is assumed as already valid, as it should come from the database or so. In addition an ‘update’ call will only apply updates in the changes part of the changeset to minimize the network overhead to the database.

2 Likes

just adding more info to what @OvermindDL1 said, there are one more reason to changeset to be a arity 2 function.
This is a contract used by Ecto in general. cast_assoc and cast_embed by default try to
cast using RelationModule.changeset/2. So if you follow the contract is a little bit more simple to use cast_assoc and cast_embed, otherwise you gonna need to add an option saying which arity 2 function you gonna use for casting the relations.

3 Likes

Also you can compose changesets…

def changeset(user, attrs) do
  user
  |> cast(attrs, :uid_nff)
  |> validate_something(params)
end

def changeset_plus(user, attrs) do
  user
  |> changeset(attrs)
  |> validate_something_else(params)
end
4 Likes