Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 have a form that allow people to create new artists in the database.
  • In this form there is a specialty field.
  • Before creating the artist, I want to split the specialty field by "," to store it as an array of string

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
like image 526
Cratein Avatar asked Jun 10 '16 19:06

Cratein


1 Answers

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.

like image 164
Sam Terrell Avatar answered Oct 22 '22 04:10

Sam Terrell