Ecto 中的多态 has_many

Polymorphic has_many in Ecto

我已经阅读了很多关于 polymorphic associations in Ecto 的文章,我同意这样的观点,即在您的 table 之间有一个本地数据库引用是有利的。

然而,大多数时候多态 belongs_to 被引用,而不是相反 (has_many)。我仍然不确定如何正确处理它。

在我的例子中,它是关于 Phoenix 后端 API,我有一个 Page 模型,它有多个 Widgets。每个小部件都有自己的 table 和模型,因为它需要存储 return 不同的字段。假设我们有一个 TwoColumnWidget 和一个 ThreeColumnWidget,它们都具有对 Page 模型的引用 page_id

我该如何理想地建模,现在我有一个中间 Widget 模型(带数据库),它有一列用于每种可能的小部件类型,并选择其中一个有 id 的那个的列。这对我来说感觉很麻烦,因为我必须在数据库中为每个需要同步的小部件存储一个额外的行,以及为每个具体小部件获取正确 serializer/view 的麻烦。

由于我的领域有很多这种类型的关系,我想找到一个总体上更好的解决方案,以便于进一步开发。有什么指点吗?

我会假设您的意思是每个小部件类型都有自己的 table(模式)来回答这个问题。由于您似乎在小部件和页面之间存在一对多关系而不是多对多关系,因此您可能需要考虑一种简单的地图数组方法,假设您使用像 postgress 一样支持它的数据库。

您需要注意地图是使用字符串键而不是原子键保存在数据库中的。因此,如果我们将一个结构保存到数据库中,当您读回它时,它将是一个带有字符串键的映射。所以你需要像这样将它转换回地图:

defmodule MyApp.Utils do

  def cast(%{} = schema, params) do
    struct schema, map_to_atom_keys(params)
  end
  def cast(module, params) when is_atom(module), do: cast(module.__struct__, params)

  def map_to_atom_keys(%{} = params) do
    Enum.reduce(params, %{}, fn({k, v}, acc) ->
      Map.put(acc, to_atom(k), v)
    end)
  end

  defp to_atom(key) when is_atom(key), do: key
  defp to_atom(key) when is_binary(key), do: String.to_existing_atom(key)

  def item_type(%{} = item), do: item_type(item.__struct__)
  def item_type(item) do
    Module.split(item)
    |> Enum.reverse
    |> hd
    |> to_string
  end
end

您可以对小部件执行类似操作 table,其中可变数据存储为地图。如果存储结构名称,则可以将其转换回结构。

defmodule Widget do
  schema "widgets" do
    field :embedded_type, :string
    belongs_to :page, Page
    field :widget, :map
  end

  def changeset(struct, params) do
    struct
    |> cast(params, [:embedded_type, :page_id, :widget])
    |> handle_widget(params)
  end
  defp handle_widget(changeset, %{widget: widget}) do
    changeset
    |> put_change(:embedded_type, widget.__struct__ |> inspect)
  end
end

然后你可以使用上面的代码将它强制转换回来。

您还可以使用 embedded_schema 创建每个小部件类型,并使用 Ecto 为您进行转换。