belongs_to 关联的 Ecto 变更集

问题描述 投票:0回答:2

我正在尝试在 Ecto 内的 Rails 中复制我习惯的行为。在 Rails 中,如果我有

Parent
Child
模型,并且
Child
属于
Parent
,我可以这样做:
Child.create(parent: parent)
。这会将
parent_id
Child
属性分配给
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

最后,我尝试从

:with
中删除
cast_assoc/3
选项。

child
|> cast(attributes, [:parent_id])
|> cast_assoc(:parent)

但是我遇到了同样的错误。

elixir phoenix-framework ecto
2个回答
1
投票

这对于内置的 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})

0
投票

@LandonSchropp 的解决方案的一个小问题是表字段的名称是硬编码的,如果架构具有自定义字段名称,它将无法工作。这是更正它的版本:

      @spec assign_assoc(Changeset.t, atom() | String.t, map(), list()) :: Changeset.t
      def assign_assoc(changeset, name, attrs \\ %{}, opts \\ []) when is_map(attrs) and is_list(opts) do
        {:assoc, %{owner_key: id_field, field: name_field}} =
          Ecto.Changeset.assoc_constraint(changeset, name).types[name]
        id_string = Atom.to_string(id_field)
        name_str  = Atom.to_string(name_field)
        validate? = Keyword.get(opts, :required, false)

        cond do
          Map.has_key?(attrs, name_str) ->
            put_assoc(changeset, name_field, attrs[name_str])
            |> maybe_validate(validate?, id_field)

          Map.has_key?(attrs, name_field) ->
            put_assoc(changeset, name_field, attrs[name_field])
            |> maybe_validate(validate?, id_field)

          Map.has_key?(attrs, id_string) ->
            put_change(changeset, id_field, attrs[id_string])

          Map.has_key?(attrs, id_field) ->
            put_change(changeset, id_field, attrs[id_field])

          true ->
            changeset
        end
      end

      @doc """
      Validates that the given association is present either in the changeset's
      changes or its data.
      """
      def validate_required_assoc(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)

        (has_id? or has_value?) && changeset || add_error(changeset, name, "is required")
      end

      defp maybe_validate(changeset, true, name), do: validate_required_assoc(changeset, name)
      defp maybe_validate(changeset,    _,    _), do: changeset
© www.soinside.com 2019 - 2024. All rights reserved.