ExcelからElixir入門③：WebにDBデータ表示

【本コラムは、5分で読めて、15分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex)です、ご覧いただいてありがとございます :bow:

[前回](https://qiita.com/piacerex/items/b7787580fce5f148242f)は、データ列の抽出を行った後、Webフレームワーク「Phoenix」をインストールし、Web上でのデータ表示切り替えを行いました

今回は、Web上にDBデータを表示します

**■「ExcelからElixir入門」シリーズの目次**
[**①データ並替え／絞り込み**](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1)
|> [**②データ列抽出、Web表示**](https://qiita.com/piacere_ex/items/b7787580fce5f148242f)
|> [<b>③WebにDBデータ表示</b>](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6)
|> [<b>④Webに外部APIデータ表示</b>](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109)
|> [<b>⑤Webにグラフ表示</b>](https://qiita.com/piacere_ex/items/290b76b76d5ff8e019bf)
|> [**⑥SPAからPhoenix製APIを呼び出す（表示編）【LiveView版】**](https://qiita.com/piacerex/items/64e77b85bfb4c6832662)
|> [**⑦SPAからPhoenix製APIを呼び出す（更新編）【LiveView版】**](https://qiita.com/piacerex/items/78ee0d2ad8cca0927980)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨ElixirサーバサイドのみでReactと同じSPA／リアルタイムUIが作れる「LiveView」**](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
|> [**⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース**](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)
|> [**⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開**](https://qiita.com/piacerex/items/b61348c282395167fd8c)
|> [**⑫Elixir／PhoenixのCRUD Webアプリをリリース**](https://qiita.com/piacerex/items/511f24888c7350639d0b)

# 「Mnesia」…Elixirにバンドルされている分散DB

通常、DBと言えば、MySQLやPostgreSQL、Oracle等のRDBMSを思い浮かべますが、最近のWeb＋DB開発では、下記クラウドDBや、Heroku／GigalixirのようなDB付きPaaSを利用することが増えており、自前でRDBMSをインストールする機会は減っています

- GCP：Cloud SQL、BigQuery
- AWS：RDS、Aurora、Redshift
- Azure：SQL Database、Cosmos DB

そのため、Elixirに標準搭載の「Mnesia（エムネジア）」と呼ばれる分散DBを利用します

SQLiteも使えるのですが、Windows実機環境だとC++ビルド環境のセッティングが必要なので、ここはMnesiaを使います

<font color="red">なお、MySQLやPostgreSQLで構築したい場合は、[こちらに手順があります](https://qiita.com/piacerex/items/b6d67d4fd628565979e4) ので、ご覧ください（ただし、ローカルDBインストールに慣れていない方はハマる可能性あります）</font>

# Mnesiaを使う前準備

[前回作ったPhoenix PJ](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix-pj%E4%BD%9C%E6%88%90%E8%B5%B7%E5%8B%95)の `mix.exs` の `extra_applications` に、`:mnesia` を追加します

```elixir:mix.exs
defmodule Basic.MixProject do
  use Mix.Project
…
  def application do
    [
      mod: {Basic.Application, []},
      extra_applications: [:logger, :runtime_tools, :mnesia]
…
```

# Mnesia用スキーマを作成

MnesiaのDBを配置するスキーマ（実態はフォルダ）を作成します

```elixir
iex> :mnesia.create_schema([node()])
:ok
iex> :mnesia.start
:ok
```

カレンドフォルダ配下を見ると、「Mnesia.nonode@nohost」というフォルダが出来ており、Mnesiaのデータはここに保存されます

```
iex> ls
.formatter.exs              .git                        .gitignore
Mnesia.nonode@nohost        README.md                   _build
assets                      config                      deps
elixir_buildpack.config     lib                         mix.exs
mix.lock                    priv                        test
```

# 複数列データと同じ構造のテーブル作成

前回の複数列データと同じデータ構造を、テーブルにて作成するため、以下をiex内で入力します

```elixir
iex> :mnesia.create_table(:members, [attributes: [:id, :name, :age, :team, :position], disc_copies: [node()]])
```

もし、作成に失敗したときは、以下で一度テーブルを削除して、作り直してください

```elixir
iex> :mnesia.delete_table(:members)
```

# データ投入

続けて、データ投入するため、以下のコードを入力してください

```elixir:lib/basic.ex
defmodule Basic do
　…
  def inserts() do
    :mnesia.start
    :mnesia.dirty_write({:members, 1, "enぺだーし", 54, "有限会社デライトシステムズ", "代表取締役、性能探求者"})
    :mnesia.dirty_write({:members, 2, "ざっきー", 50, "公立大学法人 北九州市立大学", "准教授、カーネルハッカー"})
    :mnesia.dirty_write({:members, 3, "たくと", 40, "カラビナテクノロジー株式会社", "リードエンジニア"})
    :mnesia.dirty_write({:members, 4, "piacere", 48, "株式会社DigiDockConsulting", "常務取締役CTO、技術顧問、重力プログラマ"})
  end
end
```

上記コードをコンパイルして、実行します

```elixir
iex> recompile
iex> Basic.inserts
```

投入したデータを確認します（どちらもqキーで終了します）

```elixir
iex> :ets.i(:members)
<1   > {members,2,
         <<227,129,150,227,129,163,227,129,141,227,131,  ...
<2   > {members,4,<<"piacere">>,44,
         <<227,130,171,227,131,169,227  ...
<3   > {members,3,
         <<227,129,164,227,129,161,227,130,141,227,131,  ...
<4   > {members,1,
         <<101,110,227,129,186,227,129,160,227,131,188,  ...
EOT  (q)uit (p)Digits (k)ill /Regexp -->
```

データが投入した件数だけ入っていることは確認できましたが、日本語がバイナリリストで確認できないので、qキーで抜け、以下で確認します

```elixir
iex> :mnesia.transaction(fn -> :mnesia.read(:members, 1) end)
{:atomic,
 [
   {:members, 1, "enぺだーし", 54,
    "有限会社デライトシステムズ",
    "代表取締役、性能探求者"}
 ]}
iex> :mnesia.transaction(fn -> :mnesia.read(:members, 4) end)
{:atomic,
 [
   {:members, 4, "piacere", 48, "株式会社DigiDockConsulting",
    "常務取締役CTO、技術顧問、重力プログラマ"}
 ]}
```

日本語も無事、投入されていることが確認できました

なお、以下でも確認できます

```elixir
iex> :mnesia.dirty_read(:members, 1)
```

# DBアクセスモジュールを作る

PJフォルダ内のlibフォルダ配下にutilフォルダを掘り、DBアクセスする以下2モジュールを作ります

まず `DbMnesia` モジュールで、`select` は、`:mnesia.wait_for_tables` でMnesiaテーブルが利用可になるまで待ち、Mnesiaでデータ取得するための各設定を行った後、取得して、Ectoの返却と同じ形に整形して返します

```diff_elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
  def select(table_name) do
    :mnesia.start
    table_atom = String.to_atom(table_name)
    :mnesia.wait_for_tables([table_atom], 1000)
    columns = :mnesia.table_info(table_atom, :attributes) |> Enum.map(& Atom.to_string(&1))
    specs = [table_atom] ++ Enum.map(1..Enum.count(columns), & :"$#{&1}") |> List.to_tuple
    rows = :mnesia.transaction(fn ->
      :mnesia.select(table_atom, [{specs, [], [:"$$"]}]) end) |> elem(1)
    %{
      columns: columns,
      command: :select,
      connection_id: 0,
      num_rows: Enum.count(rows),
      rows: rows
   }
  end
end
```

次に `Db` モジュールで、`query` は、SQL構文を受け付け、今回は「select」のみカバーします

`select` は、select文をパースし、`DbMnesia.select` を呼びます（今回はテーブル名抽出のみ）

パースに使っている `Regex.named_captures` は、かなり強力で便利な関数なので、どこかの別回で解説したいと思いますが、今は正規表現で文字列中から狙った部分だけ抽出できる関数くらいで捉えておいてください

`columns_rows`、`rows`、`columns` は、`DbMnesia.select` の戻り値を分解するために使います

```diff_elixir:lib/util/db.ex
defmodule Db do
  def query(sql) when sql != "" do
    cond do
      String.match?(sql, ~r/select.*/) -> select(sql)
      String.match?(sql, ~r/insert.*/) -> {:error, "insert unsupported"}
      String.match?(sql, ~r/update.*/) -> {:error, "update unsupported"}
      String.match?(sql, ~r/delete.*/) -> {:error, "delete unsupported"}
    end
  end
  def select(sql) do
    Regex.named_captures(~r/select( *)(?<columns>.*)( *)from( *)(?<tables>.*)/, sql)["tables"]
    |> DbMnesia.select
  end
  def columns_rows(result) do
    result
    |> rows
    |> Enum.map(fn row -> Enum.into(List.zip([columns(result), row]), %{}) end)
  end
  def rows(%{rows: rows} = _result), do: rows
  def columns(%{columns: columns} = _result), do: columns
end
```

# DBデータをWeb表示

さて、準備が整いましたので、DBデータをWeb表示してみます

以下のように、Webページの元となるファイルを書き換えます

```elixir:lib/basic_web/templates/page/index.html.heex
<%
datas = Db.query("select * from members") |> Db.columns_rows
%>
<table>
<%= for n <- datas do %>
<tr>
  <td><%= n["name"] %></td>
  <td><%= n["age"] %></td>
  <td><%= n["team"] %></td>
  <td><%= n["position"] %></td>
</tr>
<% end %>
</table>
```

以下のようなWebページが表示されるようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9e2bafa1-e11f-37c2-7286-f560b4b3c113.png)

なお、`datas` の作成部分以外は、前回のマップリストと全く同じコードです（つまり、Elixir内部のデータ構造が全く同じ、なので、それを処理する側も全く同じ書き方で良い、ということを意味しています）

```elixir:前回マップリスト版のindex.html.heex
<%
datas = [
  %{ "name" => "enぺだーし", "age" => 54, "team" => "有限会社デライトシステムズ", "position" => "代表取締役、性能探求者" }, 
  %{ "name" => "ざっきー", "age" => 50, "team" => "公立大学法人 北九州市立大学", "position" => "准教授、カーネルハッカー" }, 
  %{ "name" => "たくと", "age" => 40, "team" => "カラビナテクノロジー株式会社", "position" => "リードエンジニア" }, 
  %{ "name" => "piacere", "age" => 48, "team" => "株式会社DigiDockConsulting", "position" => "常務取締役CTO、技術顧問、重力プログラマ" }
]
%>
<table>
<%= for n <- datas do %>
<tr>
    <td><%= n["name"] %></td>
    <td><%= n["age"] %></td>
    <td><%= n["team"] %></td>
    <td><%= n["position"] %></td>
</tr>
<% end %>
</table>
```

# 【参考】本コラムの検証環境

本コラムは、以下環境で検証しています（恐らくUbuntu実機やMacでも動きます）

- Windows 10
  - 実機＊Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15
  - WSL2／Ubuntu 20.04＋Elixir 1.14.2 (Erlang/OTP 25)　※[**最新版のインストール手順はコチラ**](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)
    - Phoenix 1.6.15
  - Docker／Debian 11.6＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15
  - WSL2／Ubuntu 18.04＋Elixir 1.13.0 (Erlang/OTP 24)
    - Phoenix 1.6.6
    - Phoenix 1.6.2
  - 実機＋Elixir 1.11.3 
    - Phoenix 1.5.x
  - 実機＋Elixir 1.8.1
    - Phoenix 1.4.2
  - 実機＋Elixir 1.6.x
    - Phoenix 1.3.x
  - Docker＋Elixir 1.6.x
    - Phoenix 1.3.x

- Windows11
  - WSL2／Ubuntu 22.04＋Docker Compose＋Elixir 1.13.4 (Erlang/OTP 25)
    - Phoenix 1.6.15

# 終わり

今回で、DBデータをWebで表示できるようになりました

関数型言語によるWeb+DB開発が、こんなカンタンに出来ちゃうんだ … と感じていただけたら幸いです

データ元がDBに変わっても、画面側のコードが変わらなかったことに着目すると、関数型言語の威力がじょじょに見えてきます

[次回は、**Webに外部APIデータ表示** を行い、やはり画面側は、一切手直しがいらないことを更に実感していただきます](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109)