Before create callback in Ecto

I’m trying to emulate rails before_create callback in Ecto. In rails it looked like
before_create :set_deliver_at. And it just copy :created_at field before creation.

  def set_deliver_at
    self.deliver_at ||= created_at
  end

Is there something close to this behavior in Ecto?
Currently I’ve stopped on decision with two query: one for creation and the second - for updating field

Ecto does not provide ActiveRecord-like callbacks.

The recommendation is to replace callbacks with Ecto.Multi. You could wrap your two queries into an Ecto.Multi transaction, so either everything goes properly or everything fails.

1 Like

But how can I know value from created_at field before storing?

You cannot really compare AR and Ecto… There is no callback. But You do it in a different way.

For example in your case, if You want to set deliver_at automatically, You can create a function, and pipe the changeset into it. Like this.

  defp generate_deliver_at(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{created_at: created_at}} ->
        put_change(changeset, :deliver_at, created_at)
      _ ->
        changeset
    end
  end

By the way, created_at is inserted_at in Ecto.
In your changeset…

    any
    |> changeset(attrs)
    |> cast(attrs, @fields)
    |> validate_required(@fields)
    |> generate_deliver_at()

For after callback, You can use Ecto.Multi, to wrap multiple operations in a single transaction. For example this could be the way to implement acts_as_list, or counter_cache in AR.

Coming from Rails too, this was one of the biggest change I had to deal with (AR vs Ecto).

PS. You can access changeset struct and update as You like before calling Repo, this enable You to transform the data just before sending it to the DB. The Repo pattern is not present in AR.

4 Likes

There is this video that might help You better grasp the transition between Rails and Phoenix.

2 Likes

That is the way I learned to do it.

To make it a bit more before_create instead of before_update I change it just a little bit though

defp set_deliver_at(%{valid?: false} = changeset), do: changeset
defp set_deliver_at(%{valid?: true} = changeset) do
  if is_nil(get_field(changeset, :deliver_at)) do
    put_change(changeset, :deliver_at, created_at)
  else
    changeset
  end
end
2 Likes

Thanks, this is quite clear for me. But in your cases you have to set :inserted_at manually before saving. So I combined your answers and wrote this chain:

  def create_changeset(attrs) do
    __MODULE__.__struct__
    |> change(attrs)
    |> set_created_at
    |> set_deliver_at
  end

  def set_created_at(changeset) do
    if is_nil(get_field(changeset, :created_at)) do
      put_change(changeset, :created_at, DateTime.utc_now)
    else
      changeset
    end
  end

  def set_deliver_at(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{created_at: created_at}} ->
        put_change(changeset, :deliver_at, created_at)
      _ ->
        changeset
    end
  end

I really like this way to write functional code. It always make me remember Another Glitch in the Call: http://www.poppyfields.net/filks/00020.html

“We don’t need no flow control”

:joy:

1 Like