LoginSignup
10
3

More than 5 years have passed since last update.

CSVとDBアクセス(Ecto、SQL)をMapListでサクっと

Last updated at Posted at 2017-06-10

(この記事は Elixir (その2)とPhoenix Advent Calendar 2016 17日目の記事です)

前回、再帰処理を書かずにJSON項目を抜くためのミニ汎用関数「MapList.select()」を作りましたが、今回はこれを使って、以下3つを試してみましょう :shaved_ice:

① CSVファイルの特定列をリストアップする
② Ecto経由で取得したDBデータの特定列をリストアップする
③ SQLで取得したDBデータの特定列をリストアップする

※そうそう、Enum.map()をスラスラ書ける人には、MapList.select()自体が不要です:relaxed:

事前準備:MapListモジュールを作る

Elixirプロジェクトを作成します

# mix new csv_sample
# cd sample
# mkdir lib/tiny

libフォルダ配下に、MapListモジュールを作ります(前回と全く同じ内容)

lib/tiny/map_list.ex
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モジュールがあったので使ってみる

mix.exs
defmodule CsvSample.Mixfile do
 …
    defp deps do
 …
        { :csv, "~> 2.0" },
 …

モジュール取得します(要ネット接続)

# mix deps.get

CSVファイルを、1行1マップとして読み込むためのヘルパー関数を追加します

lib/tiny/map_list.ex
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列をリストアップしてみる

lib/csv.ex
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"]

うむ、かなりシンプルに仕上がった :v:

② Ecto経由で取得したDBデータの特定列をリストアップする

次は、DBデータの特定列をリストアップ :fist:

DBアクセス用のEctoを単体でインストールすることも可能だけど、Phoenixをインストールすれば、「Ecto付き」+「deps.get自動」+「Modelクラス作成がmix一発」、と非常にカンタンなので、Phoenixプロジェクトにて構築することにしてみる

※DBを扱うので、事前にPostgreSQLをインストールしておいてください

※この後出てくるPhoenix構築や、JSON API利用について、詳しく知りたい場合は、下記スライドをどうぞ

Elixir入門「Phoenixで高速Webアプリ & REST APIをサクッと書いてみる」

image.png
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テーブル構築)を行うために、ルーティング追加します

web/router.ex
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/

image.png

image.png

以下のような2件のデータを追加しておく

image.png

さて、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列をリストアップしてみよう

lib/db_sample.ex
defmodule DbSample do
 …
    def ecto_select() do
        DbSample.Posts
        |> DbSample.Repo.all()
        |> MapList.select( :title )
    end
 …

実行すると、title全件がリストアップされます :muscle_tone1:

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時の列指定で良いのでは...」というツッコミは一旦無で :sweat_smile:

まず、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」をバインドしてマップ化するためのヘルパー関数を作ります

lib/tiny/map_list.ex
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つのリストのバインディング、ホント便利 :fish_cake:

これでマップのリストとして扱えるので、MapList.select()して、title列をリストアップしてみる

lib/db_sample.ex
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全件がリストアップされます :sunrise:

iex> recompile()
iex> DbSample.sql_select()
[debug] QUERY OK db=31.0ms
select * from posts []
["タイトル1", "タイトル2"]

こんな感じで、JSONだけで無く、CSVでもDBでも、MapList.select()で、お手軽にデータ抽出できました :tada:

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()とパターンマッチの組み合わせ、強力過ぎ :race_car:


p.s.

福岡でElixirをワイワイ盛り上げるコミュニティの発足記念MeetUp、fukuoka.ex#1、おかげさまで、かなり盛り上がりました :angel:

Exlixir/Phoenixのプロダクション採用に興味強い方が多く、また関数型言語に抵抗ある方も「Elixirは、なんかはじめられそう」とのことなので、本当にこれからが楽しみ! :gift:

image.png


10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3