Reputation: 2476
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
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
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