Elliot Larson
Elliot Larson

Reputation: 11049

Elixir Ecto adding computed value only on create

What is the best practice approach for adding a computed value on create/insert? Should I create a unique changeset for both create and update?

Say, for example, I have a blog post model, and I want to create a title slug value and store it. This is a bit contrived, but say for some reason I only want to set it on create and not update. Should I do something like the following?

defmodule MyBlog.Post do
  use MyBlog.Web, :model

  schema "posts" do
    field :title, :string
    field :title_slug, :string
    field :content, :text

    timestamps
  end

  @required_fields ~w(
    title 
    content
  )

  @optional_fields ~w()

  def create_changeset(model, params \\ :empty) do
    changeset(model, params)
    |> generate_title_slug
  end

  defp changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

  defp generate_title_slug(changeset) do
    put_change(changeset, :title_slug, __some_slug_generation_code__)
  end

  def update_changeset(model, params \\ :empty) do
    changeset(model, params)
  end
end

Upvotes: 3

Views: 1789

Answers (3)

manukall
manukall

Reputation: 1462

This might not help with your real use case, but in the slug example it could also make sense to just keep the single changeset function, but check if a slug is present and only if not generate a new one.

Otherwise I agree with michalmuskala. Prefer separate changeset functions over callbacks whenever possible.

Upvotes: 0

michalmuskala
michalmuskala

Reputation: 11278

I strongly discourage callbacks - they are hard to test, introduce global state, are obscure, and are difficult to reason about. This also goes against one of the core Elixir's principles: "explicit is better than implicit".

The Ecto's core team is even considering getting rid of callbacks, or changing the name and making them less exposed. Using a callback should be a last resort option, when nothing else is possible.

To showcase what is one of the issues with callbacks, let's imagine a scenario where you indeed used a callback to solve this problem. And now you're designing an admin interface where you don't want to have this behaviour. How do you solve this? You start going down the rabbit hole of disabling callbacks, introducing exceptions upon exceptions, and have a hard to follow, multi-branch conditional logic. But this is solving a wrong problem all together!

The different changeset approach is perfectly fine and very natural regarding Ecto's architecture. This way you can have different validations for different actions and nothing is global. Let's think how you would solve the issue in the scenario I showcased earlier. It's extremely simple - you create another changeset function!

A solution I've seen couple of times is to change the changeset function to take three arguments and pattern match on the type in the first one, e.g.:

def changeset(action, model, params \\ :empty)

def changeset(:create, model, params)
  # return create changeset
end

def changeset(:update, model, params)
  # return update changeset
end

I'm not sure which is better - multiple functions or pattern matching in one function. This is mostly question of preference.

Upvotes: 7

AbM
AbM

Reputation: 7779

You can use Ecto.Model.Callbacks. In your case, the best callback would be before_insert that runs after validating the changeset, but before the changeset is inserted into the Repo:

defmodule MyBlog.Post do
  use MyBlog.Web, :model

  schema "posts" do
    field :title, :string
    field :title_slug, :string
    field :content, :text

    timestamps
  end

  @required_fields ~w(
    title 
    content
  )

  @optional_fields ~w()

  before_insert :generate_title_slug

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

  defp generate_title_slug(changeset) do
    put_change(changeset, :title_slug, __some_slug_generation_code__)
  end

end

And now both your create and update actions will call changeset

Upvotes: 1

Related Questions