How to associate tables with only the id?

I have this situation where I have two talbes. One “user_profile” and one “countries”. “user_profile” has a foreign key to “countries”. Here are the migrations:

  def change do
    create table(:user_profiles, primary_key: false) do
      # ... more columns
      add :country_code, references(:countries, [column: :code, type: :string]), size: 2, null: false

      timestamps()
    end
    create table(:countries, primary_key: false) do
      add :code, :string, size: 2, primary_key: true
      add :name, :string, null: false
    end

and the “user_profiles” schema:

  schema "user_profiles" do
    # ... more fields
    belongs_to :country, Country, references: :code, 
      foreign_key: :country_code, type: :string
    timestamps()
  end

  @required_fields ~W(birth_date area_of_residence)a
  @optional_fields ~W(first_name last_name)a

  def registration_changeset(struct,  params \\ %{}) do
    struct
    |> cast(params, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> assoc_constraint(:country)
  end

The params map that get passed to the changeset is

%{
  area_of_residence: "Emerald City", 
  birth_date: ~D[1990-12-01],
  country_code: "GR", 
  email: "voger@example.com", 
  first_name: "John",
  has_accepted_tos: true, 
  is18_or_older: true, 
  last_name: "Doe",
  password: "123456", 
  password_confirmation: "123456", 
  username: "voger5"
}

And the changeset reasonably doesn’t contain any change for the country field.

#Ecto.Changeset<action: nil,
 changes: %{area_of_residence: "Emerald City", birth_date: ~D[1990-12-01],
   first_name: "John", last_name: "Doe"}, errors: [],
 data: #Mysite.Accounts.UserProfile<>, valid?: true>

Now when I do a Repo.insert! in the changeset I get this exception

** (exit) an exception was raised:
    ** (Postgrex.Error) ERROR 23502 (not_null_violation): null value in column "country_code" violates not-null constraint

    table: user_profiles
    column: country_code

Failing row contains (22, John, Doe, 1990-12-01, null, 0, Emerald City, 2017-08-09 13:39:07.684616, 2017-08-09 13:39:07.68465, null).

One simple solution would be to just Repo.get the country from the database and do a put_assoc to the changeset. However I am curious how it can be done without having to call the database. Just cast the field and send it.

You should be able to add country_code to your list of @optional_fields and then be able to insert it without doing put_assoc.

But keep in mind that the created UserProfile will not have the Country preloaded. So if you need the country afterwards you might just as well use the put_assoc. That way it is available through .country on the resulting UserProfile.

2 Likes

EDIT: Thanks. It seems that while I was trying to learn all this stuff about customizing the fields I was passing country instead of country_code in the list on the cast. That’s why I was getting

** (exit) an exception was raised:
    ** (RuntimeError) casting assocs with cast/3 is not supported, use cast_assoc/3 instead

and thought it doesn’t work. Now it works. Thank you so much.

1 Like