Ecto: Embeds with the same primary key can be inserted

Created on 5 Oct 2016  路  6Comments  路  Source: elixir-ecto/ecto

Environment

  • Elixir 1.3.3
  • PostgreSQL 9.5.4
  • Ecto 2.1.0-rc.1
  • postgrex 0.12.1
  • Arch Linux

    Current behavior

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]}

Expected behavior

Being the primary key, names should be unique.

Bug Intermediate Discussion

Most helpful comment

@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

All 6 comments

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

stavro picture stavro  路  4Comments

tverlaan picture tverlaan  路  3Comments

jbence picture jbence  路  3Comments

jordi-chacon picture jordi-chacon  路  4Comments

nathanjohnson320 picture nathanjohnson320  路  4Comments