【本コラムは、5分で読めて、15分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます
前回は、データ列の抽出を行った後、Webフレームワーク「Phoenix」をインストールし、Web上でのデータ表示切り替えを行いました
今回は、Web上にDBデータを表示します
■「ExcelからElixir入門」シリーズの目次
①データ並替え/絞り込み
|> ②データ列抽出、Web表示
|> ③WebにDBデータ表示
|> ④Webに外部APIデータ表示
|> ⑤Webにグラフ表示
|> ⑥SPAからPhoenix製APIを呼び出す(表示編)【LiveView版】
|> ⑦SPAからPhoenix製APIを呼び出す(更新編)【LiveView版】
|> ⑧Gigalixirに本番リリース
|> ⑨ElixirサーバサイドのみでReactと同じSPA/リアルタイムUIが作れる「LiveView」
|> ⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース
|> ⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開
|> ⑫Elixir/PhoenixのCRUD Webアプリをリリース
「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を使います
なお、MySQLやPostgreSQLで構築したい場合は、こちらに手順があります ので、ご覧ください(ただし、ローカルDBインストールに慣れていない方はハマる可能性あります)
Mnesiaを使う前準備
前回作ったPhoenix PJの mix.exs
の extra_applications
に、:mnesia
を追加します
defmodule Basic.MixProject do
use Mix.Project
…
def application do
[
mod: {Basic.Application, []},
extra_applications: [:logger, :runtime_tools, :mnesia]
…
Mnesia用スキーマを作成
MnesiaのDBを配置するスキーマ(実態はフォルダ)を作成します
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内で入力します
iex> :mnesia.create_table(:members, [attributes: [:id, :name, :age, :team, :position], disc_copies: [node()]])
もし、作成に失敗したときは、以下で一度テーブルを削除して、作り直してください
iex> :mnesia.delete_table(:members)
データ投入
続けて、データ投入するため、以下のコードを入力してください
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
上記コードをコンパイルして、実行します
iex> recompile
iex> Basic.inserts
投入したデータを確認します(どちらもqキーで終了します)
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キーで抜け、以下で確認します
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、技術顧問、重力プログラマ"}
]}
日本語も無事、投入されていることが確認できました
なお、以下でも確認できます
iex> :mnesia.dirty_read(:members, 1)
DBアクセスモジュールを作る
PJフォルダ内のlibフォルダ配下にutilフォルダを掘り、DBアクセスする以下2モジュールを作ります
まず DbMnesia
モジュールで、select
は、:mnesia.wait_for_tables
でMnesiaテーブルが利用可になるまで待ち、Mnesiaでデータ取得するための各設定を行った後、取得して、Ectoの返却と同じ形に整形して返します
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
の戻り値を分解するために使います
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ページの元となるファイルを書き換えます
<%
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>
なお、datas
の作成部分以外は、前回のマップリストと全く同じコードです(つまり、Elixir内部のデータ構造が全く同じ、なので、それを処理する側も全く同じ書き方で良い、ということを意味しています)
<%
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) ※最新版のインストール手順はコチラ
- 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
- 実機*Elixir 1.14.2 (Erlang/OTP 25)
-
Windows11
- WSL2/Ubuntu 22.04+Docker Compose+Elixir 1.13.4 (Erlang/OTP 25)
- Phoenix 1.6.15
- WSL2/Ubuntu 22.04+Docker Compose+Elixir 1.13.4 (Erlang/OTP 25)
終わり
今回で、DBデータをWebで表示できるようになりました
関数型言語によるWeb+DB開発が、こんなカンタンに出来ちゃうんだ … と感じていただけたら幸いです
データ元がDBに変わっても、画面側のコードが変わらなかったことに着目すると、関数型言語の威力がじょじょに見えてきます