在Ecto

时间:2016-12-01 08:31:07

标签: elixir phoenix-framework ecto

我有一个父组件和一个子组件。我想在孩子的同时创建父母,因为父母在没有孩子的情况下不能存在。具体来说,我有subscriptions has_many services

如果我的子模型的必需字段是外部约束,我该如何同时创建两个模型?我的变更集中出现错误,表明parent.id不能为空。

我知道我可以Repo.insert!(Subscription)然后使用Service创建subscription.id变更集,但我想知道是否可以同时创建两者?

我的父母和子女变更集列在下面:

家长(订阅)

def changeset(struct, params \\ %{}) do
# get the current time and add 30 days.
    {:ok, active_until} = DateTime.utc_now()
      |> DateTime.to_unix() |> Kernel.+(2592000) |> DateTime.from_unix()

    struct
    |> change(active_until: active_until)
    |> cast(params, [:active_until, :user_id])
    |> cast_assoc(:services)
    |> validate_required([:active_until])
end

儿童(服务)

def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:start_time, :frequency, :subscription_id])
    |> validate_required([:subscription_id])
    |> foreign_key_constraint(:subscription_id)
end

3 个答案:

答案 0 :(得分:1)

这是一个鸡蛋鸡问题:数据库引擎将唯一ID分配给主记录。因此,在一次交易中执行此操作是不可能的。

唯一的可能性是自己处理主表上的ID键,通过DB内部函数生成GUID(如MySQL中的UUID()或PostgreSQL中的CREATE SEQUENCE。)在这种情况下,可以提前调用此函数并明确设置ID

但我不建议使用后一种方法。

答案 1 :(得分:1)

正如Aleksei正确指出的那样,这是一个基于父实体id的鸡与蛋问题,在准备语句时,该父实体id无法用于Ecto关联。我认为,您要问的只有通过使用Ecto.Multi驱动的交易才有可能。事务将确保即使您成功插入了父实体,但子实体之一未能通过验证检查,整个事务也将被回滚并且不会出现不一致的情况。

这是总体思路。

  • 首先,在您的上下文模块中,定义一个用于执行事务插入的新函数:
  def create_parent_with_children(attrs \\ %{}) do
    Ecto.Multi.new()
    # Insert the parent entity
    |> Ecto.Multi.insert(:parent_entity, Parent.changeset(%Parent{}, attrs))
    # then use the newly created  parent's id to insert all children
    |> Ecto.Multi.merge(fn %{parent_entity: par} ->
      attrs["children"]
      |> Enum.reduce(Ecto.Multi.new, fn child, multi ->
        # important: name each child transaction with a unique name
        child_multi_id = :"#{child["uniq_field1"]}_#{child["uniq_field2"]}"
        Ecto.Multi.insert(multi, child_multi_id, %Child{parent_id: par.id}
          |> Child.changeset(child))
      end)
    end)
    |> Repo.transaction()
  end
  • 然后添加一个新的POST / create处理程序,以在父实体的REST控制器中为嵌套的父/子结构使用新定义的函数:
def create(conn, %{"parent" => %{"children" => _} = parent_attrs}) do
    with {:ok, %{parent: parent}} <- Context.create_parent_with_children(parent_attrs) do
... (same body as create_parent/2)

确保在现有的create / 2处理程序之前添加此之前,因为它在传入的JSON结构上使用更严格的匹配。

  • 最后但并非最不重要的一点是,在FallbackController中定义一个额外的错误处理程序,因为从上述方法返回的错误结构略有不同:
  def call(conn, {:error, failed_tran, %Ecto.Changeset{} = changeset, _parent}) do
    conn
    |> put_resp_header("x-failed-transaction", Atom.to_string(failed_tran))
    |> put_status(:unprocessable_entity)
    |> put_view(InterfixWeb.ChangesetView)
    |> render("error.json", changeset: changeset)
  end

答案 2 :(得分:0)

这是一个古老的问题,但如果有人和我一样落地,请回答。

def changeset(%Subscription{} = subscription, attrs) do
  subscription
  |> cast(attrs, [...])
  |> ...
  |> cast_assoc(:services, required: true)
  |> ...
end

def create_subscription(attrs \\ %{}) do
  %Subscription{}
  |> Subscription.changeset(attrs)
  |> Repo.insert()
end

这应该做的工作