It‘s not that it‘s not „good enough“, but not what you want in the first place.
By doing put_assoc(user_cs, :roles, …)
you essentially state: I want to edit the user and I want to edit roles related to the user, as in edit the Role
record(s) in the roles
table.
Sometimes that‘s exactly what you want to do.
It‘s not what you‘re doing here though. You wanted: Edit the user and edit the relationship to roles. That‘s a different operation.
ORMs seem to have taught people that the former kind of API would do the latter, which isn‘t how ecto works. Ecto doesn‘t „automagically“ relate record of existing things in the db, but expects you to be explicit about what you want to do. In the early days it didn‘t even have many to many, because if you‘re explicit it‘s a short form for has_many ← belongs_to, belongs_to → has_many.
Ecto also has has_many through, which allows you to preload through multiple levels of relationships between schemas, but being read only. Imo this should be prefered over many_to_many unless there‘s indeed a need to also write data on the relationship.
I personally find ectos approach a lot more approachable and logical, but it seems to mess with peoples expectations a lot.