Polymorphic embedded structs

ecto, elixir

Solution

I've recently solved a similar issue, and as I see it you have two options. Either you ...

Use a custom `Ecto.Type`

This allows you to exactly control what kind of data you want to encode into the field. By doing this you can retain the module of the struct and the fields with relative ease.

A possible implementation might look like this:

defmodule EctoStruct do
  use Ecto.Type

  def type, do: :map

  def cast(%_{} = struct), do: {:ok, struct}
  def cast(_), do: :error

  def dump(%module{} = struct) do
    data = %{
      "module" => Atom.to_string(module),
      "fields" => Map.from_struct(struct)
    }

    {:ok, data}
  end

  def load(%{"module" => module, "fields" => fields}) do
    module = String.to_existing_atom(module)
    fields = Enum.map(fields, fn {k, v} -> {String.to_existing_atom(k), v} end)

    {:ok, struct!(module, fields)}
  rescue
    _ -> :error
  end
end

With this in place you can "simply" use `field :my_struct, EctoStruct` in your schema.

Alternatively you ...

Reconsider your choice of database

A tree is an inherently connected data structure. Depending on your exact requirements and how deep your tree becomes, traversing said tree with Postgres can become very slow very fast.

While I solved the issue mentioned earlier I hit said performance issues quite early, having to use recursive joins and materialized views to stay close to usable response times.

Since then I switched over to a graph database (Neo4j) and my performance issues completely disappeared. This would also easily allow you to encode various differing struct types into your tree by making use of `Labels`.

Depending on your particular requirements this might be worth a thought.

Problem

I'm wanting to store a tree structure in Postgres, and I'm hoping to embed an arbitrary Elixir struct on each node of the tree, something like this: ``` defmodule Node do use Ecto.Schema schema "nodes" do belongs_to :parent_node, Node embeds_one :struct, ArbitraryDataType end end ``` However, I believe `embeds_one` requires a specific struct data type to be specified, which won't work for my case. Is there a way around this? My backup plan is to use two fields, one for the struct `type` and one for the struct `fields`, like this: ``` defmodule Node do use Ecto.Schema schema "nodes" do belongs_to :parent_node, Node field :struct_type, :string field :fields, :map end end ``` In order to save the record in the first place, I would need to use the `__struct__` field to determine the struct type. Then, I would use logic something like the following to rebuild the original struct after retrieving the node from the database: ``` Enum.reduce( retrieved_node.fields, String.to_atom("Elixir.#{retrieved_node.struct_type}") |> struct, fn {k,v}, s -> Map.put(s, String.to_atom(k), v) end ) ```

Original source