我有一个父组件和一个子组件。我想在孩子的同时创建父母,因为父母在没有孩子的情况下不能存在。具体来说,我有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
答案 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
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
这应该做的工作