I have the ff. Parent schema:
defmodule MyApp.Parent do
use MyApp.Web, :model
schema "parent" do
field :name, :string
embeds_many :children, MyApp.Child
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name])
|> cast_embed(:children)
|> validate_required([:name])
end
end
I want to ensure that children's names are unique for a given parent. I'm trying to do this by making the children's name field the primary key:
defmodule MyApp.Child do
use MyApp.Web, :model
@primary_key {:name, :string, autogenerate: false}
embedded_schema do
field :age, :integer
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :age])
|> validate_required([:name])
end
end
However, when I try to add children with the same name, both children are added successfully:
iex(2)> Repo.insert! Parent.changeset(%Parent{}, %{name: "Alice", children: [%{name: "Bob"}, %{name: "Bob"}]})
[debug] QUERY OK db=6.0ms
INSERT INTO "parent" ("children","name","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" [[%{age: nil, name: "Bob"}, %{age: nil, name: "Bob"}], "Alice", {{2016, 10, 5}, {7, 34, 33, 658211}}, {{2016, 10, 5}, {7, 34, 33, 660880}}]
%MyApp.Parent{__meta__: #Ecto.Schema.Metadata<:loaded, "parent">,
children: [%MyApp.Child{age: nil, name: "Bob"},
%MyApp.Child{age: nil, name: "Bob"}], id: 1,
inserted_at: ~N[2016-10-05 07:34:33.658211], name: "Alice",
updated_at: ~N[2016-10-05 07:34:33.660880]}
Being the primary key, names should be unique.
That's the effect of allowing other primary keys than uuid. Given we have all the embeds each time, I think we can validate this. On the other hand, we usually don't emulate things that databases don't support, so I'm torn on this.
Should I just validate the children names in Parent.changeset/2?
For now yes. Although I do think we should provide some sort of validation, even if the database does not, as for embeds it is unlikely folks will be changing it directly in the database (maybe for PostgreSQL JSONB).
@aptinio
Yes, you can validate children names in Parent.changeset/2.
This is a temporary solution, until the bug is fixed:
defmodule MyApp.Parent do
use MyApp.Web, :model
schema "parents" do
field :name, :string
embeds_many :children, MyApp.Child
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name])
|> cast_embed(:children)
|> validate_required([:name])
|> ensure_children_have_unique_names
end
defp ensure_children_have_unique_names(%Ecto.Changeset{valid?: true, changes: %{children: children}} = changeset) do
names = Enum.map(children, fn(%{changes: %{name: name}}) -> name end)
uniq_names = Enum.uniq(names)
if length(names) == length(uniq_names) do
changeset
else
add_error(changeset, :children, "should have unique names")
end
end
defp ensure_children_have_unique_names(changeset) do
changeset
end
end
And the test (test/models/parent_test.exs):
test "cannot insert parent with children that have duplicate names" do
{:error, changeset} = Repo.insert(Parent.changeset(%Parent{}, %{name: "Alice", children: [%{name: "Bob"}, %{name: "Bob"}]}))
assert changeset.errors == [children: {"should have unique names", []}]
end
Okay @josevalim. We'll validate in Parent.changes/2 for now. Thanks! If you can point me in the right direction, I'm willing to help out in implementing some sort of validation for this.
Thanks @shhavel!
Most helpful comment
@aptinio
Yes, you can validate children names in
Parent.changeset/2.This is a temporary solution, until the bug is fixed:
And the test (
test/models/parent_test.exs):