Inserting model with association using create functions

I’m trying to insert 2 associated models into the repo using the create_xxx functions generated by “mix ecto.gen.json”. I haven’t found a way to do this without getting various errors and I couldn’t find a post on this anywhere.

I have two models: Menu and MenuItem where MenuItem has a FK to store the associated Menu.

The following works without any issues, both records are created in the database:

menu = %Cms.Content.Menu{name: "Test"}
menu_item = %Cms.Content.MenuItem{name: "Test Item", menu: menu}
Cms.Repo.insert!(menu_item)

But when I try to use the create_xxx functions I get an error:

menu = Cms.Content.create_menu(%{name: "Test Menu"})  # works fine
menu_item = Cms.Content.create_menu_item(%{name: "Test Item", menu: menu}). # error

[debug] QUERY ERROR db=6.5ms
INSERT INTO “menus” (“inserted_at”,“updated_at”) VALUES ($1,$2) RETURNING “id” [{{2018, 2, 28}, {10, 58, 43, 347401}}, {{2018, 2, 28}, {10, 58, 43, 347411}}]
[debug] QUERY OK db=0.3ms
rollback []
** (Postgrex.Error) ERROR 23502 (not_null_violation): null value in column “name” violates not-null constraint

table: menus
column: name

Why does Ecto try to insert the already created menu again? What is wrong here?

Here are the model definitions:

defmodule Cms.Content.Menu do
  use Ecto.Schema
  import Ecto.Changeset
  alias Cms.Content.{Menu, MenuItem}

  schema "menus" do
    field :name, :string
    has_many :menu_items, MenuItem

    timestamps()
  end

  @doc false
  def changeset(%Menu{} = menu, attrs) do
    menu
    |> cast(attrs, [:name])
    |> validate_required([:name])
    |> unique_constraint(:name, message: "Name is already taken.")
  end
end

defmodule Cms.Content.MenuItem do
  use Ecto.Schema
  import Ecto.Changeset
  alias Cms.Content.{Menu, MenuItem}

  schema "menu_items" do
    field :name, :string
    belongs_to :menu, Menu

    timestamps()
  end

  @doc false
  def changeset(%MenuItem{} = menu_item, attrs) do
    menu_item
    |> cast(attrs, [:name])
    |> put_assoc(:menu, [attrs.menu])
    |> validate_required([:name, :menu])
    |> unique_constraint(:name, message: "Name is already taken.")
    |> unique_constraint(:order, message: "Order number is already taken.")
  end
end

Please use ``` to format your code blocks.

```
code
```
1 Like
menu = Cms.Content.create_menu(%{name: "Test Menu"})

What’s returned from Cms.Content.create_menu/1? Is it {:ok, menu}?


I would probably avoid casting foreign keys in changesets (can open you up for vulnerabilities if attrs come from user input), but pass them to the create function “manually”.

@spec create_menu_item(map, for: %Menu{}) :: {:ok, %MenuItem{}} | {:error, Ecto.Changeset.t()}
def create_menu_item(attrs, for: %Menu{id: menu_id}) do
  %MenuItem{menu_id: menu_id}
  |> MenuItem.changeset(attrs)
  |> Repo.insert()
end

# for menu item
def changeset(%MenuItem{} = menu_item, attrs) do
  menu_item
  |> cast(attrs, [:name])
  # |> put_assoc(:menu, [attrs.menu])
  |> validate_required([:name, :menu])
  |> unique_constraint(:name, message: "Name is already taken.")
  |> unique_constraint(:order, message: "Order number is already taken.")
end

Usage

{:ok, %Menu{} = menu} = create_menu(%{name: "Test Menu"})
{:ok, %MenuItem{} = menu_item} = create_menu_item(%{name: "Test Item"}, for: menu)

Thanks for the help, that worked after removing the

|> validate_required([:name, :menu])

With the validation for :menu in place I always got an error message.

@idi527 what is that for: in the function head? I haven’t seen it before.

Just a keyword list.

2 Likes

Oh. I need more coffee :smiley: