Introduction
I have a problem with relations in Ecto. I have two schemas User and Role, and I want to be able to insert a new User with a foreign key in DB to Role table.
The problem is every time I try to insert a new user, it doesn't have a relation to the Role table (role_id column is null in User table). I tried to build an assoc in order to create the relation but it doesn't work.
Current code
My User schema is:
schema "user" do
field :first_name, :string
field :last_name, :string
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
belongs_to :role, TestApp.Role
timestamps()
end
Role schema:
schema "role" do
field :name, :string
timestamps()
has_many :users, TestApp.User
end
I defined the User changeset as the following:
@required_fields ~w(first_name last_name email password)
@optional_fields ~w(encrypted_password)
@required_update_fields ~w()
@optional_update_fields ~w(first_name last_name email password encrypted_password role)
def create_changeset(struct, params \\ :empty) do
changeset(struct, params, @required_fields, @optional_fields)
end
def update_changeset(struct, params \\ :empty) do
changeset(struct, params, @required_update_fields, @optional_update_fields)
end
defp changeset(struct, params \\ :empty, required_field, optional_fiels) do
struct
|> cast(params, required_field, optional_fiels)
|> validate_format(:email, ~r/@/, message: "invalid format")
|> validate_length(:password, min: 5)
|> validate_confirmation(:password, message: "password does not match")
|> unique_constraint(:email, message: "email already taken")
|> generate_encrypted_password
end
And Role changeset:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name])
|> validate_required([:name])
|> unique_constraint(:name)
end
In my UserController, I have a create method:
def create(conn, %{"user" => user_params}) do
changeset = User.create_changeset(%User{}, user_params)
case User.insert(changeset) do
{:ok, user} ->
conn
|> put_status(:ok)
|> render(TestApp.UserRender, "show.json", user: user)
{:error, changeset} ->
handle_user_creation_validation_error(conn, changeset: changeset)
end
end
Tested solutions (without success)
I tested several options, the following list it's just an example of what I tried so far (too many tries). It doesn't have to compile at all, i'll just post it as information.
I tried to create a insert method inside User to build an assoc and save the changeset.
def insert(changeset) do
if changeset.valid? do
IEx.pry
TestApp.Role.find_by_name("user")
|> Ecto.build_assoc(:users)
|> Ecto.change(changeset.params)
|> Repo.insert
else
{:error, changeset}
end
end
I tried also get the role and build an assoc
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.build_assoc(:users)
|> Ecto.Changeset.put_assoc(:users, user)
|> Repo.insert!
I saw this example in the Ecto documentation
def insert(changeset) do
Repo.transaction fn ->
TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.Changeset.put_assoc(:users, [changeset])
|> Repo.insert
end
Current status
I was able to create relations between users and roles, but every time I inserted a new user a new role role was inserted also (I was shocked, it just create a one_to_one relation every time!).
I know there is a right way to this, but I wonder if it will imply a preload of :users
from a role first. I don't think that it's an acceptable solution, hope someone could explain all this about relations, preloads and saving the changesets.
Solution
Well, seems that it's necessary send the FK as a param in the User changeset, so I had to change the param field role
with role_id
.
A param body request example might be:
{
"user" : {
"first_name": "first name",
"last_name": "last name",
"email": "test@email",
"password": "longtestpassword",
"password_confirmation": "longtestpassword",
"role_id": 1
}
}
I added also a cast_assoc
to the changeset validation pipeline:
|> cast_assoc(:role)
The User.insert/1
implementation was removed and the controller calls Repo.insert/1
instead.
Thank you for your help!