erlangelixirmnesia

Elixir : Mnesia : What is the most concise way to update a set of value in an element?


Problem

Learning to use Mnesia with Elixir, I created a table with multiple function (such as read, write...). One of them is to update a set of field (size from 1 to count - 1) without changing the rest of the data, and limiting the logic to the minimum inside the mnesia transaction.

During my search, I happen to find this : Erlang : Mnesia : Updating a single field value in a row though (code below). It is the same question but for Erlang, not for Elixir.

Code

From what I understand, it works in Erlang as read returns a tuple which is directly set inside a record, which allows us to save specific data in the write action as they are named.

update_a(Tab, Key, Value) ->
  fun() ->
    [P] = mnesia:wread({Tab, Key}),
    mnesia:write(P#pixel{a=Value})
  end.

Where, for Elixir, Even though Records exist, it will just be a tuple where you can only change data in the index, and return the full tuple to the write action.

Table: {table_name, id, data1, data2, data3, data4}
changes = [{2, new_val_for_data1}, {4, new_val_for_data3}]

def handle_call({:update_and_read, table, {id, changes}}, _, state) do
  {:atomic, new_object} =
    :mnesia.transaction(fn ->
      object =
        :mnesia.wread({table, id})
        |> List.first()

      ret =
        Enum.reduce(changes, object, fn {index, value}, acc ->
          acc |> Tuple.delete_at(index) |> Tuple.insert_at(index, value)
        end)

      :mnesia.write(object)
      ret
    end)

  {:reply, {:ok, new_object}, state}
end

Question

Is it possible to have shorter function in Elixir (ideally, 2 lines like the one in Erlang) ?


Solution

  • Well, you can use Record with some preliminary steps (like defining the record inside your app.)

    defmodule Pixel do
      require Record
      Record.defrecord(:table_name, id: nil, data1: nil, data2: nil)
    end
    

    and in the module where you want to update the table

    import Pixel
    
    def handle_call({:update_and_read, table, {id, changes}}, _, state) do
      # changes = [data2: new_val_for_data1, ...]
      {:atomic, result} =
        :mnesia.transaction(fn ->
          case :mnesia.wread({table, id}) do
            [object] -> 
              :mnesia.write(pixel(object, changes))
              {:ok, object}
            [] -> :error
          end
        end)
    
      {:reply, result, state}
    end
    

    Another possibility would be to go through

    changes = %{2 => new_val_for_data1, 4 => new_val_for_data3}
    object
    |> Tuple.to_list()
    |> Enum.with_index()
    |> Enum.map(fn {value, idx} -> Map.get(changes, idx, value) end)
    |> List.to_tuple()
    

    Another possibility would be to declare a macro that accepts a tuple, representing a table row, and a list of {idx, new_value} tuples, and changes the respective elements in-place.