Search code examples
elixirecto

put_assoc complains of wrong type


I have Ecto schemas representing a directed graph -- there is a Node schema with a self-referential many-to-many relation through an intermediate NodeEdge schema. i.e.

  schema "nodes" do
    field :name, :string

    many_to_many :edges, MyApp.Node,
      join_through: MyApp.NodeEdge,
      join_keys: [source: :id, target: :id]

    timestamps()
  end

  schema "node_edges" do
    belongs_to :source, MyApp.Node
    belongs_to :target, MyApp.Node
    timestamps()
  end

with the following initial migration:

create table(:nodes) do
  add :name, :string, null: false
  timestamps()
end

create table(:node_edges) do
  add :source, references(:nodes)
  add :target, references(:nodes)
  timestamps()
end

I'm trying to bulk insert edges like so:

now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)

new_edges =
  Enum.map(
    result.edges,
    &%NodeEdge{
      source: curr_name,
      target: &1,
      inserted_at: now,
      updated_at: now
    }
  )

node
|> Repo.preload(:edges)
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:edges, my_edges)
|> Repo.update!()

As far as I can tell, this is pretty close to the documentation for put_assoc/4. However, I'm getting an error:

** (ArgumentError) expected changeset data to be a Elixir.MyApp.Node struct, got: %MyApp.NodeEdge{__meta__: #Ecto.Schema.Metadata<:built, "node_edges">, id: nil, inserted_at: ~N[2019-06-25 23:18:35], source: "nodeA", source_id: nil, target: "nodeB", target_id: nil, updated_at: ~N[2019-06-25 23:18:35]}

Is the error perhaps that I don't know the IDs of the nodes in advance? Or can anyone see another problem?


Solution

  • Ab error is pretty self-explained: you are creating a list of NodeEdge, and then tries to apply this list to the association edges, which is Node type.

    Bunch of solutions:

    • Build Node list, instead of NodeEdge list. Put this list with put_assoc

    • Add relation to the node edges

      schema "nodes" do
      field :name, :string
      
      many_to_many :edges, MyApp.Node,
        join_through: MyApp.NodeEdge,
        join_keys: [source: :id, target: :id]
      
      has_many :node_edges, MyApp.NodeEdge, foreign_key: source
      
      timestamps()
      end
      

      Now, put your NodeEdge list here.

    • Simply create this list - it's absolutely self-explained. Because NodeEdge has relations - use them to build associations:

      %NodeEdge{
        source: put_assoc(:source, curr_name),
        target: put_assoc(:target, &1),
        inserted_at: now,
        updated_at: now
      }
      
      

      Build this list and then save it.