1
votes

I want to update a field in an entry based on the existing data.

Can I define the method in the changeset pipeline? Or I need to update the data firstly, then apply the changeset?

For example, I have a user with UUID. And I need update the field with extra "0" if the length is not reached to 8.

"123" => "00000123"

I know that we can always generate the map of new data, then pass to the changeset:

user = Repo.get(User, 1)
new_uuid = "00000#{user.uuid}" 

user
|> User.changeset(%{uuid: new_uuid})
|> Repo.update()

But I am thinking if it's possible to do the logic inside the changeset pipeline? If possible, how to write the code in ?

user = Repo.get(User, 1)
user
|> <modify the data>
|> Repo.update()
3

3 Answers

3
votes

The issue with @Alekseis answer is the syntax:

%{changeset | uuid: String.pad_leading(uuid, 8, "0")}

First of all the changes would live in changeset.changes, so for example changeset.changes.uuid. But they are only there when there are actually changes for this attribute. So writing

%{changeset.changes | uuid: String.pad_leading(uuid, 8, "0")}

will not help us, since this syntax only works if the key already exists in the map. We could do something like this:

Map.put(changeset, :uuid, String.pad_leading(uuid, 8, "0")

but I recommend to use the appropriate functions from Ecto.Changeset, since a changeset might only be a struct, but there is quite some logic around it. Our friend is: https://hexdocs.pm/ecto/Ecto.Changeset.html#put_change/3

So let's rewrite the solution:

@spec prepend_zeroes_to_uuid(Ecto.Changeset.t()) :: Ecto.Changeset.t()
# There are changes on uuid but it already is 8 characters long
def prepend_zeroes_to_uuid(%{changes: %{uuid: uuid}} = changeset) 
  when is_binary(uuid) and byte_size(uuid) >= 8, do: changeset

# There are changes on uuid and it needs padding
def prepend_zeroes_to_uuid(%{changes: %{uuid: uuid}} = changeset) when is_binary(uuid) do
  put_change(changeset, :uuid, String.pad_leading(uuid, 8, "0"))
end

# There are no changes on uuid
def prepend_zeroes_to_uuid(changeset), do: changeset

# Now in your pipeline:
user
|> User.changeset(%{uuid: new_uuid})
|> prepend_zeroes_to_uuid()
|> Repo.update()
1
votes

There is no magic. Ecto.Changeset is just a struct in a nutshell and all the helper functions simply modify it, possibly adding errors and/or changing the data.

That said, you might implement the function with a @spec of a kind

@spec my_fun(Ecto.Changeset.t(), ...) :: Ecto.Changeset.t()

and apply it anywhere in the pipeline (after UUID value is set.) This signature is needed to make this function pluggable into the pipeline.

Somewhat like:

@spec prepend_zeroes_to_uuid(Ecto.Changeset.t()) :: Ecto.Changeset.t()
def prepend_zeroes_to_uuid(%{uuid: uuid} = changeset)
  when is_binary(uuid) and byte_size(uuid) >= 8, do: changeset

def prepend_zeroes_to_uuid(%{uuid: uuid} = changeset)
    when is_binary(uuid) do
  %{changeset | uuid: String.pad_leading(uuid, 8, "0")}
end

def prepend_zeroes_to_uuid(changeset), do: add_error(...)

And include it into the pipeline.

user = Repo.get(User, 1)
user
|> changeset ...
...
|> prepend_zeroes_to_uuid()
|> Repo.update()

To update the User schema directly, one might apply the same function to it.

@spec prepend_zeroes_to_uuid(User.t()) :: User.t()

The rest stays the same.

0
votes

After doing some research, I found some solution to solve my problem, but I will leave this question open and see if there is better answer.

Changeset is designed for new data or changed data. If we just get the data from params, or we have the new data already, we can use changeset to valid and update the data before push to the database.

However, it cannot be used to valid/update the existing data(i.e. valid rule changed and we need update existing data). That's my issue.

Therefore, instead of using changeset to valid data, what I do is to get the data from database, change the data, then push to changeset and update the database.

Regard to my example, we get and update the uuid to new_uuid, and pass it too changeset.

user = Repo.get(User, 1)
new_uuid = "00000#{user.uuid}" 

user
|> User.changeset(%{uuid: new_uuid})
|> Repo.update()

I am not sure if this solution is the best practice, if someone has a better solution to update the existing data, please post your answer, thanks.