Using simple belongs failed to insert new entity

141 views
Skip to first unread message

Benjamin Dreux

unread,
Nov 1, 2015, 12:40:29 PM11/1/15
to elixir-ecto
Hi,

I have a simple User model, and i want to make another one which is a "couple" of user.
Here is what i've done on the cli:

````
mix phoenix.gen.model Couple couples partner1_id:references:users partner2_id:references:user
````

My migration looks like this

````
defmodule OurChild.Repo.Migrations.CreateCouple do
  use Ecto.Migration

  def change do
    create table(:couples) do
      add :partner1_id, references(:users)
      add :partner2_id, references(:users)

      timestamps
    end
    create index(:couples, [:partner1_id])
    create index(:couples, [:partner2_id])

  end
end
````

And then here is what I'm doing in the controller's action that save a new couple based on thwo user that a re already recorded:

````
  def couple(conn, params) do 
    current_user_id = get_session(conn, :current_user)
    partner_email = String.downcase(params["partner_email"])
    user1 = OurChild.Repo.get_by(User, id: current_user_id) 
    user2 = OurChild.Repo.get_by(User, email: partner_email)

    changeset = Couple.changeset(%Couple{})
    Ecto.Changeset.put_change changeset, :partner1, user1
    Ecto.Changeset.put_change changeset, :partner2, user2

    unless changeset.valid? do
      send_resp conn, 500, "Invalid users for a new couple"
    else
      case OurChild.Repo.insert changeset do
        {:error, _} ->  send_resp conn, 500, "Failed to create couple"
        {:ok, _couple} -> 
          send_resp conn, 200, "Couple created"
      end
    end
````

Does anyone see something that i'm missing here ?





Benjamin Dreux

unread,
Nov 1, 2015, 12:41:40 PM11/1/15
to elixir-ecto
I should mention that this produce an invalid changeset (return false on changeset.valid? ), with no  error

Wendy Smoak

unread,
Nov 1, 2015, 1:30:49 PM11/1/15
to elixi...@googlegroups.com
This came up on Slack and I gave it a try... here is my sample app
that demonstrates the issue. (Mine has 'people' rather than 'users'.)

https://github.com/wsmoak/people_test/

And the controller that retrieves two people and attempts to create a couple:

https://github.com/wsmoak/people_test/blob/master/web/controllers/couple_controller.ex#L19-L40

As Benjamin said, the changeset is invalid, but there are no errors.

-Wendy

Wendy Smoak

unread,
Nov 1, 2015, 1:46:27 PM11/1/15
to elixi...@googlegroups.com
On Sun, Nov 1, 2015 at 12:40 PM, Benjamin Dreux <benji...@gmail.com> >

> user1 = OurChild.Repo.get_by(User, id: current_user_id)
> user2 = OurChild.Repo.get_by(User, email: partner_email)
>
> changeset = Couple.changeset(%Couple{})
> Ecto.Changeset.put_change changeset, :partner1, user1
> Ecto.Changeset.put_change changeset, :partner2, user2

If you're not going to use a pipeline I think you'd need intermediate
variable assignments here, otherwise later you're still working with
the original unmodified changeset (from line 3 above).

changeset = Couple.changeset(%Couple{})
changeset = Ecto.Changeset.put_change changeset, :partner1, user1
changeset = Ecto.Changeset.put_change changeset, :partner2, user2

Or I believe you can do it in one line:

changeset = Couple.changeset(%Couple{partner1: user1, partner2: user2})

(But doing this, I still get an invalid changeset w/ no errors.)

- Wendy

Wendy Smoak

unread,
Nov 2, 2015, 8:10:11 AM11/2/15
to elixir-ecto
Capturing some replies from Slack so they don't disappear...

diamondgfx [9:38 PM] @benzen I believe it has something to do with needing to build it via build since you’re trying to associate your couple with users, not user ids
diamondgfx [9:38 PM]Which means you’re going through the associations, and those are not being preloaded
diamondgfx [11:41 PM] @benzen https://github.com/Diamond/couples_ecto_example If you insert with just the ids it should work just fine

gjaldon [5:28 AM] @benzen @wsmoak: @diamondgfx is correct. keep in mind that the way you were using `Ecto.Changeset.put_change/3` only works for embedded models. For associations, you use `build` if you want to create the association to a model. if the association already exists and you want to ‘link’ it to a model, you can do `put_change` to its index field.

-Wendy

José Valim

unread,
Nov 2, 2015, 9:47:47 AM11/2/15
to elixi...@googlegroups.com
>     user1 = OurChild.Repo.get_by(User, id: current_user_id)
>     user2 = OurChild.Repo.get_by(User, email: partner_email)
>
>     changeset = Couple.changeset(%Couple{})
>     Ecto.Changeset.put_change changeset, :partner1, user1
>     Ecto.Changeset.put_change changeset, :partner2, user2

If you're not going to use a pipeline I think you'd need intermediate
variable assignments here, otherwise later you're still working with
the original unmodified changeset (from line 3 above).

changeset = Couple.changeset(%Couple{})
changeset = Ecto.Changeset.put_change changeset, :partner1, user1
changeset = Ecto.Changeset.put_change changeset, :partner2, user2

Or I believe you can do it in one line:

    changeset = Couple.changeset(%Couple{partner1: user1, partner2: user2})

Both code samples have the issue where they call:

changeset = Couple.changeset(%Couple{})

When no parameter is given, it will use :empty, which always marks the changeset as invalid. However, both code samples will still fail because you are attempting to set an association via a belongs_to (i.e. child to parent) and we only support setting from parent to child.

The solution is to simply pass the IDs directly. In the Couple model, mark both ID fields as required:

@required_fields ~w(partner1_id partner2_id)

Now, because both parameters are already sent through the form, we call in the controller just:

Couple.changeset(%Couple{}, couple_params)

This will also raise by default in case the given IDs do not exist. You can convert those database errors into form errors by using assoc_constraint. Altogether:


  @required_fields ~w(partner1_id partner2_id)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> assoc_constraint(:partner1)
    |> assoc_constraint(:partner2)
  end

In any case, there is a bug in Ecto. We should raise if you are trying to set a belongs_to association. I will fix this in master. Thanks for the report Benjamin and fantastic work on creating an application that reproduces the issue Wendy, very helpful and very welcome!
Reply all
Reply to author
Forward
0 new messages