belongs_to 关联的 Ecto 变更集

Ecto changeset for belongs_to association

我正在尝试复制我在 Rails Ecto 中习惯的行为。在 Rails 中,如果我有 ParentChild 模型,并且 Child 属于 Parent,我可以这样做:Child.create(parent: parent)。这会将 Childparent_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})