vadimshvetsov

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 :slight_smile:

Thanks, have a nice day.

Most Liked

Kurisu

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

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 :smiley:

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement