Elixir Ecto добавляет вычисляемое значение только при создании

Каков наилучший подход к добавлению вычисляемого значения при создании/вставке? Должен ли я создать уникальный набор изменений как для создания, так и для обновления?

Скажем, например, у меня есть модель поста в блоге, и я хочу создать значение заголовка и сохранить его. Это немного надумано, но скажем, по какой-то причине я хочу установить его только при создании, а не при обновлении. Должен ли я сделать что-то вроде следующего?

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

person Elliot Larson    schedule 25.11.2015    source источник


Ответы (3)


Я решительно не одобряю обратные вызовы — их трудно тестировать, они вводят глобальное состояние, неясны и о них трудно рассуждать. Это также противоречит одному из основных принципов Elixir: «явное лучше, чем неявное».

Основная команда Ecto даже рассматривает возможность избавиться от обратных вызовов или изменить название и сделать их менее заметными. Использование обратного вызова должно быть последним средством, когда ничего другого не возможно.

Чтобы продемонстрировать, в чем заключается одна из проблем с обратными вызовами, давайте представим сценарий, в котором вы действительно использовали обратный вызов для решения этой проблемы. И теперь вы разрабатываете интерфейс администратора, в котором вы не хотите иметь такое поведение. Как решить эту проблему? Вы начинаете спускаться по кроличьей норе, отключая обратные вызовы, вводя исключения за исключениями, и у вас есть сложная условная логика с несколькими ответвлениями. Но это решение неправильной проблемы все вместе!

Другой подход к набору изменений прекрасно подходит и очень естественен для архитектуры Ecto. Таким образом, вы можете иметь разные проверки для разных действий, и ничто не является глобальным. Давайте подумаем, как бы вы решили проблему в сценарии, который я продемонстрировал ранее. Это очень просто — вы создаете еще одну функцию набора изменений!

Решение, которое я видел пару раз, состоит в том, чтобы изменить функцию changeset, чтобы она принимала три аргумента и соответствовала шаблону для типа в первом, например:

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

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

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

Я не уверен, что лучше - несколько функций или сопоставление с образцом в одной функции. В основном это вопрос предпочтений.

person michalmuskala    schedule 25.11.2015
comment
Что касается нескольких функций или сопоставления шаблонов для наборов изменений, это одна из причин использования разных функций, чтобы их можно было документировать отдельно. Это позволяет вам объяснить, почему существует другой набор изменений при обновлении вместо создания. - person Gazler; 25.11.2015
comment
Я провел достаточно времени, ощущая боль обратных вызовов в Rails, поэтому обычно стараюсь их избегать. Хотя я обнаружил, что с ними все в порядке при выполнении простых манипуляций с данными. Меня действительно укусили обратные вызовы, которые каким-то образом связаны с другими моделями или процессами... болезненно! Спасибо за ваше предложение. Мне нравится идея быть более явным, и идея использования одной и той же функции с разными аргументами сопоставления с образцом... все еще осваиваю это в Эликсире. :) - person Elliot Larson; 25.11.2015

Вы можете использовать Ecto.Model.Callbacks. В вашем случае лучшим обратным вызовом будет before_insert, который запускается после проверка набора изменений, но до того, как набор изменений будет вставлен в репозиторий:

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

И теперь ваши действия по созданию и обновлению будут вызывать changeset

person AbM    schedule 25.11.2015
comment
Я разделяю отвращение Михалмускалы к обратным вызовам. Однако я обнаружил, что они подходят для простых случаев использования, подобных этому. Спасибо за ваш ответ. - person Elliot Larson; 25.11.2015

Это может не помочь в вашем реальном случае использования, но в примере с слагом также может иметь смысл просто сохранить единственную функцию changeset, но проверить, присутствует ли слаг, и только если не создать новый.

В остальном я согласен с michalmuskala. По возможности отдавайте предпочтение отдельным функциям набора изменений обратным вызовам.

person manukall    schedule 25.11.2015
comment
А, это хороший момент. Я мог бы просто использовать здесь одну функцию набора изменений. Спасибо. - person Elliot Larson; 25.11.2015