sheharyarn / memento

Simple + Powerful interface to the Mnesia Distributed Database 💾
http://hexdocs.pm/memento/
MIT License
734 stars 23 forks source link

How to update specific keys in a row without overwriting the rest? #30

Closed 2FO closed 2 years ago

2FO commented 2 years ago

I’ve created a module of CRUD operations, mostly one-liners, but I’m stuck at creating a general purpose update function. Goals:

  1. Update the value of any key or keys in the row, independently of each other.
  2. Insert and or update the :updated_at key.
  3. Only update the keys specified by the update functions arguments

My approach so far has been something along these lines (pseudo code):

defmodule Todo do
  use Memento.Table,
    attributes: [:id, :name, :priority, :updated_at],
    type: :ordered_set,
    autoincrement: true

# .....
  def update(id, changes = %{}) do
    Memento.transaction do
      case get_by_id(id) do
        %Todo{} = todo ->
          todo
          |> adjust( changes)
          |> Map.put!(:updated_at, NaiveDateTime.utc_now()

        _ ->
          {:error, :not_found}
      end
    end

    defp adjust(%Todo{} = todo, changes) do
       todo
         |> Map.update!(changes))
         |> Todo.write()
    end

    def get_by_id(id) do
          Memento.transaction(fn -> Memento.Query.read(Todo, id) end)
    end

Elixir school demonstrates updates with Mnesia.write/1 but this overwrites the whole row.

The other solutions I found are either over my head or in Erlang:

sheharyarn commented 2 years ago

Hi @2FO, it would be a mistake to compare mnesia (and hence, memento) to SQL databases because the design approaches are very different.

In :mnesia, you'll first need to read the record, make changes to the fields and then write it back, all in the same transaction:

def update(id, %{} = changes) do
  Memento.transaction(fn ->
    case Memento.Query.read(Todo, id, lock: :write) do
      %Todo{} = todo ->
        todo
        |> struct(changes)
        |> Map.put(:updated_at, NaiveDateTime.utc_now())
        |> Memento.Query.write()
        |> then(&{:ok, &1})

      nil ->
        {:error, :not_found}
    end
  end)
end

I made a minor optimization here by using :write lock on the read operation. The struct/2 method is useful when applying changes to structs, however, you probably want to use custom validation separately. You can modify this method a bit to make it generic and work with different records and validation systems.

As an example, you can also look at how Que uses Memento underneath.

2FO commented 2 years ago

@sheharyarn Thank you very much for the solution, and tips. I'll definitely check out Que and have read of the Mnesia docs.

I x-posted this question on the elixir forum, so I'll add your solution to the thread.