Elixir ETSインメモリデータベースのバックアップと復元

ElixirにはETSという便利なインメモリデータベースが備えついています。ETSのデータをバックアップする方法を調べたのでメモします。

## ETSとDETS

- ETS（Erlang Term Storage）
  - Erlang備え付けのメモリー内のデータ保管庫
- DETS（Disk-based ETS）
  - Erlang備え付けのファイルに保存するデータ保管庫

https://www.erlang.org/doc/man/ets.html

https://www.erlang.org/doc/man/dets.html

https://learnyousomeerlang.com/ets

https://www.ymotongpoo.com/works/lyse-ja/ja/28_ets.html

https://elixirschool.com/ja/lessons/storage/ets

https://qiita.com/rei-m/items/e48ab2cf425e0c94a548

https://qiita.com/Joe-noh/items/1e0b03dda38a911ce957

https://qiita.com/mnishiguchi/items/54846209c37d74157868

https://qiita.com/mnishiguchi/items/b478cc6a1607bf66187f

## 依存関係

ETSもDETSもErlangの備え付けなので、Erlang/Elixir以外何もいりません。

## テーブル名をきめる

インメモリーのETSとファイル保存のDETSの両方に対して同じ名前を使います。別々の名前にしても良いのですが、共通の名前にすることにより後にコードが簡素化できる利点があります。

```elixir
table_name = :my_data
```

https://www.erlang.org/doc/man/dets.html#type-tab_name

## DETSファイル保存先をきめる

ドキュメントには書かれてませんが、相対パスじゃないとダメなようです。絶対パスだとDETSテーブル生成時に変なエラーがでました。あと、パスがディレクトリ配下の場合はディレクトリが存在していないとエラーになります。

```elixir
dets_dir = "tmp"
File.mkdir(dets_dir)

dets_path = Path.join([dets_dir, "#{table_name}.dets"])
```

https://www.erlang.org/doc/man/file.html#type-name

## ETSテーブルをつくる

テーブル名と各種オプションを指定してETSテーブルを作ります。

* 名前をつけるか
  * named_table 
* データ構造
  * set
  * ordered_set
  * bag
  * duplicate_bag
* アクセス権限
  * public
  * protected
  * private
* 並行性
  * read_concurrency
  * write_concurrency 

`named_table`オプションにより名前付きテーブルにするとテーブルへの参照が楽になります。本記事のコードは名前付きテーブルを前提としています。

詳しくは原典をご参照ください。

https://www.erlang.org/doc/man/ets.html#new-2

```elixir
init_my_ets = fn table_name ->
  :ets.new(table_name, [
    :named_table,
    :bag,
    :public
  ])
end

init_my_ets.(table_name)
```

## DETSテーブルをつくる

ファイルは相対パスで、ディレクトリを含むパスの場合はディレクトリが存在している必要があります。

同期させるためには、DETSテーブルのデータ構造(ここでは`:bag`)をETSと合わせる必要があります。

初期設定では3分に一度ファイル保存されるようです。これは`:auto_save`オプションにより変更可能です。

詳しくは原典をご参照ください。

https://www.erlang.org/doc/man/dets.html#open_file-2

```elixir
:dets.open_file(table_name,
  type: :bag,
  file: dets_path |> String.to_charlist(),
)
```

## DETSファイルに保存されたデータをETSに注入する

現時点ではDETSファイルは空だと思いますが、DETSにより保存されたデータが入っている場合は、[:dets.to_ets/2]を用いてETSに注入するすることができます。

ETSテーブルの中身は[:ets.tab2list/1]で確認できます。

[:dets.to_ets/2]: https://www.erlang.org/doc/man/dets.html#to_ets-2
[:ets.tab2list/1]: https://www.erlang.org/doc/man/ets.html#tab2list-1

```elixir
:dets.to_ets(table_name, table_name)
:ets.tab2list(table_name)
```

## ETSのデータを消してみる

後にデータを消したりしたくなると思います。ETSのデータを消す方法はいくつかあります。簡単な方法を挙げます。

### キーで検索して消去

[:ets.delete/2]

```elixir
:ets.delete(table_name, :toukon)
:ets.tab2list(table_name)
```

[:ets.delete/2]: https://www.erlang.org/doc/man/ets.html#delete-2

### 全データ消去

[:ets.delete_all_objects/1]

```elixir
:ets.delete_all_objects(table_name)
[] = :ets.tab2list(table_name)
```

[:ets.delete_all_objects/1]: https://www.erlang.org/doc/man/ets.html#delete_all_objects-1

## ETSにデータを挿入してみる

余談ですが、今回はたまたまデータ構造を`:bag`にしているのでキーの重複が認められます。キーを名前空間として利用できます。

```elixir
entries = [
  {:toukon, %{x: 1}},
  {:toukon, %{x: 2}},
  {:autorace, %{x: 3}}
]

for entry <- entries do
  true = :ets.insert(table_name, entry)
end

:ets.tab2list(table_name)
```

## ETSテーブルをDETSファイルに保存する

- [:dets.from_ets/2]でETSテーブルの内容をDETSテーブルに保存します
- [:dets.sync/1]でDETSテーブルに対して行われたすべての更新が確実にファイルに書き込まれるようにします

[:dets.from_ets/2]: https://www.erlang.org/doc/man/dets.html#from_ets-2
[:dets.sync/1]: https://www.erlang.org/doc/man/dets.html#sync-1

```elixir
persist_data = fn table_name ->
  with :ok <- :dets.from_ets(table_name, table_name),
       :ok <- :dets.sync(table_name) do
    :ok
  else
    {:error, reason} ->
      Logger.error("Unable to sync DETS #{table_name}, #{inspect(reason)}")
  end
end

persist_data.(table_name)
```

## ETSテーブルを消去してみる

[:ets.delete/1]

[:ets.delete/1]: https://www.erlang.org/doc/man/ets.html#delete-1

```elixir
:ets.delete(table_name)

init_my_ets.(table_name)
[] = :ets.tab2list(table_name)
```

## ETSを復元してみる

[:dets.to_ets/2]

[:dets.to_ets/2]: https://www.erlang.org/doc/man/dets.html#to_ets-2

```elixir
:dets.to_ets(table_name, table_name)
:ets.tab2list(table_name)
```

:tada: :tada: :tada: 

このように使い方を整理すれば、意外と簡単にできてしまいます。

ベテランの方々は簡素なパターンの場合には第三者パッケージに依存せずに自分で直にETSを使う人が多い印象をもっています。

https://github.com/elixir-lsp/elixir-ls/pull/724

## 参考文献

https://elixirpatterns.dev

https://github.com/elixir-lsp/elixir-ls/blob/ddf6688ce7d8e67bbea050f82da72826ef3e2b99/apps/language_server/lib/language_server/tracer.ex

## Elixirコミュニティ

本記事は以下のモクモク会での成果です。みなさんから刺激と元氣をいただき、ありがとうございました。

https://youtu.be/c0LP23SM7BU

https://okazakirin-beam.connpass.com/

https://autoracex.connpass.com

もしご興味のある方はお気軽にご参加ください。

https://qiita.com/piacerex/items/09876caa1e17169ec5e1

https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian

https://qiita.com/torifukukaiou/items/57a40119c9eefd056cae

https://qiita.com/piacerex/items/e0b6e46b1325bb931122

https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf

https://qiita.com/torifukukaiou/items/4481f7884a20ab4b1bea

https://note.com/awesomey/n/n4d8c355bc8f7

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/dc1ddba7-ab4c-5e20-1331-143c842be143.jpeg)

生産者の皆様いつも美味しい食材をありがとうございます。おかげで健康に元氣にもくもく取り組むことができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/43522317-8a9d-e936-f69e-8a7fc3048964.jpeg)

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/3ba8f28d-0094-2178-56e4-13d383e3b3b1.jpeg)