Shulhi Sapli
Shulhi Sapli

Reputation: 2476

Ecto one to one polymorphic assocation

Apologies if this may sound like a dumb question but I am stumped by the errors thrown by Ecto.

I am trying to achieve a one-to-one polymorphic association. I have read the documentation regarding the ways to achieve polymorphic association in Ecto, but my requirements require a one-to-one relationship.

guard -> guard_user -> user
operator -> operator_user -> user

where

guard
-----
id
position

guard_user
----
guard_id
user_id

user
-----
id
email
password
name

the same goes for operator and operator_user table.

defmodule Example.Guards.Guard do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.Guards.Guard
  alias Example.Guards.GuardUser

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "guards" do
    field :position, :string

    has_one :guard_user, GuardUser
    has_one :user, through: [:guard_user, :user]

    timestamps()
  end

  @doc false
  def changeset(%Guard{} = guard, attrs \\ %{}) do
    guard
    |> cast(attrs, [:position])
    |> cast_assoc(:guard_user, required: true)
  end
end

defmodule Example.Guards.GuardUser do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.Guards.Guard
  alias Example.Accounts.User

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "guard_user" do
    belongs_to :guard, Guard
    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(guard_user, attrs \\ %{}) do
    guard_user
    |> cast_assoc(:user, required: true)
  end
end

defmodule Example.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.Accounts.User


  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "users" do
    field :email, :string
    field :password, :string

    timestamps()
  end

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:email, :password])
    |> validate_required([:email, :password])
    |> unique_constraint(:email)
  end
end

when I run my test,

test "create guard" do
  params = %{
    "position" => "Guard",
    "guard_user" => %{
      "user" => %{
        "email" => "[email protected]",
        "password" => "example"
      }
    }
  }

  changeset = Guard.changeset(%Guard{}, params)
  assert changeset.valid?
end

the following errors are thrown:

** (FunctionClauseError) no function clause matching in Ecto.Changeset.cast_relation/4

     The following arguments were given to Ecto.Changeset.cast_relation/4:

         # 1
         :assoc

         # 2
         %Example.Guards.GuardUser{__meta__: #Ecto.Schema.Metadata<:built, "guard_user">, guard: #Ecto.Association.NotLoaded<association :guard is not loaded>, guard_id: nil, id: nil, inserted_at: nil, updated_at: nil, user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: nil}

         # 3
         :user

         # 4
         [required: true]

     Attempted function clauses (showing 2 out of 2):

         defp cast_relation(type, %Ecto.Changeset{data: data, types: types}, _name, _opts) when data == nil or types == nil
         defp cast_relation(type, %Ecto.Changeset{} = changeset, key, opts)

     code: changeset = Guard.changeset(%Guard{}, params)

 stacktrace:
   (ecto) lib/ecto/changeset.ex:665: Ecto.Changeset.cast_relation/4
   (ecto) lib/ecto/changeset.ex:712: anonymous fn/4 in Ecto.Changeset.on_cast_default/2
   (ecto) lib/ecto/changeset/relation.ex:100: Ecto.Changeset.Relation.do_cast/5
   (ecto) lib/ecto/changeset/relation.ex:237: Ecto.Changeset.Relation.single_change/5
   (ecto) lib/ecto/changeset.ex:691: Ecto.Changeset.cast_relation/4
   test/example/guards/guard_test.exs:29: (test)

Upvotes: 0

Views: 815

Answers (2)

jamescheuk
jamescheuk

Reputation: 440

tl;dr

call "cast" function to convert the model struct into Ecto.Changeset struct.

defmodule Example.Guards.GuardUser do
  use Ecto.Schema
  import Ecto.Changeset
  alias Example.Guards.Guard
  alias Example.Guards.GuardUser
  alias Example.Accounts.User

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "guard_user" do
    belongs_to :guard, Guard
    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(%GuardUser{} = guard_user, attrs \\ %{}) do

    guard_user
    |> cast(attrs, []) # <----------------- here -----------
    |> cast_assoc(:user, required: true)
  end
end

The error message tried to tell you that there are two cast_relation functions which expect a %Ecto.Changeset but you passed in a %Example.Guards.GuardUser.

Upvotes: 1

Bingoabs
Bingoabs

Reputation: 609

According to the code you paste:

 @doc false   
 def changeset(guard_user, attrs \\ %{}) do
   guard_user
   |> cast_assoc(:user, required: true)   
 end

In fact, you should first Ecto.Changeset.cast(attrs, []),like this:

 @doc false   
 def changeset(guard_user, attrs \\ %{}) do
   guard_user
   |> cast(attrs, [])
   |> cast_assoc(:user, required: true)   
 end

Look Ecto document.Use the cast before cast_assoc, will make the attr to be a changeset, and you can leave out that the user's changeset/2 check user attrs that will be done underwater

And by the way, only create the new user or guard, you can use the cast_assoc.

If the user or guard already exists, I recommend to use put_assoc or build_assoc .

Upvotes: 1

Related Questions