(この記事は Elixir (その2)とPhoenix Advent Calendar 2016 17日目の記事です)
前回、再帰処理を書かずにJSON項目を抜くためのミニ汎用関数「MapList.select()」を作りましたが、今回はこれを使って、以下3つを試してみましょう
① CSVファイルの特定列をリストアップする
② Ecto経由で取得したDBデータの特定列をリストアップする
③ SQLで取得したDBデータの特定列をリストアップする
※そうそう、Enum.map()をスラスラ書ける人には、MapList.select()自体が不要です
事前準備:MapListモジュールを作る
Elixirプロジェクトを作成します
# mix new csv_sample
# cd sample
# mkdir lib/tiny
libフォルダ配下に、MapListモジュールを作ります(前回と全く同じ内容)
defmodule MapList do
def select( list, key1, key2 \\ "", key3 \\ "", key4 \\ "" ), do: _select( list, [], key1, key2, key3, key4 )
defp _select( [], values, _key1, _key2, _key3, _key4 ), do: Enum.reverse( values )
defp _select( map_list, values, key1, key2, key3, key4 ) do
[ head | tail ] = map_list
value = cond do
key2 == "" ->
%{ ^key1 => value1 } = head
value1
key3 == "" ->
%{ ^key1 => value1, ^key2 => value2 } = head
{ value1, value2 }
key4 == "" ->
%{ ^key1 => value1, ^key2 => value2, ^key3 => value3 } = head
{ value1, value2, value3 }
true ->
%{ ^key1 => value1, ^key2 => value2, ^key3 => value3, ^key4 => value4 } = head
{ value1, value2, value3, value4 }
end
_select( tail, [ value | values ], key1, key2, key3, key4 )
end
def dig( map_list, key ) do
%{ ^key => %{ value } } = map_list
value
end
end
① CSVファイルの特定列をリストアップする
CSVはこんな感じ
sample.csv
id, title, body
1,タイトル1,ボディ1
2,タイトル2,ボディ2
3,タイトル3,ボディ3
便利なCSVモジュールがあったので使ってみる
defmodule CsvSample.Mixfile do
…
defp deps do
…
{ :csv, "~> 2.0" },
…
モジュール取得します(要ネット接続)
# mix deps.get
CSVファイルを、1行1マップとして読み込むためのヘルパー関数を追加します
defmodule MapList do
…
def csv_with_header( file_path ) do
file_path
|> File.stream!
|> CSV.decode( headers: true )
|> Enum.map( &( &1 |> elem( 1 ) ) )
end
…
これでマップのリストとして扱えるので、MapList.select()して、title列をリストアップしてみる
defmodule CsvSample do
def select() do
"sample.csv"
|> MapList.csv_with_header
|> MapList.select( "title" )
end
end
試してみると...
# iex -S mix
iex> CsvSample.select()
["タイトル1", "タイトル2", "タイトル3"]
うむ、かなりシンプルに仕上がった
② Ecto経由で取得したDBデータの特定列をリストアップする
次は、DBデータの特定列をリストアップ
DBアクセス用のEctoを単体でインストールすることも可能だけど、Phoenixをインストールすれば、「Ecto付き」+「deps.get自動」+「Modelクラス作成がmix一発」、と非常にカンタンなので、Phoenixプロジェクトにて構築することにしてみる
※DBを扱うので、事前にPostgreSQLをインストールしておいてください
※この後出てくるPhoenix構築や、JSON API利用について、詳しく知りたい場合は、下記スライドをどうぞ
Elixir入門「Phoenixで高速Webアプリ & REST APIをサクッと書いてみる」
https://www.slideshare.net/piacere_ex/elixir3phoenixweb-rest-api-75571536
(ここから本編続き)Phoenixプロジェクトを作り、DB構築します
# mix phoenix.new db_sample --no-brunch
# cd db_sample
# mix ecto.create
# mix phoenix.gen.html Posts posts title:string body:string
マイグレーション(≒MVC追加+DBテーブル構築)を行うために、ルーティング追加します
defmodule DbSample.Router do
…
scope "/", ElmPhpenix do
…
resources "/posts", PostController
end
…
マイグレーションを行った後、ビルドします
# mix ecto.migrate
# iex -S mix phoenix.server
ブラウザで、以下URLにアクセスし、「New post」リンクをクリックすると、データ追加できます
POST http://localhost:4000/posts/
以下のような2件のデータを追加しておく
さて、Ectoでテーブルのデータ全件を取得してみると、SQLのデバッグ表示の後に、1レコード毎に1マップ(構造体)のリストが取得できます
iex> DbSample.Posts |> DbSample.Repo.all()
[debug] QUERY OK source="posts" db=0.0ms decode=16.0ms
SELECT p0."id", p0."title", p0."body", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
[%A.Posts{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "ボディ1",
id: 1, inserted_at: ~N[2017-06-10 11:43:12.560000], title: "タイトル1",
updated_at: ~N[2017-06-10 11:43:12.572000]},
%A.Posts{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "ボディ2",
id: 2, inserted_at: ~N[2017-06-10 11:43:22.931000], title: "タイトル2",
updated_at: ~N[2017-06-10 11:43:22.931000]}]
それでは、Ectoで取得したデータ全件を、MapList.select()して、title列をリストアップしてみよう
defmodule DbSample do
…
def ecto_select() do
DbSample.Posts
|> DbSample.Repo.all()
|> MapList.select( :title )
end
…
実行すると、title全件がリストアップされます
iex> recompile()
iex> DbSample.ecto_select()
[debug] QUERY OK source="posts" db=32.0ms
SELECT p0."id", p0."title", p0."body", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
["タイトル1", "タイトル2"]
ちなみに、Ectoによるall()以外の操作は、Ecto.RepoのHexか、こちらのQiita記事やまた別のQiita記事が参考になります
③ SQLで取得したDBデータの特定列をリストアップする
最後に、SQLで取得したDBデータの特定列をリストアップしてみましょう
「・・・ん? それって、select時の列指定で良いのでは...」というツッコミは一旦無で
まず、SQLでデータ取得できることを確認する
iex> Ecto.Adapters.SQL.query( DbSample.Repo, "select * from posts", [] )
[debug] QUERY OK db=0.0ms
select * from posts []
{:ok,
%Postgrex.Result{columns: ["id", "title", "body", "inserted_at", "updated_at"],
command: :select, connection_id: 2796, num_rows: 2,
rows: [[1, "タイトル1", "ボディ1",
{{2017, 6, 10}, {11, 43, 12, 560000}},
{{2017, 6, 10}, {11, 43, 12, 572000}}],
[2, "タイトル2", "ボディ2", {{2017, 6, 10}, {11, 43, 22, 931000}},
{{2017, 6, 10}, {11, 43, 22, 931000}}]]}}
SQLのデバッグ表示の後に、:okと取得結果のタプルがあり、取得結果内は、「columns」に列名リスト、「rows」にデータリストのリスト、という構成なので、「columns」と「rows」をバインドしてマップ化するためのヘルパー関数を作ります
defmodule MapList do
…
def map_column( columns, rows ) do
rows
|> Enum.map( fn( row ) -> Enum.into( List.zip( [ columns, row ] ), %{} ) end )
end
…
Enum.zip()による、2つのリストのバインディング、ホント便利
これでマップのリストとして扱えるので、MapList.select()して、title列をリストアップしてみる
defmodule DbSample do
…
def sql_select() do
case Ecto.Adapters.SQL.query( DbSample.Repo, "select * from posts", [] ) do
{ :ok, result } -> MapList.map_column( result.columns, result.rows )
end
|> MapList.select( "title" )
end
…
実行すると、これまたtitle全件がリストアップされます
iex> recompile()
iex> DbSample.sql_select()
[debug] QUERY OK db=31.0ms
select * from posts []
["タイトル1", "タイトル2"]
こんな感じで、JSONだけで無く、CSVでもDBでも、MapList.select()で、お手軽にデータ抽出できました
Enum.map()をスラスラ書ける人が育ってきたら
以下のような感じで、MapList.select()も、再帰処理も、不要になります
iex> DbSample.Posts |> DbSample.Repo.all() |> Enum.map( fn( %{ title: title } ) -> title end )
[debug] QUERY OK source="posts" db=0.0ms
SELECT p0."id", p0."title", p0."body", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
["タイトル1", "タイトル2"]
iex> DbSample.Posts |> DbSample.Repo.all() |> Enum.map( fn( %{ title: title, body: body } ) -> { title, body } end )
[debug] QUERY OK source="posts" db=0.0ms
SELECT p0."id", p0."title", p0."body", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
["タイトル1,ボディ1", "タイトル2,ボディ2"]
Enum.map()とパターンマッチの組み合わせ、強力過ぎ
p.s.
福岡でElixirをワイワイ盛り上げるコミュニティの発足記念MeetUp、fukuoka.ex#1、おかげさまで、かなり盛り上がりました
Exlixir/Phoenixのプロダクション採用に興味強い方が多く、また関数型言語に抵抗ある方も「Elixirは、なんかはじめられそう」とのことなので、本当にこれからが楽しみ!