Write changeset for belong_to and has_many

Hi all

I am trying to use ecto and understand the concept of belongs_to and has_many.
On github you can find the mix project.

When you are looking on module Relation.Country the changeset function, I do not know if the code is ok or not.

Can someone please help me and tell me, if I am usingbelongs_to and has_many in the right way. Of course I could design the model in other way, but here I am trying to use belongs_to and has_many.

I tried to test with:

iex(4)> changeset = Country.changeset(%Country{}, %{ name: "United Kingdom", country_iso: "GB", language_iso: "en" })                                                         
#Ecto.Changeset<action: nil, changes: %{name: "United Kingdom"}, errors: [],
 data: #Relation.Country<>, valid?: true>
iex(5)> alias Relation.Repo
iex(6)> Repo.insert! changeset

15:12:01.022 [debug] QUERY OK db=0.3ms
begin []

15:12:01.026 [debug] QUERY OK db=1.6ms
INSERT INTO "countries" ("name","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["United Kingdom", {{2016, 12, 4}, {14, 12, 1, 0}}, {{2016, 12, 4}, {14, 12, 1, 0}}]

15:12:01.094 [debug] QUERY OK db=67.1ms
commit []
%Relation.Country{__meta__: #Ecto.Schema.Metadata<:loaded, "countries">,
 country_iso: #Ecto.Association.NotLoaded<association :country_iso is not loaded>,
 country_iso_id: nil, id: 3, inserted_at: #Ecto.DateTime<2016-12-04 14:12:01>,
 language_iso: #Ecto.Association.NotLoaded<association :language_iso is not loaded>,
 language_iso_id: nil, name: "United Kingdom",
 updated_at: #Ecto.DateTime<2016-12-04 14:12:01>}

And the record does not get inserted at all.


I am way under qualified to answer this but here are some things that caught my eye

It feels more natural for a Country to have one country code than to belong to a country code. The relation I would put in here would be :has_one

A country also has one language except for countries that have more. Canada and Switzerland come in mind. Also some languages are used in more than one country. I think here it would be more appropriate a :many_to_many relationship.

You probably know that already and maybe it’s your design for educational purposes as you stated in your post. I just mention it just in case.

Onwards to your problem. I ran your code and the record is written alright in database.

but it doesn’t do what you think it should. The reason is that a simple string is not enough to create the association between the models. You have to use one of buld_assoc or cast_assoc functions from Ecto.Changeset or pass the language and code ids.

Unfortunately I can’t provide more details because I am learning right now my self.

I hope at least pointed you at the right direction.

Here is a working changeset. In your Relation.Country model add put_assoc

alias Relation.{LanguageCode, CountryCode, Repo}

  def changeset(struct, params \\ %{}) do
    |> cast(params, [:name])
    |> unique_constraint(:country_iso)
    |> put_assoc(:language_iso, Repo.get_by(LanguageCode, iso: params.language_iso))
    |> put_assoc(:country_iso, Repo.get_by(CountryCode, iso: params.country_iso))
    |> unique_constraint(:language_iso)
    |> validate_required([:name])


also in your changeset

changeset = Country.changeset(%Country{}, %{ name: "United Kingdom", country_iso: "GB", language_iso: "en" })

language_iso needs to be “EN” according to your seed data.

@voger, is it a good idea to have put_assoc with Repo in changeset? I always thought changeset should be a pure validation function. Writing it that way, you will need to connect to a DB for testing, which seems too much work.

1 Like

@bobbypriambodo, I don’t know. I am just learning myself and just presented a way it it may work. I am also interested in the appropriate usage pattern.

It should I’d say. put_assoc should be outside of that changeset function in my opinion.

1 Like

@OvermindDL1 I’m learning my self Ecto. So in order to create a better changeset could we use cast_assoc or that function should be outside the changeset as well. Thanks

What I do is this:

  • The models/schemas are fairly dim, just have a single changeset function to do validation that every-single-record needs, like having the non-null fields filled.
  • I have other modules that handle functionality, now not necessarily of a given schema but rather of a set of functionality. For example, I have a couple of tables to handle notifications, but I have a singular MyServer.Notifications module that handles getting the notifications, caching them, broadcasting them to websockets and pubsub, etc… My controllers/websockets just call into that library, never accessing schemas directly. I also use the Ecto.Multi patterns to be able to compose them together as well, quite convenient. But the cast_assoc calls would be here (in reality I do not use associations as I’ve had unending issues with them since I use mainly many-to-many, which ecto does not support well, so I build them manually).

Thanks @OvermindDL1 for taking the time to explain. I will update this topic with the solution, after playing around. And hope you could give some tips

1 Like