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

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

We're in Beta

About us Mission Statement