Cratein
Cratein

Reputation: 453

How to modify an Ecto changeset before inserting it into the repo?

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

Upvotes: 12

Views: 6977

Answers (1)

Sam Terrell
Sam Terrell

Reputation: 161

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.

Upvotes: 15

Related Questions