I’m quite new to Elixir and Phonex but I felt in love with it so much. After books (Programming Elixir |> 1.6 and Programming Phoenix |> 1.4) and docs I read through so many posts and threads about using cast_assoc/3
and put_assoc/4
but still have issues with it usage and fully understand.
I saw this post on medium about using only id
's without assocs, but can’t see this approach confirmed as good in docs or threads.
I’m building GraphQL API with Phoenix + Absinthe (still didn’t touch) for my translation company’s CRM.
I have Manager, Performer and Client entities. All of them belongs to User, because Manager can be Performer too. Performer required to have user, country, native language and some other references.
Migrations:
create table(:users) do
add :first_name, :string, null: false
add :last_name, :string, null: false
add :middle_name, :string
add :phone, :string, null: false
add :email, :string, null: false
end
create unique_index(:users, [:phone])
create unique_index(:users, [:email])
create table(:performers) do
add :city, :string, null: false
add :bio, :text
add :birthday, :date, null: false
add :user_id, references(:users, on_delete: :delete_all), null: false
add :country_id, references(:countries), null: false
add :native_language_id, references(:languages), null: false
add :gender_id, references(:genders), null: false
add :status_id, references(:performer_statuses), null: false
end
create unique_index(:performers, [:user_id])
Schemas:
User
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@required_fields ~w(first_name last_name phone email)a
@optional_fields ~w(middle_name)a
schema "users" do
field :first_name, :string
field :last_name, :string
field :middle_name, :string
field :email, :string
field :phone, :string
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_format(:phone, ~r/^[[:digit:]]+$/)
|> validate_format(:email, ~r/@/)
|> unique_constraint(:phone)
|> unique_constraint(:email)
end
end
Performer:
defmodule MyApp.Production.Performer do
use Ecto.Schema
import Ecto.Changeset
alias MyApp.Accounts.User
alias MyApp.Production.{PerformerStatus, Gender}
alias MyApp.Reference.{Country, Language}
@required_fields ~w(city birthday)a
@optional_fields ~w(bio)a
schema "performers" do
field :bio, :string
field :birthday, :date
field :city, :string
belongs_to :user, User
belongs_to :gender, Gender
belongs_to :country, Country
belongs_to :native_language, Language
belongs_to :status, PerformerStatus
timestamps()
end
@doc false
def changeset(performer, attrs) do
performer
|> cast(attrs, @required_fields ++ @optional_fields ++ [:user_id])
|> shared_changeset(attrs)
end
@doc false
def changeset_with_existing_user(performer, attrs) do
performer
|> cast(attrs, @required_fields ++ @optional_fields)
|> shared_changeset(attrs)
|> unique_constraint(:user_id)
|> assoc_constraint(:user)
end
defp shared_changeset(performer, attrs) do
performer
|> validate_required(@required_fields)
|> put_assoc(:user, attrs.user)
|> put_assoc(:gender, attrs.gender)
|> put_assoc(:country, attrs.country)
|> put_assoc(:native_language, attrs.native_language)
|> put_assoc(:status, attrs.status)
end
end
I have 2 clauses in API for creating Performer - when User exists and when it’s not. Gender, Country, Language and PerformerStatus are
def create_performer(attrs \\ %{})
def create_performer(%{user: %{id: id}} = attrs) do
user = Accounts.get_user!(id)
%Performer{}
|> Performer.changeset_with_existing_user(%{attrs | user: user})
|> Repo.insert()
end
def create_performer(attrs) do
%Performer{}
|> Performer.changeset(attrs)
|> Repo.insert()
end
Now I’m struggling with right way of populating Performer’s changeset with all other existing references. My changeset has so much put_assoc/4
rows, It looks like I do something wrong and I will have more associations further (1-Many and Many-Many), because creating performer would be all-in-one page with form where user fill everything.
The second strange thing - is necessity of preloading everything with put_assoc/4
for update or create. I could get entity with all the foreign keys filled from API client and why I need to preload everything? It’s looks like performance issue for me, it costs so much time if entity has so many associations.
And AFAIK - put_assoc/4
preferred to use with existing data, cast_assoc/3
to cast changeset for creating new (but it could match id and be used for update existing entity).
What I’m doing wrong?
May be I need to cast all foreign keys to changeset and don’t use associations at all? (But it’s not recommended to validate_required/3
associations)
Or I need to use build_assoc/3
? But from what entity I need to build then if I have so many belongs_to
?
Or I need to move all put_assoc/4
inside Context API?
I’m stuck a little and would appreciate advice or suggestion for getting things done in proper way
Thanks, have a nice day.