Phx.gen.html belongs_to/has_many error

Hi!

I ran the following command to create a resource:

mix phx.gen.html Main Activity activities name:string datetime_start:datetime datetime_end:datetime category_id:references:categories mood_id:references:moods

After some search I discovered that the inputs for the associations are not created automatically, so I tried to do it, for that I did:

  • Edit activity.ex schema and added:
+belongs_to :category, Categories
+belongs_to :mood, Moods
-field :category_id, :id
-field :mood_id, :id

Then I used the Main context functions to retrieve all categories and moods and pass it to the new.html form:

changeset = Main.change_activity(%Activity{})
+categories = Main.list_categories() |> Enum.map(fn c -> {c.name, c.id} end)
+moods = Main.list_moods() |> Enum.map(fn m -> {m.name, m.id} end)

+render(conn, "new.html", changeset: changeset, moods: moods, categories: categories)
+<%= label f, :category %>
+<%= select f, :category, @categories, prompt: "Choose" %>
+<%= error_tag f, :category %>
+
+<%= label f, :mood %>
+<%= select f, :mood, @moods, prompt: "Choose" %>
+<%= error_tag f, :mood %>

But when loading the page in the browser I get this error:

protocol Phoenix.HTML.Safe not implemented for #Ecto.Association.NotLoaded<association :category is not loaded> of type Ecto.Association.NotLoaded (a struct). This protocol is implemented for the following type(s): Decimal, Phoenix.LiveComponent.CID, Phoenix.LiveView.Component, Phoenix.LiveView.Comprehension, Phoenix.LiveView.JS, Phoenix.LiveView.Rendered, Time, Integer, Tuple, List, DateTime, NaiveDateTime, Float, Atom, Phoenix.HTML.Form, BitString, Date

What I am doing wrong?

Thanks!

After looking for information about this error, I edited the Main.change_activity function:

def change_activity(%Activity{} = activity, attrs \\ %{}) do
Activity.changeset(activity, attrs)                                                                                                                         
+ |> Repo.preload(:category)
end

Now I get this error:

UndefinedFunctionError at GET /activities/new
function Ecto.Changeset.__schema__/2 is undefined or private

It seems that Phoenix is doing an internal mapping with the fields in the form, after renaming the field from category to category_id, the form loads correctly.

But I think this will break something when editing/showing/updating the entities.

What is the recommended way to do this?

I added to the changeset function the validation for the Category and Mood inputs:

+|> assoc_constraint(:category, name: :category_id)
+|> assoc_constraint(:mood, name: :mood_id)

But I am afraid it is not working since the values are being inserted in the database with NULL values.

I managed to save it with the correct values after adding category_id and mood_id to the cast function!

I edited the Main get_activity function to preload the relationships:

def get_activity!(id) do 
  Repo.get!(Activity, id) 
+ |> Repo.preload([:category, :mood])
end

After inspecting the object when showing it I get this:

%Main.Activity{
  __meta__: #Ecto.Schema.Metadata<:loaded, "activities">,
  category: %Flytte.Main.Categories{
    __meta__: #Ecto.Schema.Metadata<:loaded, "categories">,
    activities: #Ecto.Association.NotLoaded<association :activities is not loaded>,
    color: "#000000",
    id: 2,
    inserted_at: ~N[2022-01-01 19:39:45],
    name: "Sleep",
    updated_at: ~N[2022-01-01 19:39:45]
  },
  category_id: 2,
  datetime_end: ~N[2017-01-01 00:00:00],
  datetime_start: ~N[2017-01-01 00:00:00],
  id: 8,
  inserted_at: ~N[2022-01-01 22:15:28],
  mood: %Flytte.Main.Moods{
    __meta__: #Ecto.Schema.Metadata<:loaded, "moods">,
    activities: #Ecto.Association.NotLoaded<association :activities is not loaded>,
    emoji: ":)",
    id: 2,
    inserted_at: ~N[2022-01-01 19:43:52],
    name: "Fine",
    updated_at: ~N[2022-01-01 19:43:52]
  },
  mood_id: 2,
  name: "Test5",
  updated_at: ~N[2022-01-01 22:15:28]
}

Please note that there is a category but also category_id, It this ok, to have these 2 values there? Shouldn’t it be only one?

1 Like

It is, that’s an implementation details of many ORM / data-mapper libraries. It’s quite fine.

1 Like

That’s how Ecto works. Whenever you add an association, you’ll get both fields. This works nicely with preloads:

post = Repo.get(Post, 123) |> Repo.preload([:author, :topic])
# You can now access the associated data
foo = post.author.name
2 Likes