I have following schemas:
The first:
schema "countries_codes" do
# Country code based on ISO-2
field :iso, :string
field :name, :string
has_many :country, Country
timestamps
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:iso, :name])
|> validate_required([:iso, :name])
|> validate_length(:iso, max: 2)
end
the second:
schema "languages_codes" do
# Language code based on ISO-2
field :iso, :string
field :name, :string
has_many :country, Country
timestamps
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:iso, :name])
|> validate_required([:iso, :name])
|> validate_length(:iso, max: 2)
end
and the third:
schema "countries" do
belongs_to :country_iso, CountryCode
belongs_to :language_iso, LanguageCode
field :name, :string
timestamps
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:country_iso, :language_iso, :name])
|> cast_assoc(:country_iso)
|> validate_required([:country_iso, :language_iso, :name])
end
as you can see on the third table, the first and second field belong to other schema.
When I run the country
changeset function, I've got:
** (RuntimeError) casting assocs with cast/3 is not supported, use cast_assoc/3 instead
(ecto) lib/ecto/changeset.ex:440: Ecto.Changeset.type!/2
(ecto) lib/ecto/changeset.ex:415: Ecto.Changeset.process_param/8
(elixir) lib/enum.ex:1247: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
(ecto) lib/ecto/changeset.ex:391: Ecto.Changeset.do_cast/7
(busiket) web/models/country.ex:20: Busiket.Country.changeset/2
(elixir) lib/enum.ex:1184: Enum."-map/2-lists^map/1-0-"/2
priv/repo/seeds.exs:48: (file)
(elixir) lib/code.ex:363: Code.require_file/2
(mix) lib/mix/tasks/run.ex:71: Mix.Tasks.Run.run/1
(mix) lib/mix/task.ex:296: Mix.Task.run_task/3
(mix) lib/mix/cli.ex:58: Mix.CLI.run_task/2
(elixir) lib/code.ex:363: Code.require_file/2
My problem is, I do not know, how to write a changeset function for the schema countries
.
To fix error remove :country_iso
and :language_iso
form cast/3
in schema countries
.
Updated model Country
:
defmodule MyApp.Country do
use Ecto.Schema
import Ecto.Changeset
schema "countries" do
field :name, :string
belongs_to :country_iso, MyApp.CountryCode
belongs_to :language_iso, MyApp.LanguageCode
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :country_iso_id, :language_iso_id])
|> cast_assoc(:country_iso)
|> cast_assoc(:language_iso)
|> unique_constraint(:country_iso_id)
|> unique_constraint(:language_iso_id)
|> validate_required([:name])
end
end
Country
with new CountryCode
and LanguageCode
:changeset = Country.changeset(%Country{}, %{
name: "United Kingdom",
country_iso: %{iso: "GB", name: "United Kingdom"},
language_iso: %{iso: "en", name: "English"}
})
Repo.insert! changeset
Repo.insert! %CountryCode{iso: "GB", name: "United Kingdom"}
changeset = Country.changeset(%Country{}, %{
name: "United Kingdom",
country_iso: %{iso: "GB", name: "United Kingdom 2"},
language_iso: %{iso: "en", name: "English"}
})
{:error, changeset} = Repo.insert changeset
changeset.valid? # => false
changeset.changes[:country_iso].errors[:iso] # => {"has already been taken", []}
Need to add unique_constraint(:iso)
into CountryCode
changeset to make it work like that.
country_iso = Repo.insert! %CountryCode{iso: "GB", name: "United Kingdom"}
changeset = Country.changeset(%Country{}, %{
name: "United Kingdom",
country_iso_id: country_iso.id,
language_iso: %{iso: "en", name: "English"}
})
Repo.insert! changeset
country = Repo.one Country
country.country_iso_id == country_iso.id # => true
iso
as primary key for tables country_codes
and language_codes
See more - Ecto Custom Primary Keys
defmodule MyApp.Repo.Migrations.CreateCountryCode do
use Ecto.Migration
def change do
create table(:countries_codes, primary_key: false) do
add :iso, :string, size: 2, primary_key: true
add :name, :string
timestamps()
end
end
end
defmodule MyApp.Repo.Migrations.CreateLanguageCode do
use Ecto.Migration
def change do
create table(:languages_codes, primary_key: false) do
add :iso, :string, size: 2, primary_key: true
add :name, :string
timestamps()
end
end
end
defmodule MyApp.Repo.Migrations.CreateCountry do
use Ecto.Migration
def change do
create table(:countries) do
add :name, :string
add :country_iso, references(:countries_codes, column: :iso, type: :string, on_delete: :nothing)
add :language_iso, references(:languages_codes, column: :iso, type: :string, on_delete: :nothing)
timestamps()
end
create unique_index(:countries, [:country_iso, :language_iso])
end
end
defmodule MyApp.CountryCode do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:iso, :string, []}
schema "countries_codes" do
field :name, :string
has_many :countries, MyApp.Country
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:iso, :name])
|> validate_required([:iso, :name])
|> validate_length(:iso, max: 2)
|> unique_constraint(:iso)
end
end
defmodule MyApp.LanguageCode do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:iso, :string, []}
schema "languages_codes" do
field :name, :string
has_many :countries, MyApp.Country
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:iso, :name])
|> validate_required([:iso, :name])
|> validate_length(:iso, max: 2)
|> unique_constraint(:iso)
end
end
defmodule MyApp.Country do
use Ecto.Schema
import Ecto.Changeset
schema "countries" do
field :name, :string
belongs_to :country_code, MyApp.CountryCode, foreign_key: :country_iso, references: :iso, type: :string
belongs_to :language_code, MyApp.LanguageCode, foreign_key: :language_iso, references: :iso, type: :string
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :country_iso, :language_iso])
|> validate_required([:name, :country_iso, :language_iso])
|> foreign_key_constraint(:country_iso)
|> foreign_key_constraint(:language_iso)
end
end
Note foreign_key_constraint/3 function - it adds validation error for Country
changeset when nonexistent country_iso
or language_iso
is used.
I've also changed associations names to country_code
and language_code
Repo.insert! %CountryCode{iso: "GB", name: "United Kingdom"}
Repo.insert! %LanguageCode{iso: "en", name: "English"}
changeset = Country.changeset(%Country{}, %{name: "United Kingdom", country_iso: "GB", language_iso: "en"})
Repo.insert! changeset