У меня есть таблица учетных записей, которая ссылается на таблицу электронных писем, примерно так:
В настоящее время мой набор изменений для учетных записей использует cast_assoc для извлечения электронной почты:
|> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))
Но это означает, что мне нужно предоставить следующие данные:
%{
username: "test",
password: "secret",
emails: %{email: "[email protected]"} //<- nested
}
Я использую GraphQL, и для поддержки "регистровой" мутации формы:
register(username:"test", password:"secret", email: "[email protected]")
Мне нужно:
Есть ли способ реорганизовать это или изменить мой набор изменений, чтобы убрать вложенное поле? Я новичок в эликсире и экто.





Я нахожусь в подобной лодке (использую GraphQL), и я решил держаться как можно дальше от cast_assoc. Это меньше из-за сценария «создание» и больше из-за сценария «обновления».
Смотрим документацию для cast_assoc, вот там написано ...
- If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
- If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
- If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)
Сценарий 1 является вашим стандартным созданием, то есть ваши данные должны выглядеть похожий для вложенного ввода выше. (на самом деле это должен быть список из карты для ключа электронной почты.
Допустим, человек добавляет второй адрес электронной почты (вы указали, что это адрес «один ко многим» выше). Если ваш ввод выглядит так:
%{
id: 12345,
username: "test",
emails: [
%{email: "[email protected]"}
}
}
... это запускает оба сценария 1 (новый параметр, без идентификатора) а также сценарий 4 (дочерний элемент с идентификатором не указан), эффективно удаляя все предыдущие электронные письма. Это означает, что ваши параметры обновления действительно должны выглядеть так:
%{
id: 12345,
username: "test",
emails: [
%{id: 1, email: "[email protected]"},
%{email: "[email protected]}
]
}
... что для меня означает создание очереди большого количества дополнительных данных в запросах. Для чего-то вроде электронных писем - которых у пользователя вряд ли будет немного - стоимость невысока. Для более обильно созданной ассоциации - боль.
Вместо того, чтобы всегда помещать cast_assoc в ваш User.changeset, одним из вариантов было бы создание определенного набора изменений для регистрации, который использует приведение только один раз:
defmodule MyApp.UserRegistration do
[...schema, regular changeset...]
def registration_changeset(params) do
%MyApp.User{}
|> MyApp.Repo.preload(:emails)
|> changeset(params)
|> cast_assoc(:emails, required: true, with: &MyApp.Email.changeset(&1, &2))
end
end
Вам все равно нужно будет предоставить вложенное поле emails в свой ввод, что, возможно, облом, но, по крайней мере, тогда вы не загрязняете свой обычный набор изменений пользователя с помощью cast_assoc.
Последняя мысль: вместо того, чтобы заботиться о вложении вашего клиента, вы могли бы сделать это в своей функции преобразователя, специфичной для регистрации?
В своем приложении я использую phone_numbers аналогично тому, как вы делаете электронные письма (просто потому, что это моя бизнес-логика). Вместо обновления телефонных номеров он всегда создает или удаляет телефонные номера с помощью user_id. Это разрушает феномен «одной формы, которая делает все», но я сказал, что хорошо. Другой подход, который вы можете использовать, - это отдельный модуль, который объединяет все в транзакции.
Я скажу, я думаю, что cast_assoc имеет больше смысла в много, если ваши отношения связаны с has_one, поскольку в обновлении вы, по определению, хотите, чтобы старый ушел.
Ваш вопрос касается разных моментов в приложении, поэтому я предполагаю, что вы используете Phoenix> = 1.3, а также Абсент. Таким образом, мы можем поговорить о том, как может выглядеть ваш контекст и ваши преобразователи.
Обработка входящих запросов GraphQL включает прохождение двух уровней абстракции до достижения функций набора изменений в модулях вашего домена: во-первых, преобразователь; а затем контекстный модуль. Важной хорошей практикой является то, что ваш преобразователь должен вызывать только контекстные функции. Идея состоит в том, чтобы резолвер оставался не связанным с базовыми модулями домена, в которых находятся ваши схемы Ecto.
Затем вы можете использовать преобразователь для массажа ввода, чтобы он соответствовал ожиданиям вашей контекстной функции. Предполагая, что ваш контекст называется Accounts, ваш преобразователь может выглядеть примерно так:
def register(_root, %{username: username, password: password, email: email}, _info) do
args = %{username: username, password: password, emails: [%{email: email}]}
case Accounts.create_account(args) do
{:ok, %Account{} = account} ->
{:ok, account}
{:error, changeset} ->
{:error, message: "Could not register account", details: error_details(changeset)}
end
end
Что затем вызывает эту простую вспомогательную функцию, которая полагается на traverse_errors/2 для возврата всех сообщений проверки:
defp error_details(changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
end
Спасибо за ответ! Это похоже на то, что я делаю сейчас, но я также хотел бы вернуть ошибки проверки. traverse_errors/2 по-прежнему возвращает ошибки в серия вложенных карт, поэтому я получаю emails: {email: "has invalid format"}. Я пытаюсь возвращать ошибки в виде пар {field, message}, поэтому в итоге я возвращаю поле «emails.email» (обычно сглаживаю набор изменений), если я также не изменяю набор изменений в преобразователе register (который у меня проблемы с этим, поскольку я новичок в эликсире). Таким образом, массирование входа отлично работает, а вот массирование выхода - не очень.
Другой подход, который вы можете использовать, - это создание embedded_schema для регистрации и его собственного набора изменений, чтобы ошибки набора изменений выглядели «плоскими» при возврате клиенту. Как только все будет в формате valid?: true, вы можете разделить данные на разные схемы, поддерживаемые базой данных.
@cnorris Кажется, проблему можно решить, настроив логику в error_details/1. Вы согласны?
Спасибо, это похоже на кошмар. Я разделил свой набор изменений регистрации в соответствии с вашим предложением, но для решения описанного выше сценария обновления вы просто удалите cast_assoc и используете транзакцию (multi) или как вы подойдете к обновлению связанных данных в противном случае?