Spec-Zone.ru › Phoenix

Ecto.Changeset

Исходный код

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

Разберём, что означают эти возможности. Представьте распространённый сценарий: вы хотите получить данные из отправленной пользователем веб-формы, чтобы создать или обновить записи в базе данных. Получив эти данные на сервере, с помощью наборов изменений можно выполнить следующие действия:

  • фильтрация — поскольку вы получаете внешние данные от третьей стороны, необходимо явно указать, какие данные вы принимаете. Например, скорее всего, вы не захотите разрешать пользователю самостоятельно устанавливать для поля "is_admin" значение true

  • приведение типов — веб-форма отправляет большую часть данных в виде строк. Когда пользователь вводит число "100", Ecto получает его как строку "100", которую затем необходимо преобразовать в 100. Наборы изменений отвечают за преобразование этих значений к типам, заданным в вашей Ecto.Schema, и поддерживают даже сложные типы, такие как дата и время

  • проверки — данные, отправленные пользователем, могут быть некорректными. Например, пользователь может ввести неверный адрес электронной почты с точкой в конце. Или дата предстоящей встречи может оказаться в прошлом году. Необходимо проверить данные и сообщить пользователю об ошибках

  • ограничения — некоторые проверки можно выполнить только с помощью базы данных. Например, чтобы узнать, занят ли адрес электронной почты пользователя, необходимо выполнить запрос к базе данных. Ограничения позволяют сделать это, сохраняя целостность данных

Хотя в качестве примера мы использовали веб-форму, наборы изменений можно применять в API и во многих других сценариях. Наборы изменений также можно использовать для работы с данными, которые не будут записываться в базу данных. Эти сценарии мы рассмотрим в документации ниже. Вводный пример работы с наборами изменений и их связи со схемами и репозиториями также приведён в модуле Ecto.

Вкратце, для создания набора изменений есть две основные функции. Функция cast/4 используется для получения внешних параметров из формы, API или командной строки и преобразования их к типам, заданным в вашей Ecto.Schema. Функция change/2 используется для непосредственного изменения данных в приложении, если считать, что переданные данные корректны и соответствуют существующим типам. Остальные функции этого модуля, например проверки, ограничения и работа со связями, предназначены для изменения наборов изменений.

Внешние и внутренние данные

Наборы изменений позволяют работать с двумя видами данных:

  • внешними по отношению к приложению — например, с пользовательским вводом из формы, который необходимо привести к нужным типам и корректно проверить. Этот сценарий в первую очередь охватывает функция cast/4.

  • внутренними для приложения — например, созданными программно или полученными из других подсистем. Этот сценарий в первую очередь охватывают функции change/2 и put_change/3.

При работе с внешними данными они обычно представлены в виде карт со строковыми ключами (также называемых параметрами). С другой стороны, при работе с внутренними данными обычно используются карты с атомарными ключами или структуры. Это различие позволяет отслеживать характер данных: если у вас есть структуры или карты с атомарными ключами, значит, данные уже разобраны и проверены.

Если у вас есть внешние данные или карты, в которых ключи могут быть как строками, так и атомами, рассмотрите возможность использования cast/4 для создания набора изменений. Набор изменений разберёт и проверит эти параметры, а также предоставит API для безопасной работы с данными и их изменения.

Проверки и ограничения

Наборы изменений Ecto предоставляют как проверки, так и ограничения, которые в случае ошибки в конечном итоге преобразуются в сообщения об ошибках.

Разница между ними заключается в том, что большинство проверок можно выполнить без обращения к базе данных, поэтому они всегда выполняются до попытки вставить запись в базу данных или обновить её. Проверки выполняются сразу при вызове функции проверки для данных, содержащихся в наборе изменений на этот момент.

Некоторые проверки могут обращаться к базе данных, но по своей природе они небезопасны. Названия таких проверок начинаются с префикса unsafe_, например unsafe_validate_unique/4.

Ограничения, напротив, опираются на базу данных и всегда безопасны. Поэтому проверки всегда выполняются до ограничений. Если проверки завершились неудачно, ограничения даже не будут проверяться.

Рассмотрим пример:

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name
    field :email
    field :age, :integer
  end

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, [:name, :email, :age])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_inclusion(:age, 18..100)
    |> unique_constraint(:email)
  end
end

В приведённой выше функции changeset/2 мы задаём три проверки. Они проверяют, что поля name и email присутствуют в наборе изменений, адрес электронной почты соответствует заданному формату, а возраст находится в диапазоне от 18 до 100 лет. Кроме того, для поля электронной почты задаётся ограничение уникальности.

Предположим, что адрес электронной почты указан, но возраст некорректен. В наборе изменений будут следующие ошибки:

changeset = User.changeset(%User{}, %{age: 0, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [age: {"is invalid", []}, name: {"can't be blank", []}]

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

changeset = User.changeset(%User{}, %{age: 42, name: "Mary", email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [email: {"has already been taken", []}]

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

Отложенные ограничения

Некоторые базы данных поддерживают отложенные ограничения, то есть ограничения, которые проверяются в конце транзакции, а не в конце каждого оператора.

Наборы изменений не поддерживают такие ограничения. При работе с отложенными ограничениями нарушение при вызове Ecto.Repo.insert/2 или Ecto.Repo.update/2 не вернёт {:error, changeset}, а приведёт к возникновению ошибки в конце транзакции.

Удаление пробелов и пустые значения

Часто данные, переданные для приведения типов, нужно дополнительно обработать, особенно это касается пустых значений. Например, если вы собираете данные для приведения типов из командной строки, HTML-формы или любого другого текстового формата, скорее всего, эти способы не позволяют передать значения nil. Поэтому наборы изменений позволяют удалять пробелы из значений с помощью функции :trim_values.

При применении изменений с помощью cast/4 значения автоматически очищаются от пробелов, а затем проверяется, являются ли они пустыми, путём сопоставления со значениями поля changeset.empty_values (по умолчанию — [""]). Если значения пустые, полю присваивается значение по умолчанию. Если поле имеет тип массива, все пустые значения в массиве удаляются. Значения, считающиеся пустыми по умолчанию, можно задать с помощью параметра :empty_values или непосредственно в наборе изменений.

Связи, встраиваемые схемы и замена

С помощью наборов изменений можно работать со связями, а также со структурами встраиваемых схем. Для этого есть два основных API:

  • cast_assoc/3 и cast_embed/3 — эти функции используются при работе с внешними данными. В частности, они позволяют одновременно изменять связи и встраиваемые схемы вместе с родительской структурой.

  • put_assoc/4 и put_embed/4 — эти функции позволяют заменить связь или встраиваемую схему целиком. Их можно использовать, чтобы перенести связанные данные из одной записи в другую, полностью удалить или заменить существующие записи.

Эти функции задают определённый способ работы со связями. Если вам нужно другое поведение или явный контроль над связанными данными, можно отказаться от этой функциональности и использовать Ecto.Multi, чтобы описать выполнение нескольких операций с базой данных для нескольких схем и наборов изменений одновременно.

Подробнее о работе со связями можно узнать в нашей документации, в том числе в памятках и практических примерах. См.:

  • Документацию по функциям cast_assoc/3 и put_assoc/3
  • памятку по связям
  • руководство по ограничениям и операциям upsert
  • руководство по полиморфным связям «многие ко многим»

Параметр :on_replace

При использовании любого из этих API могут возникнуть ситуации, когда Ecto обнаруживает, что данные заменяются. Например, представьте, что у записи Post есть несколько комментариев с идентификаторами 1, 2 и 3. Если вызвать cast_assoc/3, передав только идентификаторы 1 и 2, Ecto сочтёт, что комментарий 3 «заменяется», и по умолчанию вызовет ошибку. Такое поведение можно изменить при определении связи: задайте параметр :on_replace для связи или встраиваемой схемы, выбрав одно из следующих значений:

  • :raise (по умолчанию) — не разрешать удаление связи или встроенных данных через наборы изменений родительской записи
  • :mark_as_invalid — при попытке удалить связь или встроенные данные через набор изменений родительской записи в набор изменений родительской записи будет добавлена ошибка, и он будет помечен как недопустимый
  • :nilify — установить для столбца ссылки на владельца значение nil (доступно только для связей). Используйте это значение для столбца belongs_to, чтобы разрешить очистить связь и задать для неё новое значение. Для связанных наборов изменений будет установлено action со значением :replace
  • :update — обновить связь; доступно только для has_one, belongs_to и embeds_one. Этот параметр обновляет все поля, переданные в наборе изменений, включая идентификатор связи
  • :delete — удалить связь или связанные данные из базы данных. Этот параметр следует использовать с осторожностью (см. ниже). Для связанных наборов изменений будет установлено action со значением :replace
  • :delete_if_exists — аналогично :delete, но игнорирует ошибку устаревшей записи. Например, если вы задали on_replace: :delete, но заменяемый ресурс уже был удалён отдельным запросом, возникнет ошибка Ecto.StaleEntryError. :delete_if_exists позволяет выполнить удаление, только если запись всё ещё существует

Параметры :delete и :delete_if_exists следует использовать с осторожностью, поскольку они позволяют пользователям удалить любые связанные данные, просто задав для них значение nil или пустой список. Если необходимо удаление, часто предпочтительнее добавить в схему отдельное виртуальное логическое поле и вручную пометить набор изменений для удаления, если в параметрах задано поле :delete, как показано в примере ниже. Обратите внимание, что в этом случае мы не вызываем cast/4, поскольку не хотим запрещать удаление, если изменение некорректно (изменения не имеют значения, если сущность нужно удалить).

defmodule Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string
    field :delete, :boolean, virtual: true
  end

  def changeset(comment, %{"delete" => "true"}) do
    %{Ecto.Changeset.change(comment, delete: true) | action: :delete}
  end

  def changeset(comment, params) do
    cast(comment, params, [:body])
  end
end

Наборы изменений без схемы

Во всех приведённых выше примерах мы использовали наборы изменений для проверки и приведения к нужным типам данных, содержащихся в структуре, определённой схемой Ecto, например в структуре %User{}, определённой модулем User.

Однако наборы изменений можно использовать и с обычными структурами, передав кортеж с данными и их типами:

user = %User{}
types = %{name: :string, email: :string, age: :integer}
params = %{name: "Callum", email: "callum@example.com", age: 27}
changeset =
  {user, types}
  |> Ecto.Changeset.cast(params, Map.keys(types))
  |> Ecto.Changeset.validate_required(...)
  |> Ecto.Changeset.validate_length(...)

где структура user относится к определению в следующем модуле:

defmodule User do
  defstruct [:name, :email, :age]
end

Наборы изменений также можно использовать с данными в обычной карте, применяя тот же API:

data  = %{}
types = %{name: :string, email: :string, age: :integer}
params = %{name: "Callum", email: "callum@example.com", age: 27}
changeset =
  {data, types}
  |> Ecto.Changeset.cast(params, Map.keys(types))
  |> Ecto.Changeset.validate_required(...)
  |> Ecto.Changeset.validate_length(...)

Помимо упомянутых выше базовых типов, таких как :boolean и :string, в наборах изменений без схемы можно использовать и параметризованные типы. Они реализуют поведение Ecto.ParameterizedType, а необходимые сведения о типе можно создать, вызвав функцию init/2.

Например, чтобы использовать Ecto.Enum в наборе изменений без схемы:

types = %{
  name: :string,
  role: Ecto.ParameterizedType.init(Ecto.Enum, values: [:reader, :editor, :admin])
}

data  = %{}
params = %{name: "Callum", role: "reader"}

changeset =
  {data, types}
  |> Ecto.Changeset.cast(params, Map.keys(types))
  |> Ecto.Changeset.validate_required(...)
  |> Ecto.Changeset.validate_length(...)

Наборы изменений без схемы делают Ecto чрезвычайно полезным инструментом для приведения данных к нужным типам, их проверки и очистки, даже если эти данные не предназначены для сохранения в базе данных.

Действия наборов изменений

У наборов изменений есть поле действия, которое обычно устанавливается модулем Ecto.Repo при вызове одной из операций, например insert или update:

changeset = User.changeset(%User{}, %{age: 42, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.action
#=> :insert

Это означает, что при работе с наборами изменений, которые не предназначены для сохранения в базе данных, например с наборами изменений без схемы, может потребоваться явно задать для действия определённое значение. Такие фреймворки, как Phoenix, используют значение действия, чтобы определить поведение HTML-форм.

Вместо того чтобы задавать действие вручную, можно использовать apply_action/2, которая имитирует операции, такие как c:Ecto.Repo.insert. Функция apply_action/2 вернёт {:ok, changes}, если набор изменений допустим, или {:error, changeset}, указав в наборе изменений заданное значение action в случае ошибок.

Структура Ecto.Changeset

Общедоступные поля:

  • valid? — указывает, допустим ли набор изменений
  • data — исходные данные набора изменений, например структура
  • params — параметры, переданные при создании набора изменений
  • changes — changes из параметров, прошедшие проверку при приведении типов
  • errors — все ошибки, возникшие при проверках
  • required — список всех обязательных полей в виде атомов
  • action — действие, которое необходимо выполнить с набором изменений
  • types — кэш типов полей данных
  • empty_values — список значений, которые следует считать пустыми
  • repo — репозиторий, применяющий набор изменений (устанавливается только после вызова функции Repo)
  • repo_opts — список параметров ключевое слово со значениями, передаваемых базовой операции репозитория

Следующие поля являются приватными и к ним нельзя обращаться напрямую.

  • validations
  • constraints
  • filters
  • prepare

Скрытие значений полей при инспектировании

Чтобы скрыть значение поля при инспектировании структуры Ecto.Changeset, пометьте это поле как redact: true в схеме. Тогда вместо значения будет отображаться **redacted**.

Краткое описание

Типы

action()
constraint()
data()
error()
t()
t(data_type)
traverse_result()
types()
validation()

Функции

add_error(changeset, key, message, keys \\ [])

Добавляет ошибку в набор изменений.

apply_action(changeset, action)

Применяет действие набора изменений, только если изменения корректны.

apply_action!(changeset, action)

Применяет действие набора изменений, если изменения корректны, иначе вызывает ошибку.

apply_changes(changeset)

Применяет изменения набора изменений к его данным.

assoc_constraint(changeset, assoc, opts \\ [])

Проверяет, существует ли связанное поле.

cast(data, params, permitted, opts \\ [])

Применяет заданный params в качестве изменений к data согласно набору ключей permitted. Возвращает набор изменений.

cast_assoc(changeset, name, opts \\ [])

Приводит тип заданной ассоциации с помощью параметров набора изменений.

cast_embed(changeset, name, opts \\ [])

Приводит тип заданного встроенного объекта с помощью параметров набора изменений.

change(data, changes \\ %{})

Оборачивает заданные данные в набор изменений или добавляет изменения в набор изменений.

changed?(changeset, field, opts \\ [])

Возвращает true, если поле было изменено в наборе изменений.

check_constraint(changeset, field, opts \\ [])

Проверяет наличие ограничения check для заданного поля.

constraints(changeset)

Возвращает все ограничения набора изменений.

delete_change(changeset, key)

Удаляет изменение с заданным ключом.

empty_values()

Список пустых значений, используемый Ecto по умолчанию.

exclusion_constraint(changeset, field, opts \\ [])

Проверяет наличие ограничения исключения для заданного поля.

fetch_change(changeset, key)

Извлекает изменение из заданного набора изменений.

fetch_change!(changeset, key)

Аналог fetch_change/2, но возвращает значение или вызывает ошибку, если заданный ключ не найден.

fetch_field(changeset, key)

Извлекает заданное поле из изменений или данных.

fetch_field!(changeset, key)

Аналог fetch_field/2, но возвращает значение или вызывает ошибку, если заданный ключ не найден.

field_missing?(changeset, field)

Определяет, отсутствует ли поле в наборе изменений.

force_change(changeset, key, value)

Принудительно задаёт изменение в key со значением value.

foreign_key_constraint(changeset, field, opts \\ [])

Проверяет наличие ограничения внешнего ключа для заданного поля.

get_assoc(changeset, name, as \\ :changeset)

Получает запись или записи ассоциации из изменений или данных.

get_change(changeset, key, default \\ nil)

Получает изменение или возвращает значение по умолчанию.

get_embed(changeset, name, as \\ :changeset)

Получает встроенную запись или записи из изменений или данных.

get_field(changeset, key, default \\ nil)

Получает поле из изменений или данных.

merge(changeset1, changeset2)

Объединяет два набора изменений.

no_assoc_constraint(changeset, assoc, opts \\ [])

Проверяет, что связанное поле не существует.

optimistic_lock(data_or_changeset, field, incrementer \\ &increment_with_rollover/1)

Применяет оптимистическую блокировку к набору изменений.

prepare_changes(changeset, function)

Задаёт функцию, выполняемую репозиторием при вставке, обновлении или удалении.

put_assoc(changeset, name, value, opts \\ [])

Добавляет заданную запись или записи ассоциации в набор изменений в качестве изменения.

put_change(changeset, key, value)

Добавляет изменение в key со значением value.

put_embed(changeset, name, value, opts \\ [])

Добавляет заданную встроенную запись или записи в набор изменений в качестве изменения.

reorder_assoc(changeset, name)

Изменяет порядок изменений для заданной ассоциации.

reorder_assoc(changeset, name, sort_fn)

Изменяет порядок изменений для заданной ассоциации с помощью пользовательской функции сортировки.

traverse_errors(changeset, msg_func)

Обходит ошибки набора изменений и применяет заданную функцию к сообщениям об ошибках.

traverse_validations(changeset, msg_func)

Обходит проверки набора изменений и применяет заданную функцию к проверкам.

unique_constraint(changeset, field_or_fields, opts \\ [])

Проверяет наличие ограничения уникальности для заданного поля или списка полей.

unsafe_validate_unique(changeset, fields, repo, opts \\ [])

Проверяет, что у существующей записи с другим первичным ключом нет таких же значений в этих полях.

update_change(changeset, key, function)

Обновляет изменение.

validate_acceptance(changeset, field, opts \\ [])

Проверяет, что заданный параметр имеет значение true.

validate_change(changeset, field, validator)

Проверяет заданное изменение field.

validate_change(changeset, field, metadata, validator)

Сохраняет metadata проверки и проверяет заданное изменение field.

validate_confirmation(changeset, field, opts \\ [])

Проверяет, что заданный параметр совпадает со своим подтверждением.

validate_exclusion(changeset, field, data, opts \\ [])

Проверяет, что изменение не входит в заданный перечислимый набор.

validate_format(changeset, field, format, opts \\ [])

Проверяет, что изменение соответствует заданному формату.

validate_inclusion(changeset, field, data, opts \\ [])

Проверяет, что изменение входит в заданный перечислимый набор.

validate_length(changeset, field, opts)

Проверяет, что изменение представляет собой строку или список заданной длины.

validate_number(changeset, field, opts)

Проверяет свойства числа.

validate_required(changeset, fields, opts \\ [])

Проверяет наличие одного или нескольких полей в наборе изменений.

validate_subset(changeset, field, data, opts \\ [])

Проверяет, что изменение типа enum является подмножеством заданного перечислимого набора.

validations(changeset)

Возвращает список ключевых слов с проверками для этого набора изменений.

Типы

action()Исходный код

@type action() :: nil | :insert | :update | :delete | :replace | :ignore | atom()

constraint()Исходный код

@type constraint() :: %{
  type: :check | :exclusion | :foreign_key | :unique,
  constraint: String.t() | Regex.t(),
  match: :exact | :suffix | :prefix,
  field: atom(),
  error_message: String.t(),
  error_type: atom()
}

data()Исходный код

@type data() :: map()

error()Исходный код

@type error() :: {String.t(), Keyword.t()}

t()Исходный код

@type t() :: t(Ecto.Schema.t() | map() | nil)

t(data_type)Исходный код

@type t(data_type) :: %Ecto.Changeset{
  action: action(),
  changes: %{optional(atom()) => term()},
  constraints: [constraint()],
  data: data_type,
  empty_values: term(),
  errors: [{atom(), error()}],
  filters: %{optional(atom()) => term()},
  params: %{optional(String.t()) => term()} | nil,
  prepare: [(t() -> t())],
  repo: atom() | nil,
  repo_opts: Keyword.t(),
  required: [atom()],
  types: types(),
  valid?: boolean(),
  validations: [validation()]
}

traverse_result()Исходный код

@type traverse_result() :: %{required(atom()) => [term()] | traverse_result()}

types()Исходный код

@type types() :: %{
  required(atom()) => Ecto.Type.t() | {:assoc, term()} | {:embed, term()}
}

validation()Исходный код

@type validation() :: {atom(), term()}

Функции

add_error(changeset, key, message, keys \\ [])Источник

@spec add_error(t(), atom(), String.t(), Keyword.t()) :: t()

Добавляет ошибку в набор изменений.

Можно передать дополнительный список ключевых слов keys, чтобы предоставить дополнительную контекстную информацию об ошибке. Это полезно при использовании traverse_errors/2 и при переводе ошибок с помощью Gettext

Примеры

iex> changeset = change(%Post{}, %{title: ""})
iex> changeset = add_error(changeset, :title, "empty")
iex> changeset.errors
[title: {"empty", []}]
iex> changeset.valid?
false

iex> changeset = change(%Post{}, %{title: ""})
iex> changeset = add_error(changeset, :title, "empty", additional: "info")
iex> changeset.errors
[title: {"empty", [additional: "info"]}]
iex> changeset.valid?
false

iex> changeset = change(%Post{}, %{tags: ["ecto", "elixir", "x"]})
iex> changeset = add_error(changeset, :tags, "tag '%{val}' is too short", val: "x")
iex> changeset.errors
[tags: {"tag '%{val}' is too short", [val: "x"]}]
iex> changeset.valid?
false

apply_action(changeset, action)Источник

@spec apply_action(t(), action()) :: {:ok, Ecto.Schema.t() | data()} | {:error, t()}

Применяет действие набора изменений, только если изменения допустимы.

Если изменения допустимы, все изменения применяются к данным набора изменений. Если изменения недопустимы, они не применяются, а возвращается кортеж ошибки с набором изменений, содержащим действие, которое пытались применить.

Действием может быть любой атом.

Примеры

iex> {:ok, data} = apply_action(changeset, :update)

iex> {:ok, data} = apply_action(changeset, :my_action)

iex> {:error, changeset} = apply_action(changeset, :update)
%Ecto.Changeset{action: :update}

apply_action!(changeset, action)Источник

@spec apply_action!(t(), action()) :: Ecto.Schema.t() | data()

Применяет действие набора изменений, если изменения допустимы, или вызывает ошибку.

Примеры

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> apply_action!(changeset, :update)
%Post{author: "bar", title: "foo"}

iex> changeset = change(%Post{author: "bar"}, %{title: :bad})
iex> apply_action!(changeset, :update)
** (Ecto.InvalidChangesetError) could not perform update because changeset is invalid.

Дополнительную информацию см. в разделе apply_action/2.

apply_changes(changeset)Источник

@spec apply_changes(t()) :: Ecto.Schema.t() | data()

Применяет изменения набора изменений к его данным.

Эта операция возвращает исходные данные с изменениями независимо от того, допустим набор изменений или нет. См. apply_action/2 — похожую функцию, которая проверяет допустимость набора изменений.

Примеры

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> apply_changes(changeset)
%Post{author: "bar", title: "foo"}

assoc_constraint(changeset, assoc, opts \\ [])Источник

@spec assoc_constraint(t(), atom(), Keyword.t()) :: t()

Проверяет, существует ли связанное поле.

Это похоже на foreign_key_constraint/3, но поле определяется по описанию связи. Это полезно, чтобы гарантировать, что дочерняя запись будет создана, только если родительская запись также существует в базе данных. Поэтому функция применяется только к связям belongs_to.

Как следует из названия, для работы этой функции в базе данных должно быть ограничение. Такое ограничение часто добавляется в виде ссылки на дочернюю таблицу:

create table(:comments) do
  add :post_id, references(:posts)
end

Теперь при добавлении комментария можно запретить его добавление, если связанный пост не существует:

comment
|> Ecto.Changeset.cast(params, [:post_id])
|> Ecto.Changeset.assoc_constraint(:post)
|> Repo.insert

Параметры

  • :message — сообщение в случае сбоя проверки ограничения; по умолчанию «does not exist»
  • :name — имя ограничения. По умолчанию оно определяется по таблице и полю. Если указан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. В сложных случаях может потребоваться указать его явно
  • :match — способ сопоставления имени ограничения набора изменений с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. :suffix соответствует любому ограничению репозитория, которое ends_with? :name этому ограничению набора изменений. :prefix соответствует любому ограничению репозитория, которое starts_with? :name этому ограничению набора изменений.

cast(data, params, permitted, opts \\ [])Источник

@spec cast(
  Ecto.Schema.t() | t() | {data(), types()},
  %{required(binary()) => term()} | %{required(atom()) => term()} | :invalid,
  [atom()],
  Keyword.t()
) :: t()

Применяет заданные params как изменения к data в соответствии с набором ключей permitted. Возвращает набор изменений.

data может быть набором изменений, структурой схемы или кортежем {data, types}. Второй аргумент — это карта params, значения которой приводятся к типам согласно информации о типах из data. params — это карта со строковыми ключами или карта с атомарными ключами, содержащая потенциально недопустимые данные. Смешивать ключи нельзя.

При приведении типов всем параметрам permitted, значения которых соответствуют указанной информации о типах, имя ключа преобразуется в атом и сохраняется вместе со значением как изменение в поле :changes набора изменений. Если приведенное значение совпадает с текущим значением поля, оно не будет включено в :changes, если не указан параметр force_changes: true. Все параметры, которые явно не разрешены, игнорируются.

Если приведение типов для всех полей прошло успешно, набор изменений возвращается как допустимый.

Обратите внимание, что cast/4 проверяет типы в params, но не в переданном data.

Параметры

  • :trim_values — функция с двумя аргументами, которая обрезает значения перед проверкой на принадлежность к changeset.empty_values. Значения, соответствующие пустым значениям, заменяются значением по умолчанию соответствующего поля (для наборов изменений без схемы это всегда nil). По умолчанию используется функция &Ecto.Type.trim/2

  • :empty_values — список значений, которые после обрезки считаются пустыми; по умолчанию [""]

  • :force_changes — логическое значение, указывающее, нужно ли включать значения, не изменяющие текущие данные, в :changes. Дополнительную информацию см. в разделе force_change/3. По умолчанию false

  • :message — функция с двумя аргументами, используемая для создания сообщения об ошибке при сбое приведения типа. Она вызывается для каждого поля, тип которого не удалось привести, и получает имя поля первым аргументом, а метаданные ошибки — вторым. Она должна возвращать строку или nil. Если возвращена строка, она используется как сообщение об ошибке. Если возвращено nil, используется стандартное сообщение об ошибке. Тип поля указан в метаданных под ключом :type

Примеры

iex> changeset = cast(post, params, [:title])
iex> if changeset.valid? do
...>   Repo.update!(changeset)
...> end

Передача набора изменений в качестве первого аргумента:

iex> changeset = cast(post, %{title: "Hello"}, [:title])
iex> new_changeset = cast(changeset, %{title: "Foo", body: "World"}, [:body])
iex> new_changeset.params
%{"title" => "Hello", "body" => "World"}

Или создание набора изменений из простой карты с типами:

iex> data = %{title: "hello"}
iex> types = %{title: :string}
iex> changeset = cast({data, types}, %{title: "world"}, [:title])
iex> apply_changes(changeset)
%{title: "world"}

Можно использовать пустые значения (и даже выполнять приведение типов несколько раз), чтобы изменить то, что считается пустым значением:

# Using default
iex> params = %{title: "", topics: []}
iex> changeset = cast(%Post{}, params, [:title, :topics])
iex> changeset.changes
%{topics: []}

# Changing default
iex> params = %{title: "", topics: []}
iex> changeset = cast(%Post{}, params, [:title, :topics], empty_values: [[], nil])
iex> changeset.changes
%{title: ""}

# Augmenting default
iex> params = %{title: "", topics: []}
iex> changeset =
...>   cast(%Post{}, params, [:title, :topics], empty_values: [[], nil] ++ Ecto.Changeset.empty_values())
iex> changeset.changes
%{}

Можно определить пользовательскую функцию сообщения об ошибке.

# Using field name
iex> params = %{title: 1, body: 2}
iex> custom_errors = [title: "must be a string"]
iex> msg_func = fn field, _meta -> custom_errors[field] end
iex> changeset = cast(post, params, [:title, :body], message: msg_func)
iex> changeset.errors
[
  title: {"must be a string", [type: :string, validation: :cast]},
  body: {"is_invalid", [type: :string, validation: :cast]}
]

# Using field type
iex> params = %{title: 1, body: 2}
iex> custom_errors = [string: "must be a string"]
iex> msg_func = fn _field, meta ->
...    type = meta[:type]
...    custom_errors[type]
...  end
iex> changeset = cast(post, params, [:title, :body], message: msg_func)
iex> changeset.errors
[
  title: {"must be a string", [type: :string, validation: :cast]},
  body: {"must be a string", [type: :string, validation: :cast]}
]

Объединение операций приведения типов

cast/4 также принимает набор изменений в качестве первого аргумента. В этом случае все эффекты вызова cast/4 (дополнительные ошибки и изменения) просто добавляются к уже имеющимся в наборе изменений. Параметры объединяются (без рекурсивного объединения), причём переданные в cast/4 имеют приоритет над уже имеющимися в наборе изменений.

cast_assoc(changeset, name, opts \\ [])Источник

@spec cast_assoc(t(), atom(), Keyword.t()) :: t()

Приводит типы заданной связи, используя параметры набора изменений.

Эту функцию следует использовать при работе со всей связью одновременно (а не с одним элементом связи типа many) и при получении данных извне приложения.

cast_assoc/3 сопоставляет записи, полученные из базы данных, и сравнивает их с параметрами, полученными из внешнего источника. Поэтому предполагается, что в данных набора изменений связь, для которой выполняется приведение типов, явно предварительно загружена, а все идентификаторы существуют и уникальны.

Например, представим, что у пользователя есть связь со множеством адресов, а данные запроса передаются следующим образом

%{"name" => "john doe", "addresses" => [
  %{"street" => "somewhere", "country" => "brazil", "id" => 1},
  %{"street" => "elsewhere", "country" => "poland"},
]}

а затем

User
|> Repo.get!(id)
|> Repo.preload(:addresses) # Only required when updating data
|> Ecto.Changeset.cast(params, [])
|> Ecto.Changeset.cast_assoc(:addresses, with: &MyApp.Address.changeset/2)

Параметры заданной связи будут получены из changeset.params. Предполагается, что эти параметры представляют собой карту атрибутов, подобную передаваемой в cast/4. После получения параметров cast_assoc/3 сопоставит их со связями, уже содержащимися в данных набора изменений.

После вызова cast_assoc/3 Ecto сравнит каждый параметр с уже предварительно загруженными адресами пользователя и выполнит следующие действия:

  • Если параметр не содержит идентификатор, его данные будут переданы в MyApp.Address.changeset/2 с новой структурой, и операция станет вставкой. Идентификатор считается неуказанным, только если ключа «id» нет или его значение в точности равно nil

  • Если параметр содержит идентификатор, но связанного дочернего элемента с таким идентификатором нет, его данные будут переданы в MyApp.Address.changeset/2 с новой структурой, и операция станет вставкой

  • Если параметр содержит идентификатор и связанный дочерний элемент с таким идентификатором существует, его данные будут переданы в MyApp.Address.changeset/2 с существующей структурой, и операция станет обновлением

  • Если связанный дочерний элемент имеет идентификатор, который не указан в параметре, будет вызвана функция обратного вызова :on_replace для этой связи (см. раздел «При замене» в документации модуля)

Если два или более адреса имеют одинаковые идентификаторы, Ecto сочтёт это ошибкой и добавит в набор изменений сообщение о наличии дубликатов.

При каждом вызове функции MyApp.Address.changeset/2 она должна возвращать набор изменений. Этот набор изменений всегда будет включён в changes родительского набора изменений, даже если в нём нет изменений. Это необходимо для отражения, позволяющего разработчикам изучать проверки и другие метаданные связи. После передачи родительского набора изменений функции Ecto.Repo все записи будут добавлены, обновлены или удалены в рамках одной транзакции.

Как видно выше, эта функция предполагает определённый способ работы. Если вам нужно другое поведение или явный контроль над связанными данными, можно использовать put_assoc/4 либо Ecto.Multi, чтобы описать выполнение нескольких операций с базой данных для нескольких схем и наборов изменений одновременно.

Пользовательские действия

Разработчики могут явно задавать поле :action набора изменений, чтобы указать Ecto, как действовать в определённых ситуациях. Предположим, что если у одной из связей все поля пусты, нужно полностью проигнорировать запись, а не выводить ошибку. Функцию набора изменений можно написать так:

def changeset(struct, params) do
  struct
  |> cast(params, [:title, :body])
  |> validate_required([:title, :body])
  |> case do
    %{valid?: false, changes: changes} = changeset when changes == %{} ->
      # If the changeset is invalid and has no changes, it is
      # because all required fields are missing, so we ignore it.
      %{changeset | action: :ignore}
    changeset ->
      changeset
  end
end

Также можно задать удаление, если нужно удалять данные на основе полученных параметров (например, флажка или любого другого признака).

Частичные изменения для связей типа many

Предварительно загрузив связь с помощью пользовательского запроса, можно ограничить поведение cast_assoc/3. Это позволяет работать с подмножеством данных, а не со всеми связями в базе данных.

Вернёмся к первоначальному примеру с адресами пользователей. Представим, что эти адреса принадлежат определённой стране. Если нужно разрешить пользователям массово редактировать все адреса, принадлежащие одной стране, можно изменить запрос предварительной загрузки:

query = from MyApp.Address, where: [country: ^edit_country]

User
|> Repo.get!(id)
|> Repo.preload(addresses: query)
|> Ecto.Changeset.cast(params, [])
|> Ecto.Changeset.cast_assoc(:addresses)

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

Сортировка и удаление элементов из коллекций типа many

В предыдущих примерах мы передавали связь типа many в виде списка:

%{"name" => "john doe", "addresses" => [
  %{"street" => "somewhere", "country" => "brazil", "id" => 1},
  %{"street" => "elsewhere", "country" => "poland"},
]}

Однако адреса также часто передают в виде карты, где каждый ключ — целое число, представляющее позицию элемента:

%{"name" => "john doe", "addresses" => %{
  0 => %{"street" => "somewhere", "country" => "brazil", "id" => 1},
  1 => %{"street" => "elsewhere", "country" => "poland"}
}}

Использование индексов особенно полезно с двумя поддерживающими параметрами: :sort_param и :drop_param. Эти параметры указывают, следует ли переупорядочить индексы или удалить их из данных. Например, если выполнить:

cast_embed(changeset, :addresses,
  sort_param: :addresses_sort,
  drop_param: :addresses_drop)

Теперь можно передать следующее:

%{"name" => "john doe", "addresses" => %{...}, "addresses_drop" => [0]}

После этого запись с индексом 0 будет удалена из параметров перед приведением типов. Обратите внимание, что для этого нужно задать соответствующий параметр :on_replace в определении связи или встраиваемой структуры.

Для сортировки можно выполнить похожее действие:

%{"name" => "john doe", "addresses" => %{...}, "addresses_sort" => [1, 0]}

Элементы будут отсортированы внутри, так что элемент 1 окажется перед элементом 0. Обратите внимание: любой индекс, отсутствующий в "addresses_sort", будет расположен перед отсортированными индексами. Если индекс не найден, на его месте добавляется пустая запись.

Для встраиваемых структур это гарантирует их перезапись в заданном порядке. Однако для связей этого недостаточно. Нужно добавить в схему поле field :position, :integer и добавить функцию with с тремя аргументами, которая будет добавлять позицию в набор изменений дочернего элемента. Например, можно реализовать следующее:

defp child_changeset(child, _changes, position) do
  child
  |> change(position: position)
end

Если передать её в :with, она будет вызвана с итоговой позицией элемента:

changeset
|> cast_assoc(:children, sort_param: ..., with: &child_changeset/3)

При использовании :drop_param оставшиеся элементы получают последовательные индексы, начиная с 0. Например, если заданы следующие параметры:

%{
  "addresses" => %{
    0 => %{"street" => "First St"},
    1 => %{"street" => "Second St"},
    2 => %{"street" => "Third St"}
  },
  "addresses_drop" => [1]
}

Элемент, изначально находившийся по индексу 2 («Third St»), будет передан функции :with с позицией 1, поскольку после удаления список уплотняется.

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

Дополнительные материалы

Подробнее о работе со связями, включая шпаргалки и практические примеры, можно узнать в нашей документации. См.:

  • документацию по put_assoc/3
  • шпаргалку по связям
  • руководство по ограничениям и операциям UPSERT
  • руководство по полиморфным связям многие-ко-многим

Параметры

  • :required — обязательное ли это поле связи. Для связей с кардинальностью один проверка проходит, если значение не равно nil. Для связей со множеством элементов достаточно непустого списка.

  • :required_message — сообщение при ошибке; по умолчанию «can't be blank»

  • :invalid_message — сообщение при ошибке; по умолчанию «is invalid»

  • :force_update_on_change — принудительно обновлять родительскую запись в репозитории при наличии изменений; по умолчанию true

  • :with — функция для создания набора изменений из параметров. По умолчанию используется функция changeset/2 связанного модуля. Это может быть анонимная функция, принимающая два аргумента: связанную структуру, для которой выполняется приведение типов, и её параметры. Она должна возвращать набор изменений. Для связей с кардинальностью :many допускаются функции с тремя аргументами; третьим аргументом будет позиция связанного элемента в списке или nil, если связь заменяется.

  • :drop_param — имя параметра, содержащего список индексов, которые нужно удалить из параметров связи

  • :sort_param — имя параметра, содержащего список индексов для сортировки параметров связи. Неизвестные индексы считаются новыми элементами. Индексы, не включённые в список, будут расположены перед отсортированными. Дополнительную информацию см. в разделе cast_assoc/3

cast_embed(changeset, name, opts \\ [])Источник

@spec cast_embed(t(), atom(), Keyword.t()) :: t()

Приводит типы заданной встраиваемой структуры, используя параметры набора изменений.

Параметры заданной встраиваемой структуры будут получены из changeset.params. Предполагается, что эти параметры представляют собой карту атрибутов, подобную передаваемой в cast/4. После получения параметров cast_embed/3 сопоставит их со встраиваемыми структурами, уже содержащимися в записи набора изменений. Пример работы с приведением типов и связями, применимый также к встраиваемым структурам, см. в разделе cast_assoc/3.

До вызова этой функции для набора изменений должна быть предварительно выполнена операция cast с помощью cast/4.

Параметры

  • :required — обязательное ли это поле встраиваемой структуры. Для структур с кардинальностью один проверка проходит, если значение не равно nil. Для структур со множеством элементов достаточно непустого списка.

  • :required_message — сообщение при ошибке; по умолчанию «can't be blank»

  • :invalid_message — сообщение при ошибке; по умолчанию «is invalid»

  • :force_update_on_change — принудительно обновлять родительскую запись в репозитории при наличии изменений; по умолчанию true

  • :with — функция для создания набора изменений из параметров. По умолчанию используется функция changeset/2 связанного модуля. Это должна быть анонимная функция, принимающая два аргумента: встраиваемую структуру, для которой выполняется приведение типов, и её параметры. Она должна возвращать набор изменений. Для встраиваемых структур с кардинальностью :many допускаются функции с тремя аргументами; третьим аргументом будет позиция связанного элемента в списке или nil, если встраиваемая структура заменяется.

  • :drop_param — имя параметра, содержащего список индексов, которые нужно удалить из параметров связи

  • :sort_param — имя параметра, содержащего список индексов для сортировки параметров связи. Неизвестные индексы считаются новыми элементами. Индексы, не включённые в список, будут расположены перед отсортированными. Дополнительную информацию см. в разделе cast_assoc/3

change(data, changes \\ %{})Источник

@spec change(
  Ecto.Schema.t() | t() | {data(), types()},
  %{required(atom()) => term()} | Keyword.t()
) ::
  t()

Оборачивает заданные данные в набор изменений или добавляет изменения в набор изменений.

changes — это карта или список ключевых слов, где ключ — атом, представляющий поле, связь или встраиваемую структуру, а значение — произвольный термин. Обратите внимание, что value напрямую сохраняется в наборе изменений без какой-либо проверки. Поэтому эта функция предназначена для работы с данными внутри приложения.

При изменении встраиваемых структур и связей см. put_assoc/4 — полный справочник по допустимым значениям.

Эта функция полезна для:

  • обёртывания структуры в набор изменений
  • непосредственного изменения структуры без приведения типов и проверок
  • непосредственного массового добавления изменений в набор изменений

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

Если первым аргументом передан набор изменений, изменения из второго аргумента объединяются с уже имеющимися в нём, если они отличаются от значений в структуре.

Если первым аргументом передан {data, types}, создаётся набор изменений с заданными данными и типами, помеченный как допустимый.

Если нужно приводить типы и проверять внешние параметры, см. cast/4.

Примеры

iex> changeset = change(%Post{})
%Ecto.Changeset{...}
iex> changeset.valid?
true
iex> changeset.changes
%{}

iex> changeset = change(%Post{author: "bar"}, title: "title")
iex> changeset.changes
%{title: "title"}

iex> changeset = change(%Post{title: "title"}, title: "title")
iex> changeset.changes
%{}

iex> changeset = change(changeset, %{title: "new title", body: "body"})
iex> changeset.changes.title
"new title"
iex> changeset.changes.body
"body"

changed?(changeset, field, opts \\ [])Источник

@spec changed?(t(), atom(), Keyword.t()) :: boolean()

Возвращает true, если поле было изменено в наборе изменений.

Эта функция может проверять связи и встраиваемые структуры, но не поддерживает параметры :to и :from для таких полей.

Параметры

  • :to — проверяет, было ли поле изменено на заданное значение
  • :from — проверяет, было ли поле изменено с заданного значения

Примеры

iex> post = %Post{title: "Foo", body: "Old"}
iex> changeset = change(post, %{title: "New title", body: "Old"})

iex> changed?(changeset, :body)
false

iex> changed?(changeset, :title)
true

iex> changed?(changeset, :title, to: "NEW TITLE")
false

check_constraint(changeset, field, opts \\ [])Источник

@spec check_constraint(t(), atom(), Keyword.t()) :: t()

Проверяет наличие ограничения CHECK для заданного поля.

Ограничение CHECK работает так: база данных проверяет, было ли нарушено ограничение, а если да, Ecto преобразует это в ошибку набора изменений.

Чтобы использовать ограничение CHECK, сначала нужно определить его в миграции:

create constraint("users", :age_must_be_positive, check: "age > 0")

Теперь, когда ограничение создано, при изменении пользователей можно указать ограничение CHECK в наборе изменений, чтобы Ecto знал, как преобразовать его в сообщение об ошибке:

cast(user, params, [:age])
|> check_constraint(:age, name: :age_must_be_positive)

Теперь при вызове Ecto.Repo.insert/2 или Ecto.Repo.update/2, если возраст не является положительным числом, базовая операция завершится ошибкой, но Ecto преобразует исключение базы данных в ошибку набора изменений и вернёт кортеж {:error, changeset}. Обратите внимание, что ошибка возникнет только после обращения к базе данных, поэтому она не будет видна, пока не пройдут все остальные проверки. Если ограничение нарушено внутри транзакции, транзакция будет помечена как прерванная.

Параметры

  • :message — сообщение в случае сбоя проверки ограничения. По умолчанию «is invalid»
  • :name — имя ограничения. По умолчанию оно определяется по таблице и полю. Если указан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. В сложных случаях может потребоваться указать его явно
  • :match — способ сопоставления имени ограничения набора изменений с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. :suffix соответствует любому ограничению репозитория, которое ends_with? :name этому ограничению набора изменений. :prefix соответствует любому ограничению репозитория, которое starts_with? :name этому ограничению набора изменений.

constraints(changeset)Источник

@spec constraints(t()) :: [constraint()]

Возвращает все ограничения набора изменений.

Ограничение — это карта со следующими полями:

  • :type — тип ограничения, которое будет проверяться в базе данных, например :check, :unique и т. д.
  • :constraint — имя ограничения в базе данных в виде строки или Regex. Ограничение на уровне базы данных будет проверяться на соответствие этому значению согласно типу :match
  • :match — тип сопоставления, который Ecto выполнит для нарушенного ограничения относительно значения :constraint. Возможны значения :exact, :suffix или :prefix
  • :field — поле, для которого будет добавлена ошибка при нарушении ограничения
  • :error_message — сообщение об ошибке при нарушении ограничения
  • :error_type — тип ошибки, определяющий сообщение об ошибке

delete_change(changeset, key)Источник

@spec delete_change(t(), atom()) :: t()

Удаляет изменение с заданным ключом.

Примеры

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = delete_change(changeset, :title)
iex> get_change(changeset, :title)
nil

empty_values()Источник

Список пустых значений Ecto по умолчанию.

По умолчанию [""].

exclusion_constraint(changeset, field, opts \\ [])Источник

Проверяет наличие ограничения исключения для заданного поля.

Ограничение исключения работает так: база данных проверяет, было ли нарушено ограничение, а если да, Ecto преобразует это в ошибку набора изменений.

Параметры

  • :message — сообщение в случае сбоя проверки ограничения; по умолчанию «violates an exclusion constraint»
  • :name — имя ограничения. По умолчанию оно определяется по таблице и полю. Если указан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. В сложных случаях может потребоваться указать его явно
  • :match — способ сопоставления имени ограничения набора изменений с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. :suffix соответствует любому ограничению репозитория, которое ends_with? :name этому ограничению набора изменений. :prefix соответствует любому ограничению репозитория, которое starts_with? :name этому ограничению набора изменений.

fetch_change(changeset, key)Источник

@spec fetch_change(t(), atom()) :: {:ok, term()} | :error

Извлекает изменение из заданного набора изменений.

Эта функция проверяет только поле :changes заданного changeset и возвращает {:ok, value}, если изменение присутствует, или :error, если его нет.

Примеры

iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
iex> fetch_change(changeset, :title)
{:ok, "bar"}
iex> fetch_change(changeset, :body)
:error

fetch_change!(changeset, key)Источник

@spec fetch_change!(t(), atom()) :: term()

То же, что и fetch_change/2, но возвращает значение или вызывает исключение, если указанный ключ не найден.

Примеры

iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
iex> fetch_change!(changeset, :title)
"bar"
iex> fetch_change!(changeset, :body)
** (KeyError) key :body not found in: %{title: "bar"}

fetch_field(changeset, key)Источник

@spec fetch_field(t(), atom()) :: {:changes, term()} | {:data, term()} | :error

Извлекает указанное поле из изменений или из данных.

В то время как fetch_change/2 для получения значения проверяет только текущий changes, эта функция проверяет изменения, а затем обращается к данным, возвращая в итоге :error, если значение недоступно.

Для связей эти функции возвращают исходные данные changeset с применёнными изменениями. Чтобы получить необработанные changeset, используйте fetch_change/2.

Примеры

iex> post = %Post{title: "Foo", body: "Bar baz bong"}
iex> changeset = change(post, %{title: "New title"})
iex> fetch_field(changeset, :title)
{:changes, "New title"}
iex> fetch_field(changeset, :body)
{:data, "Bar baz bong"}
iex> fetch_field(changeset, :not_a_field)
:error

fetch_field!(changeset, key)Источник

@spec fetch_field!(t(), atom()) :: term()

То же, что и fetch_field/2, но возвращает значение или вызывает исключение, если указанный ключ не найден.

Примеры

iex> post = %Post{title: "Foo", body: "Bar baz bong"}
iex> changeset = change(post, %{title: "New title"})
iex> fetch_field!(changeset, :title)
"New title"
iex> fetch_field!(changeset, :other)
** (KeyError) key :other not found in: %Post{...}

field_missing?(changeset, field)Источник

@spec field_missing?(t(), atom()) :: boolean()

Определяет, отсутствует ли поле в changeset.

Поле считается отсутствующим, если оно равно nil или совпадает с одним из значений, указанных в changeset.empty_values.

Это полезно при выполнении сложных проверок, которые невозможно реализовать с помощью validate_required/3. Например, чтобы проверить, присутствует ли хотя бы одно поле из списка или присутствует ли ровно одно поле из списка.

Примеры

iex> changeset = cast(%Post{}, %{color: "Red"}, [:color])
iex> missing_fields = Enum.filter([:title, :body], &field_missing?(changeset, &1))
iex> changeset =
...>   case missing_fields do
...>     [_, _] -> add_error(changeset, :title, "at least one of `:title` or `:body` must be present")
...>     _ -> changeset
...>   end
...> changeset.errors
[title: {"at least one of `:title` or `:body` must be present", []}]

force_change(changeset, key, value)Источник

@spec force_change(t(), atom(), term()) :: t()

Принудительно задаёт изменение для указанного key со значением value.

Если изменение уже присутствует, оно заменяется новым значением. Если позднее значение будет изменено с помощью put_change/3 и update_change/3 и возвращено к исходному значению, изменение будет отменено, если только force_change/3 не будет вызвана ещё раз.

Примеры

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> changeset = force_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "bar"}

iex> changeset = force_change(changeset, :author, "bar")
iex> changeset.changes
%{title: "bar", author: "bar"}

foreign_key_constraint(changeset, field, opts \\ [])Источник

@spec foreign_key_constraint(t(), atom(), Keyword.t()) :: t()

Проверяет ограничение внешнего ключа для указанного поля.

Ограничение внешнего ключа работает за счёт того, что база данных проверяет, существуют ли связанные данные. Это полезно, чтобы гарантировать, что дочерняя запись будет создана только в том случае, если родительская запись также существует в базе данных.

Чтобы использовать ограничение внешнего ключа, сначала нужно определить внешний ключ в миграции. Часто для этого используются ссылки. Например, предположим, что вы создаёте таблицу комментариев, связанную с публикациями. Тогда можно сделать следующее:

create table(:comments) do
  add :post_id, references(:posts)
end

По умолчанию Ecto создаст ограничение внешнего ключа с именем "comments_post_id_fkey" (имя можно настроить).

Теперь, когда ограничение создано, при создании комментариев можно добавить к changeset ограничение внешнего ключа, чтобы Ecto знал, как преобразовать его в сообщение об ошибке:

cast(comment, params, [:post_id])
|> foreign_key_constraint(:post_id)

Теперь при вызове Ecto.Repo.insert/2 или Ecto.Repo.update/2, если связанная публикация не существует, базовая операция завершится ошибкой, но Ecto преобразует исключение базы данных в ошибку changeset и вернёт кортеж {:error, changeset}. Обратите внимание: ошибка возникнет только после обращения к базе данных, поэтому она будет обнаружена лишь после успешного прохождения всех остальных проверок. Если ограничение нарушено внутри транзакции, транзакция будет помечена как прерванная.

Параметры

  • :message — сообщение на случай неудачной проверки ограничения; по умолчанию "does not exist"
  • :name — имя ограничения. По умолчанию оно определяется по таблице и полю. Если задан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. В сложных случаях его может потребоваться указать явно
  • :match — способ сопоставления имени ограничения changeset с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. :suffix сопоставляет с любым ограничением репозитория, которое ends_with? :name этому ограничению changeset. :prefix сопоставляет с любым ограничением репозитория, которое starts_with? :name этому ограничению changeset.

get_assoc(changeset, name, as \\ :changeset)Источник

@spec get_assoc(t(), atom(), :changeset | :struct) ::
  [t() | Ecto.Schema.t()] | t() | Ecto.Schema.t() | nil

Получает запись или записи связи из изменений или из данных.

По умолчанию возвращаемые данные преобразуются в changeset. Передайте флаг :struct, чтобы получить данные в виде структур с применёнными изменениями, как и в случае с get_field/2.

Примеры

iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
...> |> change()
...> |> get_assoc(:posts)
[%Ecto.Changeset{data: %Post{id: 1, title: "hello"}, changes: %{}}]

iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
...> |> cast(%{posts: [%{id: 1, title: "world"}]}, [])
...> |> cast_assoc(:posts)
...> |> get_assoc(:posts, :changeset)
[%Ecto.Changeset{data: %Post{id: 1, title: "hello"}, changes: %{title: "world"}}]

iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
...> |> cast(%{posts: [%{id: 1, title: "world"}]}, [])
...> |> cast_assoc(:posts)
...> |> get_assoc(:posts, :struct)
[%Post{id: 1, title: "world"}]

get_change(changeset, key, default \\ nil)Источник

@spec get_change(t(), atom(), term()) :: term()

Получает изменение или возвращает значение по умолчанию.

Для ассоциаций и встраиваемых данных эта функция всегда возвращает nil, changeset или список changeset.

Примеры

iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
iex> get_change(changeset, :title)
"bar"
iex> get_change(changeset, :body)
nil

get_embed(changeset, name, as \\ :changeset)Источник

Получает встраиваемую запись или записи из изменений или из данных.

По умолчанию возвращаемые данные преобразуются в changeset. Передайте флаг :struct, чтобы получить данные в виде структур с применёнными изменениями, как и в случае с get_field/2.

Примеры

iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
...> |> change()
...> |> get_embed(:comments)
[%Ecto.Changeset{data: %Comment{id: 1, body: "hello"}, changes: %{}}]

iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
...> |> cast(%{comments: [%{id: 1, body: "world"}]}, [])
...> |> cast_embed(:comments)
...> |> get_embed(:comments, :changeset)
[%Ecto.Changeset{data: %Comment{id: 1, body: "hello"}, changes: %{body: "world"}}]

iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
...> |> cast(%{comments: [%{id: 1, body: "world"}]}, [])
...> |> cast_embed(:comments)
...> |> get_embed(:comments, :struct)
[%Comment{id: 1, body: "world"}]

get_field(changeset, key, default \\ nil)Источник

@spec get_field(t(), atom(), term()) :: term()

Получает поле из изменений или из данных.

В то время как get_change/3 для получения значения проверяет только текущий changes, эта функция проверяет изменения, а затем обращается к данным, возвращая в итоге default, если значение недоступно.

Для ассоциаций и встраиваемых данных эта функция всегда возвращает nil, структуру или список структур. При наличии изменений данные changeset будут содержать все применённые изменения. Это гарантирует согласованный результат независимо от того, были ли применены изменения. Используйте get_change/2 или get_assoc/3/get_embed/3, если нужно получить связи в виде changeset или более точно управлять результатом.

iex> post = %Post{title: "A title", body: "My body is a cage"}
iex> changeset = change(post, %{title: "A new title"})
iex> get_field(changeset, :title)
"A new title"
iex> get_field(changeset, :not_a_field, "Told you, not a field!")
"Told you, not a field!"

merge(changeset1, changeset2)Источник

@spec merge(t(), t()) :: t()

Объединяет два changeset.

Эта функция объединяет два changeset, если они применены к одним и тем же данным (их поле :data совпадает); если данные различаются, возникает исключение ArgumentError. Если поле :repo одного из changeset не равно nil, значение этого поля используется как поле :repo результирующего changeset; если оба changeset имеют значение, отличное от nil, и разные значения поля :repo, возникает исключение ArgumentError.

Остальные поля объединяются по следующим правилам:

  • params — параметры объединяются (без глубокого объединения), причём при конфликте приоритет имеют параметры changeset2. Если для обоих changeset в поле :params установлено значение nil, в результирующем changeset поле параметров также будет иметь значение nil.
  • changes — изменения объединяются, причём приоритет имеют изменения changeset2.
  • errors и validations — просто объединяются в один список.
  • required — обязательные поля объединяются; все поля, входящие в список обязательных полей обоих changeset, добавляются в список обязательных полей результирующего changeset.

Примеры

iex> changeset1 = cast(%Post{}, %{title: "Title"}, [:title])
iex> changeset2 = cast(%Post{}, %{title: "New title", body: "Body"}, [:title, :body])
iex> changeset = merge(changeset1, changeset2)
iex> changeset.changes
%{body: "Body", title: "New title"}

iex> changeset1 = cast(%Post{body: "Body"}, %{title: "Title"}, [:title])
iex> changeset2 = cast(%Post{}, %{title: "New title"}, [:title])
iex> merge(changeset1, changeset2)
** (ArgumentError) different :data when merging changesets

no_assoc_constraint(changeset, assoc, opts \\ [])Источник

@spec no_assoc_constraint(t(), atom(), Keyword.t()) :: t()

Проверяет, что связанное поле не существует.

Эта функция похожа на foreign_key_constraint/3, но поле определяется по описанию ассоциации. Это полезно, чтобы гарантировать, что родительскую запись можно удалить (или изменить её первичный ключ), только если в базе данных нет дочерних записей. Поэтому функция применима только к ассоциациям has_*.

Как следует из названия, для работы этой функции в базе данных должно быть создано ограничение. Обычно такое ограничение добавляется в виде ссылки на дочернюю таблицу:

create table(:comments) do
  add :post_id, references(:posts)
end

Теперь при удалении публикации можно запретить удаление любой публикации, если с ней всё ещё связаны комментарии:

post
|> Ecto.Changeset.change
|> Ecto.Changeset.no_assoc_constraint(:comments)
|> Repo.delete

Параметры

  • :message — сообщение на случай неудачной проверки ограничения; по умолчанию "is still associated with this entry" (для has_one) и "are still associated with this entry" (для has_many)
  • :name — имя ограничения. По умолчанию оно определяется по таблице и полю. Если задан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. В сложных случаях его может потребоваться указать явно
  • :match — способ сопоставления имени ограничения changeset с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. :suffix сопоставляет с любым ограничением репозитория, которое ends_with? :name этому ограничению changeset. :prefix сопоставляет с любым ограничением репозитория, которое starts_with? :name этому ограничению changeset.

optimistic_lock(data_or_changeset, field, incrementer \\ &increment_with_rollover/1)Источник

@spec optimistic_lock(Ecto.Schema.t() | t(), atom(), (term() -> term())) :: t()

Применяет оптимистическую блокировку к changeset.

Оптимистическая блокировка (или оптимистическое управление параллельным доступом) — это метод, позволяющий одновременно редактировать одну запись. Пессимистическая блокировка блокирует ресурс на время всей транзакции, а оптимистическая проверяет, изменился ли ресурс, непосредственно перед его обновлением.

Для этого запись регулярно извлекается из базы данных, а наличие изменений, внесённых другим пользователем, проверяется только при обновлении записи. Такой подход идеален в ситуациях, когда вероятность одновременного обновления одной и той же записи невелика; если это не так, лучше подойдут пессимистическая блокировка или другие способы управления параллельным доступом.

Использование

Оптимистическая блокировка работает за счёт счётчика «версий» для каждой записи; этот счётчик увеличивается при каждом изменении записи. Поэтому для использования оптимистической блокировки в схеме должно быть поле для хранения версии. Обычно это поле имеет целочисленный тип, но поддерживаются и другие типы.

Примеры

Предположим, у нас есть схема Post (хранящаяся в таблице posts). Сначала добавим столбец версии в таблицу posts:

alter table(:posts) do
  add :lock_version, :integer, default: 1
end

Имя столбца может быть любым и не обязано быть :lock_version. Теперь добавим поле и в схему:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :lock_version, :integer, default: 1
  end

  def changeset(:update, struct, params \\ %{}) do
    struct
    |> Ecto.Changeset.cast(params, [:title])
    |> Ecto.Changeset.optimistic_lock(:lock_version)
  end
end

Теперь попробуем оптимистическую блокировку в действии:

iex> post = Repo.insert!(%Post{title: "foo"})
%Post{id: 1, title: "foo", lock_version: 1}
iex> valid_change = Post.changeset(:update, post, %{title: "bar"})
iex> stale_change = Post.changeset(:update, post, %{title: "baz"})
iex> Repo.update!(valid_change)
%Post{id: 1, title: "bar", lock_version: 2}
iex> Repo.update!(stale_change)
** (Ecto.StaleEntryError) attempted to update a stale entry:

%Post{id: 1, title: "baz", lock_version: 1}

При конфликте (когда ранее извлечённая запись обновляется, но с момента её извлечения эта же запись была изменена) возникает исключение Ecto.StaleEntryError.

Оптимистическая блокировка также работает с операциями удаления. Просто вызовите функцию optimistic_lock/3 для данных перед удалением:

iex> changeset = Ecto.Changeset.optimistic_lock(post, :lock_version)
iex> Repo.delete(changeset)

По умолчанию optimistic_lock/3 предполагает, что поле блокировки имеет целочисленный тип. Чтобы использовать другой тип, передайте третий аргумент, задающий способ генерации следующего значения:

iex> Ecto.Changeset.optimistic_lock(post, :lock_uuid, fn _ -> Ecto.UUID.generate end)

prepare_changes(changeset, function)Источник

@spec prepare_changes(t(), (t() -> t())) :: t()

Предоставляет функцию, которую репозиторий выполняет при вставке, обновлении или удалении.

Если переданный репозиторию changeset действителен, функция, переданная в prepare_changes/2, будет вызвана с changeset и должна вернуть changeset. Это позволяет разработчикам внести последние изменения в changeset или выполнить команды для обеспечения согласованности данных. Репозиторий доступен внутри функции в поле repo changeset. Если переданный репозиторию changeset недействителен, функция вызвана не будет.

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

Пример

Один из распространённых сценариев — обновление кэша счётчика; в этом случае при создании комментария обновляется количество комментариев у публикации:

def create_comment(comment, params) do
  comment
  |> cast(params, [:body, :post_id])
  |> prepare_changes(fn changeset ->
       if post_id = get_change(changeset, :post_id) do
         query = from Post, where: [id: ^post_id]
         changeset.repo.update_all(query, inc: [comment_count: 1])
       end
       changeset
     end)
end

Мы извлекаем репозиторий непосредственно из changeset комментария и используем update_all, чтобы обновить кэш счётчика одним запросом. В конце необходимо вернуть исходный changeset.

put_assoc(changeset, name, value, opts \\ [])Источник

@spec put_assoc(t(), atom(), term(), Keyword.t()) :: t()

Добавляет указанную запись или записи ассоциации в changeset как изменение.

Эта функция предназначена для работы с ассоциациями целиком. Например, если у публикации есть много комментариев, она позволяет сразу добавить, удалить или изменить все комментарии, автоматически вычисляя вставки, обновления и удаления путём сравнения переданных данных с уже хранящимися в базе. Если ваша цель — управлять отдельными ресурсами, например добавлять новый комментарий к публикации или обновлять публикацию, связанную с комментарием, использовать эту функцию не нужно. Мы рассмотрим это позже в разделе «Пример: добавление комментария к публикации».

Для этой функции связанные данные должны быть предварительно загружены, кроме случая, когда родительский changeset только что создан и ещё не сохранён. Отсутствующие данные приведут к вызову поведения :on_replace, определённого для ассоциации.

Для ассоциаций с кратностью один можно использовать nil, чтобы удалить существующую запись. Для ассоциаций с несколькими записями вместо этого можно передать пустой список.

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

Связанные данные можно передать в разных форматах:

  • карта или список ключевых слов, представляющие изменения, которые нужно применить к связанным данным. Карту или список ключевых слов можно передать для обновления связанных данных, если совпадают первичные ключи. Например, put_assoc(changeset, :comments, [%{id: 1, title: "changed"}]) найдёт комментарий с :id, равным 1, и обновит его заголовок. Если комментария с таким идентификатором нет, он будет создан автоматически. Поскольку передан только один комментарий, он заменит все остальные связанные комментарии. Во всех случаях ожидается, что ключи будут атомами. В отличие от cast_assoc и embed_assoc, переданная карта (или структура) никак не проверяется и вставляется как есть. Этот API в основном используется в скриптах и тестах, чтобы упростить одновременное создание схем с ассоциациями, например:

    Ecto.Changeset.change(
      %Post{},
      title: "foo",
      comments: [
        %{body: "first"},
        %{body: "second"}
      ]
    )
  • changeset — переданные changeset считаются каноническими данными, а связанные данные, уже хранящиеся в ассоциации, обновляются или заменяются. Например, при вызове put_assoc(post_changeset, :comments, [list_of_comments_changesets]) все комментарии с совпадающими идентификаторами будут обновлены в соответствии с changeset. Новые комментарии или комментарии, не связанные ни с одной публикацией, будут корректно связаны. Для уже связанных комментариев, идентификаторов которых нет в списке changeset, применяется настройка ассоциации :on_replace (можно выбрать вызов исключения, игнорирование операции, обновление или удаление). Если в любом из changeset есть изменения, они также будут сохранены.

  • структуры — переданные структуры считаются каноническими данными, а связанные данные, уже хранящиеся в ассоциации, заменяются. Например, при вызове put_assoc(post_changeset, :comments, [list_of_comments_structs]) все комментарии с совпадающими идентификаторами будут заменены новыми структурами. Новые комментарии или комментарии, не связанные ни с одной публикацией, будут корректно связаны. Для уже связанных комментариев, идентификаторов которых нет в списке changeset, применяется настройка ассоциации :on_replace (можно выбрать вызов исключения, игнорирование операции, обновление или удаление). В отличие от передачи changeset, изменения структур не отслеживаются. Иными словами, если изменить структуру комментария и передать её в put_assoc/4, обновления этой структуры не будут сохранены. Вместо этого необходимо использовать changeset, списки ключевых слов или карты. put_assoc/4 со структурами лишь гарантирует, что комментарии и родительские данные будут связаны. Это особенно полезно при связывании существующих данных, как мы увидим в разделе «Пример: добавление тегов к публикации».

После передачи родительского changeset в функцию Ecto.Repo все записи будут вставлены, обновлены или удалены в рамках одной транзакции.

Если требуется другое поведение или явный контроль над работой этой функции, можно отказаться от неё и использовать Ecto.Multi, чтобы задать порядок выполнения нескольких операций с базой данных над разными схемами и changeset.

Пример: добавление комментария к публикации

Представим, что у публикации есть много комментариев и вы хотите добавить новый комментарий к существующей публикации. Хотя для этого можно использовать put_assoc/4, это было бы неоправданно сложно. Рассмотрим пример.

Сначала извлечём публикацию со всеми существующими комментариями:

post = Post |> Repo.get!(1) |> Repo.preload(:comments)

Следующий подход неправильный:

post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "bad example!"}])
|> Repo.update!()

Приведённый выше пример неправильный, потому что put_assoc/4 всегда работает с полным набором данных. Поэтому такой пример фактически удалит все предыдущие комментарии, оставив только добавляемый комментарий. Вместо этого можно попробовать следующее:

post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "so-so example!"} | post.comments])
|> Repo.update!()

В этом примере мы добавляем новый комментарий в начало списка существующих комментариев. Ecto сравнит список комментариев, который сейчас находится в post, со списком переданных комментариев и корректно добавит новый комментарий в базу данных. Однако Ecto выполняет много работы, чтобы выяснить то, что было известно с самого начала: добавляется только один новый комментарий.

В подобных случаях, когда нужно работать только с одной записью, гораздо проще работать непосредственно с ассоциацией. Например, вместо этого можно задать ассоциацию post в комментарии:

%Comment{body: "better example"}
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:post, post)
|> Repo.insert!()

Или можно сразу связать комментарий с публикацией при его создании:

Ecto.build_assoc(post, :comments)
|> Ecto.Changeset.change(body: "great example!")
|> Repo.insert!()

Также можно просто задать post_id непосредственно в комментарии:

%Comment{body: "better example", post_id: post.id}
|> Repo.insert!()

Иными словами, если вам нужно работать только с частью данных, скорее всего, использовать put_assoc/4 не нужно. Вместо этого следует работать с другой стороной ассоциации.

Рассмотрим пример, в котором put_assoc/4 подходит для решения задачи.

Пример: добавление тегов к публикации

Представим, что вы получили набор тегов, которые нужно связать с публикацией. Предположим, что эти теги уже существуют и сохранены в базе данных. Допустим, данные поступают в следующем формате:

params = %{"title" => "new post", "tags" => ["learner"]}

Поскольку теги уже существуют, получим их все из базы данных и добавим непосредственно в публикацию:

tags = Repo.all(from t in Tag, where: t.name in ^params["tags"])

post
|> Repo.preload(:tags)
|> Ecto.Changeset.cast(params, [:title]) # No need to allow :tags as we put them directly
|> Ecto.Changeset.put_assoc(:tags, tags) # Explicitly set the tags

В этом случае мы всегда требуем, чтобы пользователь передавал все теги сразу, поэтому put_assoc/4 подходит отлично. Функция автоматически удалит все непереданные теги и корректно свяжет с публикацией все переданные теги.

Кроме того, поскольку сведения о тегах передаются в виде структур, напрямую полученных из базы данных, Ecto считает эти данные корректными и выполняет только минимально необходимую работу для связывания публикаций и тегов, не пытаясь обновить или сравнить поля структуры тега.

Хотя функция принимает аргумент opts, в настоящее время put_assoc/4 не поддерживает никаких параметров.

Дополнительные материалы

Дополнительные сведения о работе с ассоциациями, включая памятки и практические примеры, доступны в документации. См.:

  • Документацию по cast_assoc/3
  • памятку по ассоциациям
  • руководство по ограничениям и операциям upsert
  • руководство по полиморфным ассоциациям «многие ко многим»

put_change(changeset, key, value)Источник

@spec put_change(t(), atom(), term()) :: t()

Добавляет изменение для указанного key со значением value.

key — это атом, обозначающий любое поле, встраиваемые данные или ассоциацию в changeset. Обратите внимание, что value напрямую сохраняется в changeset без какой-либо проверки. Поэтому эта функция предназначена для работы с данными внутри приложения.

Если изменение уже присутствует, оно заменяется новым значением. Если значение изменения совпадает со значением данных changeset, изменения не добавляются (а существующие изменения удаляются).

При изменении встраиваемых данных и ассоциаций полный список допустимых значений см. в описании put_assoc/4.

Примеры

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = put_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "bar"}

iex> changeset = change(%Post{title: "foo"})
iex> changeset = put_change(changeset, :title, "foo")
iex> changeset.changes
%{}

put_embed(changeset, name, value, opts \\ [])Источник

@spec put_embed(t(), atom(), term(), Keyword.t()) :: t()

Добавляет указанную встраиваемую запись или записи в changeset как изменение.

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

Если встраиваемые данные не содержат изменений, они будут пропущены. Если они недействительны, changeset будет помечен как недействительный.

Список поддерживаемых значений и их поведение описаны в put_assoc/4. Если переданное значение не входит в этот список, возникнет исключение.

Хотя эта функция принимает аргумент opts, в настоящее время put_embed/4 не поддерживает никаких параметров.

reorder_assoc(changeset, name)Источник

@spec reorder_assoc(t(), atom()) :: t()

Изменяет порядок изменений для указанной ассоциации.

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

Например, если для поля :name задано уникальное ограничение и список изменений может привести к конфликтам, эта функция позволит расположить изменения в порядке: сначала удаления, затем обновления и вставки. Поведение :on_replace будет обработано автоматически.

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

Будьте осторожны при совместном использовании этой функции с параметром :sort_param в cast_assoc/3. Обе функции изменяют внутренний порядок ассоциации, поэтому воздействие этой функции нужно ограничить только операцией с базой данных.

Если вы хотите использовать собственную функцию сортировки, см. reorder_assoc/3.

Пример

iex> # assume `:comments` association has `on_replace: delete`
iex> cs = %Post{comments: [%Comment{id: 1, body: "hello"}, %Comment{id: 2, body: "bye"}]}
...> |> change()
...> |> put_assoc(:comments, [%Comment{id: 3, body: ""}, %Comment{id: 2, body: "hello"}])
...> |> reorder_assoc(:comments, sort_fn)
iex> cs.changes.comments
[%Ecto.Changeset{data: %Comment{id: 1}}, %Ecto.Changeset{data: %Comment{id: 2}}, %Ecto.Changeset{data: %Comment{id: 3}}]

reorder_assoc(changeset, name, sort_fn)Источник

@spec reorder_assoc(t(), atom(), (t(), t() -> boolean())) :: t()

Изменяет порядок изменений для заданной ассоциации с помощью пользовательской функции сортировки.

Поведение аналогично reorder_assoc/2, за исключением того, что позволяет пользователю определить собственную функцию сортировки. Она должна иметь арность 2, а её аргументами должны быть сравниваемые наборы изменений. Функция должна возвращать true, если первый набор изменений предшествует второму или находится с ним на одном месте, и false в противном случае.

Пример

iex> sort_fn = cs1, _cs2 ->
...>   # ensure inserts come first
...>   case cs1.action do
...>     :insert -> true
...>      _ -> false
...>   end
...> end
iex> cs = %Post{comments: [%Comment{id: 1, body: "hello"}]}
...> |> change()
...> |> put_assoc(:comments, [%Comment{id: 2, body: "hello"}, %Comment{id: 1, body: ""}])
...> |> reorder_assoc(:comments, sort_fn)
iex> cs.changes.comments
[%Ecto.Changeset{data: %Comment{id: 1}}, %Ecto.Changeset{data: %Comment{id: 2}}]

traverse_errors(changeset, msg_func)Источник

@spec traverse_errors(
  t(),
  (error() -> String.t()) | (t(), atom(), error() -> String.t())
) ::
  traverse_result()

Обходит ошибки набора изменений и применяет заданную функцию к сообщениям об ошибках.

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

Вместе с набором изменений передаётся функция, которая применяется к каждому сообщению об ошибке при обходе набора изменений. Функция обработки сообщений об ошибках получает кортеж ошибки {msg, opts}, например:

{"should be at least %{count} characters", [count: 3, validation: :length, min: 3]}

Примеры

iex> traverse_errors(changeset, fn {msg, opts} ->
...>   Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
...>     opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
...>   end)
...> end)
%{title: ["should be at least 3 characters"]}

Функция также может принимать три аргумента: changeset, field и кортеж ошибки {msg, opts}. Это полезно, если вы хотите извлечь правила валидации из changeset.validations, чтобы создать подробное описание ошибки.

traverse_validations(changeset, msg_func)Источник

@spec traverse_validations(
  t(),
  (validation() -> String.t()) | (t(), atom(), validation() -> String.t())
) :: traverse_result()

Обходит валидации набора изменений и применяет к ним заданную функцию.

Функция работает так же, как traverse_errors/2, но обрабатывает валидации набора изменений, а не ошибки.

Примеры

iex> traverse_validations(changeset, &(&1))
%{title: [format: ~r/pattern/, length: [min: 1, max: 20]]}

iex> traverse_validations(changeset, fn
...>   {:length, opts} -> {:length, "#{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)}"}
...>   {:format, %Regex{source: source}} -> {:format, "/#{source}/"}
...>   {other, opts} -> {other, inspect(opts)}
...> end)
%{title: [format: "/pattern/", length: "1-20"]}

unique_constraint(changeset, field_or_fields, opts \\ [])Источник

@spec unique_constraint(t(), atom() | [atom(), ...], Keyword.t()) :: t()

Проверяет наличие ограничения уникальности для указанного поля или списка полей.

Проверка ограничения уникальности выполняется базой данных. Если ограничение нарушено, Ecto преобразует это в ошибку набора изменений.

Чтобы использовать ограничение уникальности, сначала необходимо определить уникальный индекс в миграции:

create unique_index(:users, [:email])

Теперь, когда ограничение создано, при изменении пользователей мы можем пометить набор изменений ограничением уникальности, чтобы Ecto знал, как преобразовать его в сообщение об ошибке:

cast(user, params, [:email])
|> unique_constraint(:email)

Теперь при вызове Ecto.Repo.insert/2 или Ecto.Repo.update/2, если адрес электронной почты уже существует, базовая операция завершится неудачей, но Ecto преобразует исключение базы данных в ошибку набора изменений и вернёт кортеж {:error, changeset}. Обратите внимание: ошибка возникнет только после обращения к базе данных, поэтому она не будет видна, пока не пройдут все остальные проверки. Если ограничение нарушено внутри транзакции, транзакция будет помечена как прерванная.

Параметры

  • :message — сообщение на случай нарушения ограничения; по умолчанию — «уже занято»

  • :name — имя ограничения. По умолчанию имя ограничения определяется по таблице и полю. Если задан этот параметр, аргумент field указывает только поле, к которому будет добавлена ошибка. Для сложных случаев его может потребоваться задать явно

  • :match — способ сопоставления имени ограничения набора изменений с ограничением репозитория; возможны значения :exact, :suffix или :prefix. По умолчанию используется :exact. Значение :suffix сопоставляет любое ограничение репозитория, которое ends_with? :name этому ограничению набора изменений. Значение :prefix сопоставляет любое ограничение репозитория, которое starts_with? :name этому ограничению набора изменений.

  • :error_key — ключ, к которому будет добавлена ошибка набора изменений при нарушении ограничения; по умолчанию используется имя первого поля из заданного списка.

Сложные ограничения

Поскольку логика ограничений реализуется в базе данных, при их определении можно использовать все возможности базы данных. Например, предположим, что адреса электронной почты уникальны в пределах идентификатора компании:

# In migration
create unique_index(:users, [:email, :company_id])

# In the changeset function
cast(user, params, [:email])
|> unique_constraint([:email, :company_id])

Имя первого поля, :email в данном случае, будет использоваться как ключ ошибки в списке ключевых слов ошибок набора изменений. Например, приведённый выше вызов unique_constraint/3 создаст примерно следующее:

Repo.insert!(%User{email: "john@elixir.org", company_id: 1})
changeset = User.changeset(%User{}, %{email: "john@elixir.org", company_id: 1})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [email: {"has already been taken", []}]

В сложных случаях вместо того, чтобы полагаться на определение имени, лучше задать имя ограничения явно:

# In the migration
create unique_index(:users, [:email, :company_id], name: :users_email_company_id_index)

# In the changeset function
cast(user, params, [:email])
|> unique_constraint(:email, name: :users_email_company_id_index)

Секционирование

Если таблица секционирована, имя уникального индекса может отличаться для каждого раздела. Например, Postgres добавляет p<number> в середину имени ключа, как показано ниже:

users_p0_email_key
users_p1_email_key
...
users_p99_email_key

В этом случае можно совместно использовать параметры name и suffix, чтобы сопоставить такие динамические индексы:

cast(user, params, [:email])
|> unique_constraint(:email, name: :email_key, match: :suffix)

В некоторых случаях номер добавляется и к имени таблицы, и к имени индекса, в результате чего имя индекса выглядит так:

user_p0_email_idx2
user_p1_email_idx3
...
user_p99_email_idx101

В этом случае для сопоставления можно использовать Regex:

cast(user, params, [:email])
|> unique_constraint(:email, name: ~r/user_p+_email_idx+/)

Чувствительность к регистру

К сожалению, разные базы данных по-разному обеспечивают чувствительность к регистру. Например, в MySQL сравнения по умолчанию не учитывают регистр. В Postgres можно определить столбец без учёта регистра, используя тип/расширение :citext. В миграции:

execute "CREATE EXTENSION IF NOT EXISTS citext"
create table(:users) do
  ...
  add :email, :citext
  ...
end

Если по какой-либо причине ваша база данных не поддерживает столбцы без учёта регистра, можно явно преобразовывать значения в нижний регистр перед вставкой или обновлением:

cast(data, params, [:email])
|> update_change(:email, &String.downcase/1)
|> unique_constraint(:email)

unsafe_validate_unique(changeset, fields, repo, opts \\ [])Источник

@spec unsafe_validate_unique(t(), atom() | [atom(), ...], Ecto.Repo.t(), Keyword.t()) ::
  t()

Проверяет, что не существует другой записи с теми же значениями этих полей и другим первичным ключом.

Эта функция предназначена для быстрого предоставления обратной связи пользователям приложения. На неё не следует полагаться для обеспечения целостности данных: у неё есть состояние гонки, и она по своей сути небезопасна. Например, если эта проверка выполняется дважды за один и тот же промежуток времени (поскольку пользователь отправил форму дважды), обе проверки могут пройти, и в базе данных появятся дубликаты. Поэтому для защиты данных от повреждения также следует использовать unique_constraint/3.

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

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

Параметры

  • :message — сообщение на случай нарушения ограничения; по умолчанию — «уже занято». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

  • :error_key — ключ, к которому будет добавлена ошибка набора изменений при нарушении ограничения; по умолчанию используется имя первого поля из заданного списка.

  • :prefix — префикс, в котором выполняется запрос (например, путь схемы в Postgres или база данных в MySQL). Подробнее см. в документации Ecto.Repo.

  • :nulls_distinct — логическое значение, определяющее, считаются ли разные значения null различными (неравными). Если значение равно false, для значений nil будет проверяться уникальность. В противном случае проверка выполняться не будет. Это имеет смысл только в сочетании с уникальным индексом, который считает значения null равными, например с параметром NULLS NOT DISTINCT в Postgres 15. По умолчанию — true

  • :repo_opts — параметры, передаваемые в вызов Ecto.Repo.

  • :query — базовый запрос для проверки. По умолчанию используется схема набора изменений. Если первичный ключ задан, будет добавлено условие, исключающее из проверки саму строку набора изменений.

Примеры

unsafe_validate_unique(changeset, :city_name, repo)
unsafe_validate_unique(changeset, [:city_name, :state_name], repo)
unsafe_validate_unique(changeset, [:city_name, :state_name], repo, message: "city must be unique within state")
unsafe_validate_unique(changeset, [:city_name, :state_name], repo, prefix: "public")
unsafe_validate_unique(changeset, [:city_name, :state_name], repo, query: from(c in City, where: is_nil(c.deleted_at)))

update_change(changeset, key, function)Источник

@spec update_change(t(), atom(), (term() -> term())) :: t()

Обновляет изменение.

Заданная функция function вызывается со значением изменения, только если для key есть изменение. После вызова функция работает как put_change/3.

Обратите внимание: значение изменения по-прежнему может быть nil (если только поле не было помечено как обязательное с помощью validate_required/3).

Примеры

iex> changeset = change(%Post{}, %{impressions: 1})
iex> changeset = update_change(changeset, :impressions, &(&1 + 1))
iex> changeset.changes.impressions
2

validate_acceptance(changeset, field, opts \\ [])Источник

@spec validate_acceptance(t(), atom(), Keyword.t()) :: t()

Проверяет, что заданный параметр имеет значение true.

Обратите внимание: эта проверка оценивает только сам параметр, а не поле в схеме. Параметры подтверждения не требуется сохранять, поскольку по определению они всегда хранятся как true.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «должно быть принято». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_acceptance(changeset, :terms_of_service)
validate_acceptance(changeset, :rules, message: "please accept rules")

validate_change(changeset, field, validator)Источник

@spec validate_change(
  t(),
  atom(),
  (atom(), term() -> [{atom(), String.t()} | {atom(), error()}])
) :: t()

Проверяет заданное изменение field.

Функция validator выполняет проверку, только если существует изменение для заданного field и его значение не равно nil. Функция должна возвращать список ошибок (пустой список означает отсутствие ошибок).

Если есть хотя бы одна ошибка, список ошибок будет добавлен в поле :errors набора изменений, а флаг :valid? будет установлен в false.

Примеры

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = validate_change changeset, :title, fn :title, title  ->
...>   # Value must not be "foo"!
...>   if title == "foo" do
...>     [title: "cannot be foo"]
...>   else
...>     []
...>   end
...> end
iex> changeset.errors
[title: {"cannot be foo", []}]

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = validate_change changeset, :title, fn :title, title  ->
...>   if title == "foo" do
...>     [title: {"cannot be foo", additional: "info"}]
...>   else
...>     []
...>   end
...> end
iex> changeset.errors
[title: {"cannot be foo", [additional: "info"]}]

validate_change(changeset, field, metadata, validator)Источник

@spec validate_change(
  t(),
  atom(),
  term(),
  (atom(), term() -> [{atom(), String.t()} | {atom(), error()}])
) :: t()

Сохраняет метаданные валидации metadata и проверяет заданное изменение field.

Работает аналогично validate_change/3, но сохраняет метаданные валидации в списке валидаторов набора изменений. Метаданные валидатора часто используются для рефлексии, чтобы автоматически создавать код на основе доступных валидаций.

Примеры

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = validate_change changeset, :title, :useless_validator, fn
...>   _, _ -> []
...> end
iex> changeset.validations
[title: :useless_validator]

validate_confirmation(changeset, field, opts \\ [])Источник

@spec validate_confirmation(t(), atom(), Keyword.t()) :: t()

Проверяет, совпадает ли заданный параметр с его подтверждением.

При вызове validate_confirmation(changeset, :email) эта проверка сравнивает значения параметров "email" и "email_confirmation". Обратите внимание: проверяются только сами параметры, а не поля схемы. Поэтому поле "email_confirmation" не нужно добавлять в схему в качестве виртуального поля.

Обратите внимание: если поле подтверждения отсутствует, ошибка валидации не добавляется. Это сделано намеренно: проверка подтверждения не запускается там, где подтверждение не требуется (например, в API). Можно сделать параметр подтверждения обязательным с помощью параметров функции (см. ниже).

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «не совпадает с подтверждением». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.
  • :required — логическое значение, определяющее, требуется ли наличие параметра подтверждения для добавления ошибки. По умолчанию — false

Примеры

validate_confirmation(changeset, :email)
validate_confirmation(changeset, :password, message: "does not match password")

cast(data, params, [:password])
|> validate_confirmation(:password, message: "does not match password")

validate_exclusion(changeset, field, data, opts \\ [])Источник

@spec validate_exclusion(t(), atom(), Enum.t(), Keyword.t()) :: t()

Проверяет, что изменение не входит в заданный перечислимый набор.

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «зарезервировано». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_exclusion(changeset, :name, ~w(admin superadmin))

validate_format(changeset, field, format, opts \\ [])Источник

@spec validate_format(t(), atom(), Regex.t(), Keyword.t()) :: t()

Проверяет, соответствует ли изменение заданному формату.

Формат должен быть задан регулярным выражением.

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «неверный формат». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_format(changeset, :email, ~r/@/)

validate_inclusion(changeset, field, data, opts \\ [])Источник

@spec validate_inclusion(t(), atom(), Enum.t(), Keyword.t()) :: t()

Проверяет, входит ли изменение в заданный перечислимый набор.

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «недопустимое значение». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_inclusion(changeset, :cardinal_direction, ["north", "east", "south", "west"])
validate_inclusion(changeset, :age, 0..99)

validate_length(changeset, field, opts)Источник

@spec validate_length(t(), atom(), Keyword.t()) :: t()

Проверяет, что изменение является строкой или списком заданной длины.

Обратите внимание: по умолчанию длина строки подсчитывается в графемах. Если эта проверка используется для соответствия ограничению на количество символов в базе данных, вероятно, это ограничение не учитывает графемы, а ограничивает число символов Unicode. В этом случае можно использовать параметр :count, чтобы ограничить число кодовых точек (:codepoints) или число байтов (:bytes).

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :is — длина должна точно соответствовать этому значению
  • :min — длина должна быть не меньше этого значения
  • :max — длина должна быть не больше этого значения
  • :count — способ подсчёта длины строки: :graphemes (по умолчанию), :codepoints или :bytes
  • :message — сообщение при неудачной проверке; зависит от типа проверки и может быть одним из следующих:
    • для строк:
      • «должно быть %{count} символов»
      • «должно быть не менее %{count} символов»
      • «должно быть не более %{count} символов»
    • для двоичных данных:
      • «должно быть %{count} байт»
      • «должно быть не менее %{count} байт»
      • «должно быть не более %{count} байт»
    • для списков и отображений:
      • «должно содержать %{count} элементов»
      • «должно содержать не менее %{count} элементов»
      • «должно содержать не более %{count} элементов». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_length(changeset, :title, min: 3)
validate_length(changeset, :title, max: 100)
validate_length(changeset, :title, min: 3, max: 100)
validate_length(changeset, :code, is: 9)
validate_length(changeset, :topics, is: 2)
validate_length(changeset, :icon, count: :bytes, max: 1024 * 16)

validate_number(changeset, field, opts)Источник

@spec validate_number(t(), atom(), Keyword.t()) :: t()

Проверяет свойства числа.

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :less_than
  • :greater_than
  • :less_than_or_equal_to
  • :greater_than_or_equal_to
  • :equal_to
  • :not_equal_to
  • :message — сообщение при неудачной проверке; по умолчанию используется одно из следующих:
    • «должно быть меньше %{number}»
    • «должно быть больше %{number}»
    • «должно быть меньше или равно %{number}»
    • «должно быть больше или равно %{number}»
    • «должно быть равно %{number}»
    • «должно быть не равно %{number}». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_number(changeset, :count, less_than: 3)
validate_number(changeset, :pi, greater_than: 3, less_than: 4)
validate_number(changeset, :the_answer_to_life_the_universe_and_everything, equal_to: 42)

validate_required(changeset, fields, opts \\ [])Источник

@spec validate_required(t(), list() | atom(), Keyword.t()) :: t()

Проверяет, присутствует ли в наборе изменений одно или несколько полей.

Можно передать имя одного обязательного поля или список имён обязательных полей.

Если значение поля равно nil или совпадает с одним из значений, заданных в changeset.empty_values (по умолчанию [] и ""), набор изменений помечается как недопустимый, поле удаляется из списка изменений набора, а также добавляется ошибка. Если для поля уже есть ошибка, новая ошибка добавлена не будет.

Не используйте эту функцию для проверки обязательных ассоциаций. Вместо этого передайте параметр :required в cast_assoc/3 или cast_embed/3.

В отличие от других проверок, эта функция не сохраняет валидацию под ключом changeset.validations. Вместо этого она сохраняет все обязательные поля под ключом changeset.required.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «не может быть пустым». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_required(changeset, :title)
validate_required(changeset, [:title, :body])

validate_subset(changeset, field, data, opts \\ [])Источник

@spec validate_subset(t(), atom(), Enum.t(), Keyword.t()) :: t()

Проверяет, что изменение типа enum является подмножеством заданного перечислимого набора.

Проверяется, входит ли список значений в заданный перечислимый набор. Чтобы проверить, входит ли в него отдельное значение, используйте вместо этого validate_inclusion/4.

Тип поля должен быть array.

Проверка выполняется, только если существует изменение для заданного field и его значение не равно nil.

Параметры

  • :message — сообщение при неудачной проверке; по умолчанию — «содержит недопустимую запись». Также может быть кортежем {msg, opts}, позволяющим задать дополнительные параметры при использовании traverse_errors/2.

Примеры

validate_subset(changeset, :pets, ["cat", "dog", "parrot"])
validate_subset(changeset, :lottery_numbers, 0..99)

validations(changeset)Источник

@spec validations(t()) :: [{atom(), term()}]

Возвращает список ключевых слов с валидациями этого набора изменений.

Ключи списка — имена полей, а значения — связанные с полями валидации. Поле может встречаться в списке несколько раз.

Пример

%Post{}
|> change()
|> validate_format(:title, ~r/^\w+:\s/, message: "must start with a topic")
|> validate_length(:title, max: 100)
|> validations()
#=> [
  title: {:length, [ max: 100 ]},
  title: {:format, ~r/^\w+:\s/}
]

В результат могут входить следующие валидации. Список не обязательно является исчерпывающим. Например, в возвращаемом значении также будут присутствовать пользовательские валидации, написанные разработчиком.

Первая группа содержит валидации со списком ключевых слов валидаторов. Этот список также может включать ключ :message.

  • {:length, [option]}

    • min: n
    • max: n
    • is: n
    • count: :graphemes | :codepoints

  • {:number, [option]}

    • equal_to: n
    • greater_than: n
    • greater_than_or_equal_to: n
    • less_than: n
    • less_than_or_equal_to: n

Для остальных валидаторов задаётся только значение:

  • {:exclusion, Enum.t}
  • {:format, ~r/pattern/}
  • {:inclusion, Enum.t}
  • {:subset, Enum.t}

Обратите внимание: вызов validate_required/3 не сохраняет валидацию под ключом changeset.validations, поэтому она не будет включена в результат этой функции. Обязательные поля сохраняются под ключом changeset.required.

Пакет Hex Предпросмотр Hex Просмотреть llms.txt Скачать версию ePub

Создано с помощью ExDoc (v0.40.2) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Changeset.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API