belongs_to 关联的 Ecto 变更集
Ecto changeset for belongs_to association
我正在尝试复制我在 Rails Ecto 中习惯的行为。在 Rails 中,如果我有 Parent
和 Child
模型,并且 Child
属于 Parent
,我可以这样做:Child.create(parent: parent)
。这会将 Child
的 parent_id
属性分配给 parent
的 ID。
这是我的最小 Ecto 示例:
defmodule Example.Parent do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "parent" do
has_many :children, Example.Child
end
def changeset(parent, attributes) do
parent |> cast(attributes, [])
end
end
defmodule Example.Child do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "child" do
belongs_to :parent, Example.Parent
end
def changeset(child, attributes) do
child
|> cast(attributes, [:parent_id])
end
end
这是我想要的行为示例:
parent = %Example.Parent{id: Ecto.UUID.generate()}
changeset = Example.Child.changeset(%Example.Child{}, %{parent: parent})
# This should be the parent's ID!
changeset.changes.parent_id
我试过的
我已经尝试了几种不同的方法来让它在 Ecto 中工作,但我总是做不到。
child
|> cast(attributes, [:parent_id])
|> put_assoc(:parent, attributes.parent)
这似乎没有分配关联。
我试过直接投射关联:
child
|> cast(attributes, [:parent_id, :parent])
但这会产生一个 RuntimeError
告诉我使用 cast_assoc/3
。这似乎不是我想要的,但我还是试过了。
child
|> cast(attributes, [:parent_id])
|> cast_assoc(:parent, with: &Example.Parent.changeset/2)
这会产生一个 Ecto.CastError
。
最后,我尝试从 cast_assoc/3
中删除 :with
选项。
child
|> cast(attributes, [:parent_id])
|> cast_assoc(:parent)
但是我得到了同样的错误。
内置的 Ecto 函数似乎无法做到这一点。为了启用它,我自己写了:
defmodule Example.Schema do
@moduledoc """
This module contains schema helpers which can be mixed into any schema. In addition, it also
automatically sets the ID type to a UUID, and uses and imports the standard Ecto modules.
"""
defmacro __using__(_options) do
quote do
use Ecto.Schema
import Ecto.Changeset
@doc """
Allows an association to be assigned to a changeset from the changeset's data with minimal fuss.
You can either assign the association's ID attribute, or assign the association struct directly.
"""
def assign_assoc(changeset, attributes = %{}, name) do
name_string = to_string(name)
name_atom = String.to_existing_atom(name_string)
id_string = "#{name_string}_id"
id_atom = String.to_existing_atom(id_string)
cond do
Map.has_key?(attributes, name_string) ->
put_assoc(changeset, name_atom, attributes[name_string])
Map.has_key?(attributes, name_atom) ->
put_assoc(changeset, name_atom, attributes[name_atom])
Map.has_key?(attributes, id_string) ->
put_change(changeset, id_atom, attributes[id_string])
Map.has_key?(attributes, id_atom) ->
put_change(changeset, id_atom, attributes[id_atom])
true ->
changeset
end
end
@doc """
Validates that the given association is present either in the changeset's changes or its data.
"""
def validate_assoc_required(changeset, name) do
# NOTE: The name value doesn't use `get_field` because that produces an error when the
# association isn't loaded.
id_value = get_field(changeset, :"#{name}_id")
name_value = get_change(changeset, name) || Map.get(changeset.data, name)
has_id? = id_value != nil
has_value? = name_value != nil && Ecto.assoc_loaded?(name_value)
unless has_id? || has_value? do
add_error(changeset, name, "is required")
else
changeset
end
end
end
end
end
这两个函数使得向变更集添加 belongs_to
关联变得非常容易。
def changeset(child, attributes) do
child
|> cast(attributes, [])
|> assign_assoc(attributes, :parent)
|> validate_assoc_required(:parent)
end
这种方法可以让您随心所欲地分配关联。两种形式都适用于 Repo.insert
.
Example.Child.changeset(%Example.Child{}, %{parent: parent})
Example.Child.changeset(%Example.Child{}, %{parent_id: parent.id})
Example.Child.changeset(%Example.Child{parent: parent}, %{parent: nil})
Example.Child.changeset(%Example.Child{parent_id: parent_id}, %{parent_id: nil})
我正在尝试复制我在 Rails Ecto 中习惯的行为。在 Rails 中,如果我有 Parent
和 Child
模型,并且 Child
属于 Parent
,我可以这样做:Child.create(parent: parent)
。这会将 Child
的 parent_id
属性分配给 parent
的 ID。
这是我的最小 Ecto 示例:
defmodule Example.Parent do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "parent" do
has_many :children, Example.Child
end
def changeset(parent, attributes) do
parent |> cast(attributes, [])
end
end
defmodule Example.Child do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "child" do
belongs_to :parent, Example.Parent
end
def changeset(child, attributes) do
child
|> cast(attributes, [:parent_id])
end
end
这是我想要的行为示例:
parent = %Example.Parent{id: Ecto.UUID.generate()}
changeset = Example.Child.changeset(%Example.Child{}, %{parent: parent})
# This should be the parent's ID!
changeset.changes.parent_id
我试过的
我已经尝试了几种不同的方法来让它在 Ecto 中工作,但我总是做不到。
child
|> cast(attributes, [:parent_id])
|> put_assoc(:parent, attributes.parent)
这似乎没有分配关联。
我试过直接投射关联:
child
|> cast(attributes, [:parent_id, :parent])
但这会产生一个 RuntimeError
告诉我使用 cast_assoc/3
。这似乎不是我想要的,但我还是试过了。
child
|> cast(attributes, [:parent_id])
|> cast_assoc(:parent, with: &Example.Parent.changeset/2)
这会产生一个 Ecto.CastError
。
最后,我尝试从 cast_assoc/3
中删除 :with
选项。
child
|> cast(attributes, [:parent_id])
|> cast_assoc(:parent)
但是我得到了同样的错误。
内置的 Ecto 函数似乎无法做到这一点。为了启用它,我自己写了:
defmodule Example.Schema do
@moduledoc """
This module contains schema helpers which can be mixed into any schema. In addition, it also
automatically sets the ID type to a UUID, and uses and imports the standard Ecto modules.
"""
defmacro __using__(_options) do
quote do
use Ecto.Schema
import Ecto.Changeset
@doc """
Allows an association to be assigned to a changeset from the changeset's data with minimal fuss.
You can either assign the association's ID attribute, or assign the association struct directly.
"""
def assign_assoc(changeset, attributes = %{}, name) do
name_string = to_string(name)
name_atom = String.to_existing_atom(name_string)
id_string = "#{name_string}_id"
id_atom = String.to_existing_atom(id_string)
cond do
Map.has_key?(attributes, name_string) ->
put_assoc(changeset, name_atom, attributes[name_string])
Map.has_key?(attributes, name_atom) ->
put_assoc(changeset, name_atom, attributes[name_atom])
Map.has_key?(attributes, id_string) ->
put_change(changeset, id_atom, attributes[id_string])
Map.has_key?(attributes, id_atom) ->
put_change(changeset, id_atom, attributes[id_atom])
true ->
changeset
end
end
@doc """
Validates that the given association is present either in the changeset's changes or its data.
"""
def validate_assoc_required(changeset, name) do
# NOTE: The name value doesn't use `get_field` because that produces an error when the
# association isn't loaded.
id_value = get_field(changeset, :"#{name}_id")
name_value = get_change(changeset, name) || Map.get(changeset.data, name)
has_id? = id_value != nil
has_value? = name_value != nil && Ecto.assoc_loaded?(name_value)
unless has_id? || has_value? do
add_error(changeset, name, "is required")
else
changeset
end
end
end
end
end
这两个函数使得向变更集添加 belongs_to
关联变得非常容易。
def changeset(child, attributes) do
child
|> cast(attributes, [])
|> assign_assoc(attributes, :parent)
|> validate_assoc_required(:parent)
end
这种方法可以让您随心所欲地分配关联。两种形式都适用于 Repo.insert
.
Example.Child.changeset(%Example.Child{}, %{parent: parent})
Example.Child.changeset(%Example.Child{}, %{parent_id: parent.id})
Example.Child.changeset(%Example.Child{parent: parent}, %{parent: nil})
Example.Child.changeset(%Example.Child{parent_id: parent_id}, %{parent_id: nil})