Updating Eco associations

Hello! I am new to the Elixir world and I’d appreciate some help to understand how associations work.
Scenario:

defmodule Pokemon.Trainer do
    schema "trainers" do
        # fields
        has_many :pokemons, on_replace: :nilify
        has_many :badges, on_replace: :nilify
        has_one :hometown, on_replace: :nilify
    end

  def changeset(trainer, attrs) do
    # regular cast and validations
    |> cast_assoc(:pokemons)
    |> cast_assoc(:badges)
    |> cast_assoc(:hometown)
  end
end

At some point I’ll get a JSON with all the nested information.
If the information of any child (ex: Pokemon level) changes, I want to perform an update. Otherwise, do nothing.

# in a service
def check_trainer_data([trainer | trainers]) do
# after fetching JSON, decoding, etc
    pkmtrainer = %{
      name: trainer["name"],
      locale: trainer["locale"],
      online_trainer_id: trainer["online_id"],
      pokemons: Pokemon.PokemonParseService(trainer), #returns the list of pokemons
      badges: Pokemon.BadgeParseService(trainer), #returns the list of badges
      hometown: Pokemon.HometownParseService(trainer), #returns hometown
    }

  case Repo.get_by(Trainer, online_trainer_id: trainer["online_id]) do
    nil -> # creates a trainer; cast_assoc worked
    a_trainer -> 
      a_trainer
      |> Repo.preload([:pokemon, : badges, :hometown])
     |> Trainer.changeset(pkmtrainer)
     # here is my question ; attempts inserted here
     |> Repo.update()
end

In order to create or update the associated data, it looks like I could use put_assoc.

  1. Attempt 1: Ecto.Changeset.put_assoc(:pokemons, Pokemon.PokemonParseService(trainer, a_trainer))
    This ParseService returns the list of pokemons for a specific JSON and associated the id of the persisted trainer. It works, but I noticed it will update the children structure (pokemons) every time, even if there are no changes. Is this expected?

  2. Attempt 2 - I went back to the Trainer class and created a custom changeset, receiving the associations parameters:

def changeset(trainer, pokemons, badges, hometown, attrs)
  # default cast and validation
  |> put_assoc(:pokemons, pokemons)
  |> put_assoc(:badges, badges)
  |> put_assoc(:hometown, hometown)
end

And I called it at the placeholder. It also worked, but with the same problem - updating all the children every time.

Is there a way to update the associated structures only when there are real changes? Is there a more appropriated way to do this?

Thank you very much for your help and patience!

Best,

Does your JSON/your created assoc structs include the ids of existing items? If not there‘s no way for ecto to know you want to update a record in the db instead of creating one. There‘s no magical matching on „this new thing kinda looks like this old thing, so let‘s try to update it". Ecto will only update if the IDs between existing data and the updates match.

2 Likes

Thanks for the info! The external JSON does not contain the id if the child record exists. I can manually make a query, insert it into the struct and then it seems to work like a charm.

Best,