I am trying to make an association between table.
I currently have a "users" table with the following fields:
id | password | first_name | last_name | role | |
---|---|---|---|---|---|
1 | teacher@example.io | hash | Bob | White | teacher |
2 | student@example.io | hash | Joe | Brown | student |
I am trying to implement two new user types: teachers and students
As such, I want to make two different tables for both user types which will reference the main table like so:
id | user_id | first_name | last_name |
---|---|---|---|
1 | 1 (reference to "users") | Bob | White |
id | user_id | first_name | last_name |
---|---|---|---|
1 | 2 (reference to "users") | Joe | Brown |
I generated a new migration and created the "teachers" table like so:
create table("teachers") do
add :user_id, references("users")
add :first_name, :string
add :last_name, :string
end
I then defined a schema for the teachers table like this:
schema "teachers" do
field :first_name, :string
field :last_name, :string
belongs_to :user, User
Lastly, I added this to the user schema:
has_many :teachers, Teacher
I have now successfully added the teachers table with my desired columns. The problem is, my registration form is only accommodating one changeset. The user schema does all the validation and adds the user into the “users” table when the registration is successful. But the “teachers” table remains empty. What would be the best practice to populate the teachers table with the registering user’s info?
The first thing to know is there is no one "best practice" for this.
In my experience, there are 2 different approaches:
cast_assoc
in your User.changeset
function.create_user
function, use Ecto.Multi
to create the :user
and then use this user ID to create the teacher
or student
.Neither of these 2 is "better". Use whichever one feels better for you.
I'll explain both approaches in more detail now:
cast_assoc
approachIn your accounts.ex
context, you would have:
def create_user(attrs) do
%User{}
|> User.creation_changeset(attrs)
|> Repo.insert()
end
The attrs
map would look like this:
%{
password: "unhashed-password"
student: %{
first_name: "Bob",
last_name: "Smith"
}
}
or this:
%{
password: "unhashed-password"
teacher: %{
first_name: "Bob",
last_name: "Smith"
}
}
I decided to create a changeset function just for creating a user, because maybe I sometimes want to just update a user without updating the associated teacher/student. So, in user.ex
file, I have this:
def creation_changeset(user, attrs) do
User
|> cast(attrs, @user_fields_to_cast)
|> cast_assoc(:teacher, with: &Teacher.user_creation_changeset/2)
|> cast_assoc(:student, with: &Student.user_creation_changeset/2)
end
And that's pretty much it.
The create_user
function should return a {:ok, %User{}}
(or {:error, %Ecto.Changeset{}}
) tuple with the teacher
and student
assocs loaded on the user (either teacher or student should be nil
).
You don't have to use dedicated "creation" changesets, but I have found it usually makes this easier.
Ecto.Multi
approachIf you're doing this, there's no need to use cast_assoc
and you can leave the User/Teacher/Student schemas as they are. Instead, the context function will look something like this:
alias Ecto.Multi
def create_user(attrs) do
{teacher_attrs, attrs} = Map.pop(attrs, :teacher)
{student_attrs, attrs} = Map.pop(attrs, :student)
Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, attrs)
|> Multi.run(:teacher, fn repo, %{user: user} ->
if teacher_attrs do
%Teacher{}
|> Teacher.changeset(Map.put(teacher_attrs, :user_id, user.id))
|> repo.insert()
else
{:ok, nil}
end
end)
|> Multi.run(:student, fn repo, %{user: user} ->
if student_attrs do
%Student{}
|> Student.changeset(Map.put(student_attrs, :user_id, user.id))
|> repo.insert()
else
{:ok, nil}
end
end)
end
and this will return (in the happy path), this:
{:ok, %{user: user, student: student, teacher: teacher}}
and either student or teacher will be nil.
OK I'm done.
I typed all this right in the browser without testing anything so beware typos, unmatched brackets and other small problems! It's just meant to be illustrative.