Adding and removing elements from jsonb field using embedded schema

I’m working on a small project where I’m trying to learn how to use embedded schemas. So I have albums and tracks. Album can have many tracks and they are saved in jsonb field using embedded schema. I want to be able to add new tracks and also, delete them.

In create_track() function, first I get existing tracks, then there is a map with a new track and then they both get merged, put into changeset and album get updated.

def create_track(%Album{} = album, attrs \\ %{}) do
    existing_tracks = album.tracks
    
    new_track = 
      %TrackEmbed{
        # this is just to show how TrackEmbed looks, real data comes from attrs
        name: "track name"
      }
      
    tracks = [new_track | existing_tracks]
    changeset = Ecto.Changeset.change(album)

    changeset
    |> Ecto.Changeset.put_embed(:tracks, tracks)
    |> Repo.update()
  end

To delete a track, I get existing tracks, find a list index where track I want to delete is and then I remove it from the list and the album gets updated.


def delete_track(%Album{} = album, attrs \\ %{}) do
    existing_tracks = album.tracks
    
    # real data comes from attrs here too
    existing_track_index = Enum.find_index(existing_tracks, fn x -> x.name == "track name" end)
    
    tracks = List.delete_at(existing_tracks, existing_track_index)
    changeset = Ecto.Changeset.change(album)

    changeset
    |> Ecto.Changeset.put_embed(:tracks, tracks)
    |> Repo.update()
end

The code I have here works but here I’m getting an existing list and just adding or removing from it, so I was wondering if there is maybe a better way?

Nothing dramatically better exists, as far I am aware. You could use cast_embed but that doesn’t help you much because you would still need to load the old value of the list and then update it, meaning that put_embed is the better choice here since it assumes the exact same use case.

cast_embed would be a good choice if you were receiving an entirely new list with which overwrites the old one (and usually if that new list is coming from external request).

There are several tutorials about embedded schemas out there and no one wrote anything about this so I just wanted to check if I’m doing something wrong. Thanks!

Well, if you are storing things in PostgreSQL you could reach for its various JSON functions: PostgreSQL: Documentation: 13: 9.16. JSON Functions and Operators

This SO thread might help a bit in orienting you how are they used: sql - PostgreSQL: Remove attribute from JSON column - Stack Overflow

This one seems to give you info on how to delete an object inside a JSONB array by index: update - Delete an object from within an array using JSONB in PostgreSQL - Database Administrators Stack Exchange

(Not very useful if you don’t have the index though.)

But I’d strongly caution against poking in the guts of your DB with DB-specific functions unless you can gain a big performance win. Say if you notice that the requests to your app that are supposed to delete tracks have abnormally high latency (which in Elixir/Phoenix land basically means anything above 30ms :003:) then it’s worth the pursuit. Otherwise I wouldn’t.