Wrong INSERT values when having 2 has_many associations to the same model

Hi, I’m learning Ecto and I am having a weird situation with INSERT query :thinking:

Here are my models (import/alias are removed for clarity

defmodule MyModule.DataModel do
  use Ecto.Schema

  @primary_key {:id, Ecto.UUID, autogenerate: true}
  @foreign_key_type :binary_id

  schema "data_models" do
    field :name, :string

    belongs_to :project, Project

    has_many :child_models, DataModelRelation, foreign_key: :target_id, on_replace: :delete
    has_many :parent_models, DataModelRelation, foreign_key: :host_id, on_replace: :delete

    timestamps()
end

defmodule MyModule.DataModelRelation do
  use Ecto.Schema

  @type_options ["Belongs To": :belongs_to, "Has Many": :has_many]

  @primary_key {:id, Ecto.UUID, autogenerate: true}
  @foreign_key_type :binary_id

  schema "data_model_relations" do
    field :type, Ecto.Enum, values: Enum.map(@type_options, fn {_, value} -> value end)
    field :field, :string

    belongs_to :host, DataModel
    belongs_to :target, DataModel

    timestamps()
  end

And here is how I am creating the DataModel entry

  defp create_data_model_entry(project_id, name, schema) do
    %DataModel{}
    |> DataModel.changeset(%{
      project_id: project_id,
      name: name,
      schema: schema
    })
    |> Repo.insert()
  end

  defp create_model_relations({:ok, data_model}, child_models, parent_models) do
    child =
      Enum.map(child_models, &Map.put(&1, "host_id", data_model.id))

    parent =
      Enum.map(parent_models, &Map.put(&1, "target_id", data_model.id))

    data_model
    |> Repo.preload([:child_models, :parent_models])
    |> DataModel.changeset(%{"child_models" => child, "parent_models" => parent})
    |> IO.inspect()
    |> Repo.update()
  end

  def create_project_data_model(
        project_id,
        %{"name" => name, "schema" => schema} = args,
        _user_id \\ nil
      ) do
    create_data_model_entry(project_id, name, schema)
    |> create_model_relations(
      Map.get(args, "child_models", []),
      Map.get(args, "parent_models", [])
    )
  end

It’s working… kinda, except that the INSERT queries are wrong for the DataModelRelation table. I am able to create a new DataModel, correct amount of DataModelRelation rows but the data is wrong. For example, here are some INSERT statements

23:06:50.337 [debug] QUERY OK db=1.0ms
INSERT INTO "data_model_relations" ("type","field","host_id","target_id","id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7) [:has_many, "relation1", "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e", "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e", "2b117b7b-7cf8-4c5b-8b00-4710c6be3681", ~N[2023-08-16 20:06:50], ~N[2023-08-16 20:06:50]]
↳ anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4, at: lib/ecto/adapters/sql.ex:1203
23:06:50.338 [debug] QUERY OK db=1.0ms
INSERT INTO "data_model_relations" ("type","field","host_id","target_id","id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7) [:belongs_to, "relation2", "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e", "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e", "163f84c1-0a82-4190-a91a-c09734675565", ~N[2023-08-16 20:06:50], ~N[2023-08-16 20:06:50]]

and here is the IO.inspect() output just before I call Repo.update()

#Ecto.Changeset<
  action: nil,
  changes: %{
    child_models: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{
          type: :belongs_to,
          field: "relation2",
          host_id: "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e",
          target_id: "0ee4fed3-0d5e-4d5b-8aec-42ac471aceb2"
        },
        errors: [],
        data: #Kotini.Projects.DataModelRelation<>,
        valid?: true
      >
    ],
    parent_models: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{
          type: :has_many,
          field: "relation1",
          host_id: "aced1e16-d9a9-409a-a041-87242f4320ee",
          target_id: "a25e6d4c-b5e5-4273-9500-063c4e8e1e0e"
        },
        errors: [],
        data: #Kotini.Projects.DataModelRelation<>,
        valid?: true
      >
    ]
  },
  errors: [],
  data: #Kotini.Projects.DataModel<>,
  valid?: true
>

For some reason the host_id and target_id are always the same even though I specifically set them to different value. What I’m trying to achieve here is to model relationships between 2 DataModel rows, one row can have belongs_to/has_many relationship to different rows

I’m very new to Elixir so I might have missed something obvious :frowning:

Thank you in advance!!!

I’ve found the solution :slight_smile:

So… with this definition, Ecto will automatically populate target_id column of the DataModelRelation row to be created with the id of the current DataModel that is used to construct the changeset

    has_many :child_models, DataModelRelation, foreign_key: :target_id, on_replace: :delete

And because I set the input data incorrectly, target_id instead of host_id. Ecto later overwrites my target_id with the correct value which results in 2 same values :man_facepalming:

Welcome to the forum and I’m glad you solved your problem :tada:

1 Like