I'm really new to Phoenix/Elixir and I'm trying to wrap my head around changesets.
I understands it holds a set of changes that is use to either create or update a model.
What I would like to know is if and how I can modify a change before pushing it to the database.
My use case is the following :
I'm not even sure it's doable by modifying directly the changeset due to immutability constraints but i could maybe create an other changeset to insert in the repo.
Any suggestion is welcome and don't hesitate to point bad practices or stupid things i might be doing!
EDIT following comment : I'm looking at something like :
defp put_specialty_array(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{specialty: spec}} ->
put_change(changeset, :specialty, String.split(spec, ","))
_ ->
changeset
end
end
I believe what you are looking for is a custom Ecto.Type. I do this all the time, and it works great! It would look something like this:
defmodule MyApp.Tags do
@behaviour Ecto.Type
def type, do: {:array, :string}
def cast(nil), do: {:ok, nil} # if nil is valid to you
def cast(str) when is_binary(str) do
str
|> String.replace(~r/\s/, "") # remove all whitespace
|> String.split(",")
|> cast
end
def cast(arr) when is_list(arr) do
if Enum.all?(arr, &String.valid?/1), do: {:ok, arr}, else: :error
end
def cast(_), do: :error
def dump(val) when is_list(val), do: {:ok, val}
def dump(_), do: :error
def load(val) when is_list(val), do: {:ok, val}
def load(_), do: :error
end
Then in your migration, add a column with the right type
add :tags, {:array, :string}
Finally in your schema specify the field type that you created.
field :tags, MyApp.Tags
Then you can just add it as a field in your changeset. If cast of your type returns :error
, then the changeset will have an error something like {:tags, ["is invalid"]}
. You don't have to worry about any processing of the field in your model or controller then. If the user posts a string array for the value or just a comma separated string, it will work.
If you need to save the value to the database in a different format, you would just change the return value of def type
and ensure that def dump
returns a value of that type and that def load
can read a value of that type to whatever internal representation you want. One common pattern is to define a struct for the internal representation so that you can make your own implementation of Poison's to_json
that could even return a simple string. One example might be a LatLng type that encodes to 12.12345N,123.12345W
in json, stores as some GIS type in postgres, but has a struct like %LatLng{lat: 12.12345, lng: -123.12345}
that lets you do some simple math in elixir. The DateTime formats work a lot like that (there is a struct for elixir, a tuple format for the db driver and an ISO format for json).
I think this works really well for password fields, btw. You can squash the JSON representation, use a struct to represent the algorithm, parameters to the algorithm separate salt from hash or whatever else makes life easy. In your code, to update a password, it would just be Ecto.Changeset.change(user, password: "changeme")
.
I realize this is a 6mo old question and you've probably found something, but I ended up here from a google search, and assume others will, too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With