Каков правильный способ обработки вложенных форм /ecto изменений в Phoenix?

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

Эффективно отношения между организациями и пользователями много для многих.

Я придумал следующее:

  • Пользовательская схема:

    defmodule MyApp.User do
    use MyApp.Web, :model
    
    schema "users" do
      field :name, :string
      field :email, :string
      field :password, :string, virtual: true
      field :password_hash, :string
    end
    
    def changeset(...) # validate email, password confirmation etc.
    
  • Схема организации:

    defmodule MyApp.Org do
      use MyApp.Web, :model
    
      schema "orgs" do
        field :official_name, :string
        field :common_name, :string
    
        has_many :org_staff_users, MyApp.OrgStaffUser
        has_many :users, through: [:org_staff_users, :user]
     end
    
     def changeset(model, params \\ :empty) do
      model
      |> cast(params, ~w(official_name common_name), [])
     end
    
     def provisioning_changeset(model, params \\ :empty) do
       model
       |> changeset(params)
       |> cast_assoc(:org_staff_users, required: true)
     end
    
  • Таблица соединений org_staff_users и соответствующая схема Ecto с user_id и org_id

  • Контроллер со следующим действием new:

     def new(conn, _params) do
       data = %Org{org_staff_users: [%User{}]}
       changeset = Org.provisioning_changeset(data)
       render(conn, "new.html", changeset: changeset)
     end
    
  • Шаблон со следующим отрывом:

     <%= form_for @changeset, @action, fn f -> %>
          <%= if @changeset.action do %>
            <div class="alert alert-danger">
              <p>Oops, something went wrong! Please check the errors below:</p>
              <ul>
                <%= for {attr, message} <- f.errors do %>
                  <li><%= humanize(attr) %> <%= message %></li>
                <% end %>
              </ul>
            </div>
          <% end %>
    
        <%= text_input f, :official_name, class: "form-control" %>
        <%= text_input f, :common_name, class: "form-control" %>
    
        <%= inputs_for f, :org_staff_users, fn i -> %>
            <%= text_input f, :email, class: "form-control" %>
            <%= text_input f, :password, class: "form-control" %>
            <%= text_input f, :password_confirmation, class: "form-control" %>
        <% end %>
    
        <%= submit "Submit", class: "btn btn-primary" %>
    <% end %>
    

Пока что так хорошо, форма хорошо выглядит.

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

Неясно, следует ли использовать один набор изменений (и как?) или явно три набора изменений на каждый объект (User, Org и таблица соединений).

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

Параметры, которые я получаю после отправки формы, находятся в пределах %{"org" => ...} карты, включая те, которые фактически связаны с User. Как я должен правильно создать форму?

Я прочитал недавно обновленный http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/ но я все равно смущаюсь.

FWIW, я на Phoenix 1.0.4, Phoenix Ecto 2.0 и Phoenix HTML 2.3.0.

Приветствуются любые советы.

Ответы

Ответ 1

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

if organization_changeset.valid? and Enum.all?(staff_changesets, & &1.valid?) do
  Repo.transaction fn ->
    Repo.insert!(organization_changeset)
    Enum.each staff_changesets, &Repo.insert!/1)
  end
end

Обратите внимание, что я выполняю valid? проверку наборов изменений, которая не является идеальной, поскольку она не учитывает ограничения. Если в наборах изменений есть ограничения, вам нужно использовать Repo.insert (без bang !).

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

<%= inputs_for f, :org_staff_users, fn org_staff -> %>
  <%= inputs_for org_staff, :user, fn user -> %>
    # Your user form here
  <% end %>    
<% end %>

Однако мы также будем поддерживать many_to_many, который сделает его полностью прямым.

Ответ 2

Используйте встроенные схемы, как описано здесь