Updating structs with Ecto changesets and put_assoc

Hey there, this is my first post in here! I’m “just” in need of a little help with changesets and associations.

I have a changeset I built that works wonders to create and validate structs. However, now that I want to also add functionality to update structs this one’s not gonna do it since I’m using put_assoc and passing in the params to my helper function:

def changeset(%Course{} = struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :description])
    |> put_assoc(:scopes, parse_scopes(params), required: true)
    |> validate_required([:title, :description])
  end

  defp parse_scopes(params) do
    (params["scopes"] || [])
    |> Enum.map(fn name ->
      Repo.get_by(Scope, name: name)
    end)
  end

If I wanted to just update the title, I would just pass in the title through params and the existing course through struct, but that would mean the scopes would be reset to none since put_assoc destroys any association.

What is the best way to approach this problem?

Also, I’ve got another problem with this and it is that if the user passes in an invalid scope this will throw an error and I have no idea how to catch that in a clean way. I may open a new question for this later but I’d appreciate it if someone had some insight on this too.

Thanks a lot!

Edit: fixed a word

Hey,

I don’t know, if that is, what you are looking for, but maybe try to use build_assoc since this don’t overwrite the association.

I’m using many_to_many so I think build_assoc won’t work with this. Should I change it to has_many? I still have no idea how to approach this :frowning:

I ended up doing this but I really don’t like it, it feels ugly.

def changeset(%Course{} = struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :description])
    |> put_scopes
    # |> put_assoc(:scopes, parse_scopes(params), required: true)
    |> validate_required([:title, :description])
  end

  defp put_scopes(%Ecto.Changeset{params: params} = changeset) do
    if params["scopes"] == [] do
      changeset
    else
      put_assoc(changeset, :scopes, parse_scopes(params), required: true)
    end
  end

  defp parse_scopes(params) do
    (params["scopes"] || [])
    |> Enum.map(fn name ->
      Repo.get_by(Scope, name: name)
    end)
  end

Please, if someone knows a better solution let me know! I really want to have it be a bit cleaner

Why is there a params["scopes"] if you’re not updating them?

If the record exists already and you wish to update it why not query for the parent and use Repo.preload to preload the scopes onto it.

def update_changeset(course_id,params) do
  Repo.get(Course, course_id) |> Repo.preload([:scopes])
  |> cast(params, [:title, :description])
  |> cast_assoc(:scopes, required: true)
  |> ....
end

If you preload the children Ecto should be able to figure out that you are updating rarther than creating a new one, as long as the params have ids for the scopes in them (which they should)

I don’t think I can use cast_assoc because I’m just passing in a list of strings from which i get the proper records and put them with put_assoc. From what I know, cast_assoc only works with a certain format for the params key.

I think I forgot to mention that I don’t pass the scopes struct, I just pass a string list through params. That may help a bit. I’m also not using Phoenix.