vadimshvetsov
Ecto where to place put_assoc/4 when I insert child and why we need to preload if we know id?
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.
Most Liked
Kurisu
There are examples of put_change usage in the official Phoenix docs. I think there is nothing wrong with that if you don’t want to make any change to the entity which you have its id.
So for example you can check this example from the Context docs:
def create_page(%Author{} = author, attrs \\ %{}) do
%Page{}
|> Page.changeset(attrs)
|> Ecto.Changeset.put_change(:author_id, author.id)
|> Repo.insert()
end
def ensure_author_exists(%Accounts.User{} = user) do
%Author{user_id: user.id}
|> Ecto.Changeset.change()
|> Ecto.Changeset.unique_constraint(:user_id)
|> Repo.insert()
|> handle_existing_author()
end
defp handle_existing_author({:ok, author}), do: author
defp handle_existing_author({:error, changeset}) do
Repo.get_by!(Author, user_id: changeset.data.user_id)
end
Hope it’s helpfull.
vadimshvetsov
Thanks, @Kurisu.
As I understood from docs and books - put_assoc/4 and cast_assoc/3 are useful when I want to update some fields in associations alongside updating main entity. In my case I won’t touch other entities except user, which I cast_assoc/3 (changeset function is proper place for it or better place it in context layer?) in changeset. For me this cods isn’t looks good anyway and I hope it will evolve with me ![]()
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









