So I wasted 5 hours on trying to solve this one. And finally gave up in hope of some communal support .
I have a LeadPrediction schema that belongs to Manufacturer. I followed all the webs wisdom and got the following code going:
defmodule Cybord.PredictionModels.LeadPrediction do
use Ecto.Schema
import Ecto.Changeset
alias Cybord.ComponentManufacturers.Manufacturer
@derive {Jason.Encoder, only: [:manufacturer]}
schema "lead_predictions" do
field :capture_method, :string
field :component_type, :string
field :package, :string
belongs_to :manufacturer, Manufacturer, references: :name, type: :string
timestamps()
end
@doc false
def changeset(lead_prediction, attrs) do
lead_prediction
|> cast(attrs, [:component_type, :package, :capture_method, :manufacturer_id])
|> validate_required([:component_type, :package, :capture_method])
|> cast_or_constraint_assoc(:manufacturer)
|> foreign_key_constraint(:manufacturer)
end
defp cast_or_constraint_assoc(changeset, name) do
{:assoc, %{owner_key: key}} = changeset.types[name]
if changeset.changes[key] do
assoc_constraint(changeset, name)
else
cast_assoc(changeset, name, required: true)
end
end
defmodule Cybord.ComponentManufacturers.Manufacturer do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:name, :string, autogenerate: false}
@derive {Phoenix.Param, key: :name}
@derive {Jason.Encoder, only: [:name]}
schema "manufacturers" do
timestamps()
end
@doc false
def changeset(manufacturer, attrs) do
manufacturer
|> cast(attrs, [:name])
|> unique_constraint(:name, name: :manufacturers_pkey)
|> validate_required([:name])
end
end
def create(conn, %{"lead_prediction" => lead_prediction_params}) do
with {:ok, %LeadPrediction{} = lead_prediction} <-
PredictionModels.create_lead_prediction_with_preloads(lead_prediction_params,
preloads: [:manufacturer]
) do
IO.puts("lead_prediction: #{inspect(lead_prediction)}")
conn
|> put_status(:created)
|> put_resp_header("location", Routes.lead_prediction_path(conn, :show, lead_prediction))
|> render("show.json", lead_prediction: lead_prediction)
end
end
def create_lead_prediction_with_preloads(attrs \\ %{}, preloads \\ []) do
preloads = Keyword.get(preloads, :preloads, [])
{:ok, prediction} =
%LeadPrediction{}
|> Repo.preload(preloads)
|> LeadPrediction.changeset(attrs)
|> Repo.insert()
prediction = get_lead_prediction_with_preloads!(prediction.id, preloads: [:manufacturer])
{:ok, prediction}
end
def get_lead_prediction_with_preloads!(id, preloads \\ []) do
preloads = Keyword.get(preloads, :preloads, [])
Repo.get!(LeadPrediction, id)
|> Repo.preload(preloads)
end
When I run this test:
test "renders lead_prediction when data is valid", %{conn: conn} do
conn =
post(conn, Routes.lead_prediction_path(conn, :create), lead_prediction: @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, Routes.lead_prediction_path(conn, :show, id))
assert %{
"id" => id,
"capture_method" => "some capture_method",
"component_type" => "some component_type",
"package" => "some package"
} = json_response(conn, 200)["data"]
end
I get the following error:
** (RuntimeError) cannot encode association :manufacturer from Cybord.PredictionModels.LeadPrediction to JSON because the association was not loaded.
You can either preload the association:
Repo.preload(Cybord.PredictionModels.LeadPrediction, :manufacturer)
Or choose to not encode the association when converting the struct to JSON by explicitly listing the JSON fields in your schema:
defmodule Cybord.PredictionModels.LeadPrediction do
# ...
@derive {Jason.Encoder, only: [:name, :title, ...]}
schema ... do
code: conn = get(conn, Routes.lead_prediction_path(conn, :show, id))
stacktrace:
(ecto 3.4.4) lib/ecto/json.ex:4: Jason.Encoder.Ecto.Association.NotLoaded.encode/2
(jason 1.2.1) lib/encode.ex:182: Jason.Encode.map_naive_loop/3
(jason 1.2.1) lib/encode.ex:183: Jason.Encode.map_naive_loop/3
(jason 1.2.1) lib/encode.ex:173: Jason.Encode.map_naive/3
(jason 1.2.1) lib/encode.ex:172: Jason.Encode.map_naive/3
(jason 1.2.1) lib/encode.ex:35: Jason.Encode.encode/2
(jason 1.2.1) lib/jason.ex:197: Jason.encode_to_iodata!/2
(phoenix 1.5.3) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4
(cybord 0.1.0) lib/cybord_web/controllers/lead_prediction_controller.ex:1: CybordWeb.LeadPredictionController.action/2
(cybord 0.1.0) lib/cybord_web/controllers/lead_prediction_controller.ex:1: CybordWeb.LeadPredictionController.phoenix_controller_pipeline/2
(phoenix 1.5.3) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2
(cybord 0.1.0) lib/cybord_web/endpoint.ex:1: CybordWeb.Endpoint.plug_builder_call/2
(cybord 0.1.0) lib/cybord_web/endpoint.ex:1: CybordWeb.Endpoint.call/2
(phoenix 1.5.3) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
test/cybord_web/controllers/lead_prediction_controller_test.exs:47: (test)
But when I inspect the lead_prediction in the LeadPredictionController :create i get:
lead_prediction: %Cybord.PredictionModels.LeadPrediction{__meta__: #Ecto.Schema.Metadata<:loaded, "lead_predictions">, capture_method: "some capture_method", component_type: "some component_type", id: 343, inserted_at: ~N[2020-07-04 17:38:57], manufacturer: %Cybord.ComponentManufacturers.Manufacturer{__meta__: #Ecto.Schema.Metadata<:loaded, "manufacturers">, inserted_at: ~N[2020-07-04 17:38:57], name: "some manufacturer", updated_at: ~N[2020-07-04 17:38:57]}, manufacturer_id: "some manufacturer", package: "some package", updated_at: ~N[2020-07-04 17:38:57]}
And as you see, it seems that everything is loaded as required.
I simply don’t understand what I am doing wrong.