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.
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.
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
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