Updating a map in an ETS table

Sample code:

defmodule Foo do

  def fast_cars_cache do
    fast_cars = 
    {
      %{color: "Red", make: "Mclaren", mileage: 15641.469},
      %{color: "Blue", make: "Ferrari", mileage: 120012.481},
      %{color: "Red", make: "Ferrari", mileage: 29831.021},
      %{color: "Black", make: "Ferrari", mileage: 24030.674},
      %{color: "Cobalt", make: "Ferrari", mileage: 412.811},
      %{color: "Blue", make: "Koenigsegg", mileage: 250.762},
      %{color: "Cobalt", make: "Koenigsegg", mileage: 1297.76}, 
      %{color: "Titanium", make: "Koenigsegg", mileage: 5360.336},
      %{color: "Blue", make: "Maserati", mileage: 255.78}
    }

    if Enum.member?(:ets.all(), :fast_cars_cache) do
      :ets.delete(:fast_cars_cache)
      :ets.new(:fast_cars_cache, [:duplicate_bag, :public, :named_table])
      :ets.insert(:fast_cars_cache, fast_cars)
    else
      :ets.new(:fast_cars_cache, [:duplicate_bag, :public, :named_table])
      :ets.insert(:fast_cars_cache, fast_cars)
    end
  end


  def update_record do
    fast_cars_cache()

    old_record = :ets.first(:fast_cars_cache)

    new_record = 
    %{color: "Black", make: old_record.make, mileage: 1641.469}

    :ets.delete(:fast_cars_cache, {old_record})
    |> IO.inspect(label: "Old record deleted")

    :ets.insert(:fast_cars_cache, {new_record})
    :ets.tab2list(:fast_cars_cache)
  end

end

Output:


iex(1)> Foo.update_record
Old record deleted: true
[
  {%{color: "Red", make: "Mclaren", mileage: 15641.469},
   %{color: "Blue", make: "Ferrari", mileage: 120012.481},
   %{color: "Red", make: "Ferrari", mileage: 29831.021},
   %{color: "Black", make: "Ferrari", mileage: 24030.674},
   %{color: "Cobalt", make: "Ferrari", mileage: 412.811},
   %{color: "Blue", make: "Koenigsegg", mileage: 250.762},
   %{color: "Cobalt", make: "Koenigsegg", mileage: 1297.76},
   %{color: "Titanium", make: "Koenigsegg", mileage: 5360.336},
   %{color: "Blue", make: "Maserati", mileage: 255.78}},
  {%{color: "Black", make: "Mclaren", mileage: 1641.469}}
]

Observations/Questions:

  1. According to the IO.inspect, old_record was deleted yet, as tab2list reveals, this record still exists. Why is that?
  2. If, in fact, old_record was never deleted, what adjustments does the code need to accomplish this?
  3. Ideally, I’d like to make use of :ets.select_replace, if applicable, to perform this update all in one step but I can’t make heads or tails of the stipulations for the match specification requirement. It’d be really helpful if someone could disambiguate it with an example or two based on the sample above.

As always, thanks so much for your helpful guidance and suggestions :slight_smile:

:ets.delete/2 will always return true.

What is the intended shape of the fast_cars_cache table in ETS? The code in Foo.fast_cars_cache inserts a single object that is fast_cars; did you mean for fast_cars to be a tuple { or a list [?

Even when it fails?

Ideally:

  {
   %{color: "Black", make: "Mclaren", mileage: 1641.469},
   %{color: "Blue", make: "Ferrari", mileage: 120012.481},
   %{color: "Red", make: "Ferrari", mileage: 29831.021},
   %{color: "Black", make: "Ferrari", mileage: 24030.674},
   %{color: "Cobalt", make: "Ferrari", mileage: 412.811},
   %{color: "Blue", make: "Koenigsegg", mileage: 250.762},
   %{color: "Cobalt", make: "Koenigsegg", mileage: 1297.76},
   %{color: "Titanium", make: "Koenigsegg", mileage: 5360.336},
   %{color: "Blue", make: "Maserati", mileage: 255.78}
  }

:ets is a database, so it’s best to work with it like with other databases - one item per row. In your case one car per row. Then you can update each row without affecting any of the other ones.

2 Likes

Also would keep copying down to a minimum (a copy of the data is returned each time something is read from ets, and a copy of the data is stores each time as well)

Edit: I meant @LostKobrakai ‘s approach will keep copying down to a minimum too

If you change the table type to :set, then you won’t need those deletes and a simple insert will overwrite your old record

3 Likes

Thanks for the suggestion :slight_smile:

Thank you for this clarification :slight_smile:

Interesting. It seems that running out of memory should be a concern. If so, how does one garbage collect these copies when they’re no longer needed?

The gc works automatically but there are some things you need to do. The memory management of all the data in a table is nothing you have to worry about. When you delete an element or modify one, which actually entails making a new copy, then all the old data will be collected, reclaimed and reused later. So this is nothing you have to think about.

However, a table will never disappear by itself. So even if you no longer have any references to it the table is there forever until you explicitly delete it. This is because an ets table is the closest we have to global data on the BEAM.

To clarity what @LostKobrakai and @bottlenecked mentioned is that you never directly reference the data in the table. A process can only access its own local data so when you are working with ets tables you are copying data between the process heap and ets table memory area. Everytime you read from a table you copy the element into the process heap and when you create/modify an element you copy the data from your process to the ets table. That’s the price you pay for using them. BUT you can keep very large amounts of data in a table and they are globally accessible if yo have a reference or if it’s a named table and you know the name. You can control who has access to a tble.

5 Likes

That was a great explanation, thank you! :slight_smile:

What about when I perform operations like Enum.map, List.first and Tuple.to_list? Should garbage collection be a concern? If so, what’s the best way to address this?

Unless there are references to native resources involved – like from libraries in C, Rust, Zig, D etc. that you included in your project and are using them – then no, you have absolutely nothing to worry about.

EDIT: There are some caveats with big strings but you aren’t likely to bump into them unless you parse huge JSON files and keep many parsed records in memory.

Is that because copies created with Enum.map, Map.get, List.first, Tuple.to_list and the like are automatically deleted by the system when they’re no longer needed?

Yes, as @rvirding said. You never actually replace a value in Erlang / Elixir – you are creating a full copy of the original with the modifications on top. And since the BEAM is a garbage-collected runtime, the old (now discarded) value gets garbage-collected for you.

There’s nothing more required of you at all. :slight_smile:

1 Like

Fantastic! Thanks for clearing that up :slight_smile: