Updating entries through associative table without many_to_many

This post seems the most relevant, but I can’t figure out how to insert relational data. I have been beating my head against this for 3 days now and can’t figure out why if I use many_to_many, relational data inserts, but when I try to do has_many things like put_assoc stop working completely and I get all sorts of errors. Roughly, this is what I’m working with, I created a clean new app and am using cities and roads, with city_roads being the associative table (since roads could go through multiple cities), why I don’t want to just use many_to_many is because I want to do things like add a field to city_roads (say built_date :string or something) which many_to_many isn’t supporting. Additionally, even if I just have a one to many (say instead of city_roads I just have people with name string and belongs_to cities). I can find multitudes of examples on how to fetch the data, and I can find examples on how to add data with many_to_many, but hardly anything on adding data with has_many.

defmodule Example.City do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.CityRoad

  schema "cities" do
    field :name, :string
    has_many :city_roads, CityRoad
    has_many :roads, through: [:city_roads, :road]
  end

  def changeset(city, attrs) do
    city
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end

defmodule Example.Road do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.CityRoad

  schema "roads" do
    field :name, :string
    has_many :city_roads, CityRoad
    has_many :cities, through: [:city_roads, :city]
  end

  def changeset(road, attrs) do
    road
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end

defmodule Example.CityRoad do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.{City,Road}

  schema "city_roads" do
    belongs_to :city, City
    belongs_to :road, Road

    timestamps()
  end

  def changeset(city_road, attrs) do
    city_road
    |> cast(attrs, [:city_id, :road_id])
    |> validate_required([:city_id, :road_id])
  end
end

Then doing something like this to insert both the town “Townsville” and a road named “Street Ave” associated with it. I am pretty sure something like put_assoc needs to go below the changeset line but it just throws an error that the field might be read only, this also happens with the direct version of city has many people.

city_people = %{
  name: "Townsville", 
  roads: [%{name: "Street Ave"}]
}

%City{}
|> City.changeset(city_people)
|> Repo.insert()

Edit: I got people working as a direct has_many, had to preload, then do changeset, then do put_assoc, but when I try to apply this to city_roads it bombs out still, like it really hates it. I’m missing the link between City|>preload(:roads)|>changeset|>put_assoc|>insert with a one to many and doing many to many.

Edit2: Man I finally figured it out, I went back and followed https://hexdocs.pm/ecto/polymorphic-associations-with-many-to-many.html#content talking about all the boilerplate, and made sure I was passing in %{city_roads: [%{road: %{name: “name”}}]} and everything “just worked”. I must have been overthinking it before but cascading the cast_assoc worked. Mainly what I think got me was there’s just a lot of methods to learn and what they do, like cast vs put vs build, and preloading, but it makes sense now. I added the “built” field as string to city_roads and I can get it from city.city_roads and get built and road info from it.