将嵌套形式与Ecto.Multis和changeset结合使用

时间:2019-02-18 06:10:13

标签: elixir phoenix-framework

在我的应用程序的注册页面上,我尝试将嵌套表单(使用form_for/3inputs_for/4)与Ecto.Multis和changeset结合使用。

目标:如果一个或多个服务器端验证失败,则使用以前输入的信息重新呈现表单,以便用户可以进行更正,而不是从头开始。


该应用具有组织架构,其中有一个用户是创建者/所有者:

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:processDebugManifest'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:103)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:73)
    at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:101)
    at org.gradle.api.internal.tasks.execution.FinalizeInputFilePropertiesTaskExecuter.execute(FinalizeInputFilePropertiesTaskExecuter.java:44)
    at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:62)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
    at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:256)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:249)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:238)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:663)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:597)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: Manifest merger failed with multiple errors, see logs
    at com.android.builder.core.AndroidBuilder.mergeManifestsForApplication(AndroidBuilder.java:540)
    at com.android.build.gradle.tasks.MergeManifests.doFullTaskAction(MergeManifests.java:173)
    at com.android.build.gradle.internal.tasks.IncrementalTask.taskAction(IncrementalTask.java:106)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
    at org.gradle.api.internal.project.taskfactory.IncrementalTaskAction.doExecute(IncrementalTaskAction.java:50)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:124)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:113)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:95)
    ... 33 more

“组织”上下文具有此schema "organizations" do field :owner_id, :integer has_one :owner, MyApp.Accounts.User, references: :owner_id, foreign_key: :id end 函数:

create_organization

(我之所以使用Ecto.Multi的原因是,例如,如果用户插入失败,我希望整个操作都回滚。)

我的控制器动作:

def create_organization(org_attrs, user_attrs) do
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:organization, organization_changeset(%Organization{}, org_attrs))
  |> Ecto.Multi.run(:user, fn _repo, %{organization: organization} ->
    %User{organization_id: organization.id}
    |> Accounts.register_user_changeset(user_attrs)
    |> Repo.insert()
  end)
  |> Ecto.Multi.run(:update_organization, fn(_repo, %{user: user, organization: organization}) ->
    organization
    |> organization_creator_changeset(%{owner_id: user.id})
    |> Repo.update()
  end)
  |> Repo.transaction()
end

我的表单:

def new(conn, _params) do
  changeset = Organizations.create_organization_changeset(%Organization{owner: %User{}})
  conn
  |> render("new.html", changeset: changeset)
end


def create(conn, %{"organization" => %{"owner" => user_params}} = org_params) do
  case Organizations.create_organization(org_params, user_params) do
    {:ok, %{user: user}} ->
      conn
      |> put_status(:created)
      |> put_view(MyAppWeb.Accounts.AccountView)
      |> render("confirm.html", email: user.email)
    {:error, _resource, changeset, _changes} ->
      conn
      |> put_status(:unprocessable_entity)
      |> put_flash(:error, MyAppWeb.ErrorHelpers.transform_errors(changeset))
      |> render("new.html", changeset: changeset)
  end
end

问题:如果一切顺利,将成功创建组织和用户并确认.html呈现。但是,如果发生变更集错误,我会得到:

<%= form_for @changeset, account_path(@conn, :create), fn f -> %>

  <div class="row">
    <%= text_input :organization, :name, class: "form-control", required: true %>
    <%= label f, :organization_name %>
    <%= error_tag f, :name %>
  </div>

  <%= inputs_for f, :owner, fn u -> %>

    <div class="row">
      <%= email_input u, :email, class: "form-control", required: true %>
      <%= label u, :email %>
      <%= error_tag u, :email %>
    </div>

    <div class="row">
      <%= password_input u, :password, class: "form-control", pattern: ".{8,}", title: "Password must have 8 or more characters.", required: true %>
      <%= label u, :password %>
      <%= error_tag u, :password %>
    </div>

    <div class="row">
      <%= password_input u, :password_confirmation, class: "form-control", required: true %>
      <%= label u, :password_confirmation %>
      <%= error_tag u, :password_confirmation %>
    </div>

  <% end %>

<% end %>

相信,这是因为传递给new.html的Ecto.Changeset是独立的%User {}变更集:

could not generate inputs for :owner from MyApp.Accounts.User. Check the field exists and it is one of embeds_one, embeds_many, has_one, has_many, belongs_to or many_to_many

解决此问题的最佳方法是什么?

1 个答案:

答案 0 :(得分:0)

与其引入自己的重新实现以将多个嵌套/相关记录插入不同的表中,不如直接使用Ecto提供的功能:User中的Ecto.Changeset.put_assoc/4的变更集。

它将为您执行所有验证,并在出现问题时返回无效的changeset