Has_many / through throws in Ecto "custom query did not return a map"

Hi,

As a new Elixir user, while trying to setup has_many, through relationship, I had the following exception:

> An.Repo.get(An.User, 42) |> An.Repo.preload(:followed)

(ArgumentError) cannot preload through association `followed` on `An.User` because custom query did not return a map.

When preloading through associations, the custom query must always return a map with at least all primary keys, got: `%An.Target{...}`

I don’t really understand the exception :confused: Here is the schema that I use if that can help somehow.

defmodule An.User do
  use An.Web, :model

  schema "users" do
    has_many :user_follows, An.UserFollow, foreign_key: :user_uid
    has_many :followed, through: [:user_follows, :target]

    timestamps()
  end
end

defmodule An.Target do
  use An.Web, :model

  @primary_key {:uid, :string, []}
  schema "targets" do
    field :name, :string

    has_many :user_follows, An.UserFollow, foreign_key: :target_uid
    has_many :followers, through: [:user_follows, :user]

    timestamps()
  end
end

defmodule An.UserFollow do
  use An.Web, :model

  schema "user_follows" do
    belongs_to :user, An.User, foreign_key: :user_uid
    belongs_to :target, An.Target, foreign_key: :target_uid, references: :uid, type: :string

    timestamps()
   end
end

Do you have any ideas ? Or at least can someone try to explain me what this error is about ?
Thank you in advance :slight_smile:

NB: I simplified the schemas to not bloat the topic, I can add more info if necessary !

2 Likes

Which Ecto version are you using? it looks like an Ecto bug. At least it is a poor error message that must be fixed. The only hint I have is to change this line:

has_many :user_follows, An.UserFollow, foreign_key: :target_uid

to also include the references:

has_many :user_follows, An.UserFollow, foreign_key: :target_uid, references: :uid

Does it work? If not, please reproduce the error in a sample app and I will gladly take a look.

2 Likes

Please find hereunder a sample app with the instructions to reproduce in the README.MD
https://github.com/QuentinFchx/ectobug

Do not hesitate to tell me if you have issues :slight_smile:

Hi @josevalim,

You’re probably very busy but do you think you’ll have the time to take a quick look at the sample app today or tomorrow ?

Thank you in advance :slight_smile:

When you pinged I was working on it. :slight_smile: It has been fixed in Ecto master!

2 Likes

Does this fix in Ecto 2.0.6? I have similar issue. The major difference is that one table use composite primary key. The schema of my legacy system blows my mind.

MySQL 5.6

defmodule Jpos.Role do
  use Jpos.Web, :model

  @primary_key {:secroleid, :id, autogenerate: true}
  @derive {Phoenix.Param, key: :secroleid}
  schema "securityroles" do
    field :secrolename, :string
    field :is_synchronized_sale_point, :boolean, default: false
    field :modulesallowed, :string

    has_many :users, Jpos.User, foreign_key: :fullaccess, references: :secroleid
    has_many :security_groups, Jpos.SecurityGroup, foreign_key: :secroleid, references: :secroleid
    has_many :groups_tokens, through: [:security_groups, :token]
  end

defmodule Jpos.SecurityToken do
  use Jpos.Web, :model

  @primary_key {:tokenid, :id, autogenerate: false}
  @derive {Phoenix.Param, key: :tokenid}
  schema "securitytokens" do
    field :tokenname, :string
    field :moudule_index, :integer

    has_many :security_groups, Jpos.SecurityGroup, references: :tokenid, foreign_key: :tokenid
  end

defmodule Jpos.SecurityGroup do
  use Jpos.Web, :model

  @primary_key false
  schema "securitygroups" do
    belongs_to :role, Jpos.Role, references: :secroleid, foreign_key: :secroleid, primary_key: true
    belongs_to :token, Jpos.SecurityToken, references: :tokenid, foreign_key: :tokenid, primary_key: true
    field :add_flag, :boolean, default: false
    field :edit_flag, :boolean, default: false
    field :delete_flag, :boolean, default: false
    field :audit_flag, :boolean, default: false
    field :process_flag, :boolean, default: false

  end

iex log =>

iex(7)> role |> Repo.preload(:groups_tokens)
[debug] QUERY OK source="securitygroups" db=1.8ms decode=2.8ms
SELECT s0.`secroleid`, s0.`tokenid`, s0.`add_flag`, s0.`edit_flag`, s0.`delete_flag`, s0.`audit_flag`, s0.`process_flag`, s0.`secroleid` FROM `securitygroups` AS s0 WHERE (s0.`secroleid` = ?) ORDER BY s0.`secroleid` [30]
[debug] QUERY OK source="securitytokens" db=2.2ms decode=1.5ms queue=0.1ms
SELECT s0.`tokenid`, s0.`tokenname`, s0.`moudule_index`, s0.`tokenid` FROM `securitytokens` AS s0 WHERE (s0.`tokenid` IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)) [1, 15, 101100, 101102, 101103, 101105, 101200, 101201, 101202, 101203, 101204, 101207, 101209, 101212, 101300, 101301, 101302, 101401, 101402, 101405, 101406, 101407, 101409, 101422, 101423, 101424, 101506, 101600, 101601, 101602, 101604, 101605, 101606, 101607, 101608, 101609, 101610, 101611, 101612, 101613, 101614, 101615, 101616, 101617, 101618, 101619, 101620, 101621, 101622, 101623, ...]
** (ArgumentError) cannot preload through association `groups_tokens` on `Jpos.Role` because custom query did not return a map.

When preloading through associations, the custom query must always return a map with at least all primary keys, got: `%Jpos.SecurityToken{__meta__: #Ecto.Schema.Metadata<:loaded, "securitytokens">, moudule_index: 0, security_groups: #Ecto.Association.NotLoaded<association :security_groups is not loaded>, tokenid: 1, tokenname: "訂單輸入/查詢及客戶資料存取"}`
    (elixir) lib/enum.ex:1184: Enum."-map/2-lists^map/1-0-"/2
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
       (iex) lib/iex/evaluator.ex:135: IEx.Evaluator.handle_eval/6
       (iex) lib/iex/evaluator.ex:128: IEx.Evaluator.do_eval/4
       (iex) lib/iex/evaluator.ex:108: IEx.Evaluator.eval/4
       (iex) lib/iex/evaluator.ex:27: IEx.Evaluator.loop/3