erlangelixirets

ETS table - Updating maps as records


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 :)


Solution

  • You should clearly distinguish maps and tuples. Map is a key-value structure. Tuple is not.

    :ets records are tuples, not maps.

    Your initial structure is a tuple of maps. One single tuple, having many maps. It gets inserted as a single element into :ets, which is clearly visible in your output (check curly braces.)

    I would guess you were to insert many elements into the cache.

    iex|1 ▶ :ets.new(:fast_cars_cache, [:duplicate_bag, :public, :named_table])
    iex|2 ▶ fast_cars = [
    ...|2 ▶   {"Red", "Mclaren", 15641.469},
    ...|2 ▶   {"Blue", "Ferrari", 120012.481},
    ...|2 ▶   {"Red", "Ferrari", 29831.021}
    ...|2 ▶ ]
    iex|3 ▶ :ets.insert(:fast_cars_cache, fast_cars)
    

    :ets.first/1, as it’s stated in the documentation, returns the first key. :ets.detele/2 deletes a record by key and your code wraps whatever was returned from :ets.first/1 into a one-element tuple, making :ets.delete/2 a no-op no matter what (and yes, :ets.delete/2 always returns true.)

    iex|4 ▶ :ets.first(:fast_cars_cache)
    #⇒ "Red"
    iex|5 ▶ :ets.delete(:fast_cars_cache, {"Red"}) # NOOP
    #⇒ true
    iex|6 ▶ :ets.delete(:fast_cars_cache, "Red") # DELETED
    #⇒ true
    

    Then if you want to insert a new record, create a tuple and insert it, don’t create a map wrapped into a single-element tuple. To get the old record by key, one usually uses :ets.lookup/2, or more sophisticated :ets.select.

    iex|7 ▶ [{_, make, _}|_] = :ets.lookup(:fast_cars_cache, "Red")
    iex|8 ▶ new_record = {"Black", make, 1641.469}
    iex|9 ▶ :ets.insert(:fast_cars_cache, new_record)
    

    I have linked enough documentation to leave using :ets.select_replace/2 as homework.