Maxximiliann
Maxximiliann

Reputation: 35

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

Upvotes: 1

Views: 994

Answers (2)

Suren Kirakosyan
Suren Kirakosyan

Reputation: 41

As Aleksei said, you can also use the :ets.select_replace/2 function. Here's an example:

:ets.new :t0, [:set, :public, :named_table]
:ets.insert :t0, {{:foo, [:a, :b]}, {:ok, :c}, [ttl: :os.system_time(:seconds) - 1]}
:ets.insert :t0, {{:boo, [:d, :e]}, {:ok, :g}, [ttl: :os.system_time(:seconds) - 1]}

new_opt = {{:new_opt, :this_is_new_opt}}
ms = [{
  patter = {key = {:boo, :"$1"}, :"$2", [ttl: :"$3"]}, 
  where = [{:<, :"$3", :os.system_time(:seconds)}], 
  replace = [{{
    key = {{:boo, :"$1"}}, 
    new_value0 = {{:ok, :replaced}}, 
    new_value1 = [
      {{:ttl, :os.system_time(:seconds)}}, 
      new_opt
    ]
  }}]
}]

:ets.select_replace :t0, ms
:ets.tab2list :t0

Writing it wholly by hands seems little annoying, instead of it you can hack it just using :ets.fun2ms/1 and then copy past it and adding your variables in the generated matchSpec.

ms = :ets.fun2ms(fn {{:boo, a}, b, [ttl: c]} = x when c > 1700807468 ->
  {{:boo, a}, b, [ttl: c, new: :new]} 
end)

Note:

  • you cannot use the :ets.select_replace/2 in order to replace key, this mean you cannot replace {:boo, [:d, :e]};
  • you cannot use the :ets.fun2ms/1 function in an Elixir module, this function is available only in shell;
  • this answer maybe useful if you want to use :ets.fun2ms/1 in your codebase;

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

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.

Upvotes: 1

Related Questions