LoginSignup
5
2

More than 1 year has passed since last update.

Ecto から取得したデータを CSV に変換するモジュールを作る(NimbleCSV 版)

Last updated at Posted at 2022-06-15

@koyo-miyamura です!
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております

前回下記の記事でCSVエクスポート機能を作成しました
ここから一歩踏み込んで、Ecto から取得したデータをCSV文字列に変換するモジュールを作成しつつ、使用するライブラリをよりメジャーな NimbleCSV に変えて実装してみようと思います

モジュールの作成

作りたいモジュールのインターフェース

作成するモジュール名を PaginateSample.Utils.CsvConverter として下記のようなインターフェースを想定します
Ecto から取得したレコードの配列とそこから抽出したいフィールドのキーワードリストを入力として受け取り、CSV文字列を出力します

lib/paginate_sample_web/controllers/export_user_controller.ex
defmodule PaginateSampleWeb.ExportUserController do
  use PaginateSampleWeb, :controller

  alias PaginateSample.Users
+ alias PaginateSample.Utils.CsvConverter

  def create(conn, _params) do
    send_download(conn, {:binary, csv_data()}, filename: "export.csv")
  end

  defp csv_data do
+   CsvConverter.to_csv(users(), fields())
-   users()
-   |> CSV.encode(headers: fields())
-   |> Enum.join()
  end

  defp users do
    Users.list_all_users()
  end

  defp fields do
    [:id, :name, :inserted_at, :updated_at]
  end
end

モジュールの実装

実装は下記のようになります。また doctest も書いてみます

lib/paginate_sample/utils/csv_converter.ex
defmodule PaginateSample.Utils.CsvConverter do
  @moduledoc """
  CSV Converter
  """

  @doc ~S"""
  Convert struct lists to CSV

  ## Examples

      iex> import DummyStruct
      iex> records = [%DummyStruct{id: 1, name: :hoge, age: 10}, %DummyStruct{id: 2, name: :fuga, age: 20}, %DummyStruct{id: 3, name: :hogefuga, age: 30}]
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:name, :age])
      "name,age\r\nhoge,10\r\nfuga,20\r\nhogefuga,30\r\n"
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:age])
      "age\r\n10\r\n20\r\n30\r\n"
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:not_found])
      "not_found\r\n\r\n\r\n\r\n"

  """
  def to_csv(records, fields) do
    records
    |> CSV.encode(headers: fields)
    |> Enum.join()
  end
end

モジュールのテスト

doctest をするためにテストファイルを配置します

test/paginate_sample/utils/csv_converter_test.exs
defmodule PaginateSample.Utils.CsvConverterTest do
  use ExUnit.Case
  doctest PaginateSample.Utils.CsvConverter
end

doctest 中で使用するテスト用ダミー struct を定義しておきます(doctest 中で struct 定義はうまく機能しなかった :innocent:

test/support/csv_converter/dummy_struct.ex
defmodule DummyStruct do
  defstruct id: nil, name: nil, age: nil
end

テスト実行すると無事通りますね :thumbsup:

$ mix test test/paginate_sample/utils/csv_converter_test.exs
.

Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 doctest, 0 failures

Randomized with seed 513464

使用するライブラリを NimbleCSV にする

今まで使用していた CSV ライブラリはやや更新が滞っています

なのでよりアクティブに開発されている NimbleCSV を使用する実装に変えてみます

NimbleCSV を用いた実装

lib/paginate_sample/utils/csv_converter.ex
defmodule PaginateSample.Utils.CsvConverter do
  @moduledoc """
  CSV Converter
  """

  @doc ~S"""
  Convert struct lists to CSV

  ## Examples

      iex> import DummyStruct
      iex> records = [%DummyStruct{id: 1, name: :hoge, age: 10}, %DummyStruct{id: 2, name: :fuga, age: 20}, %DummyStruct{id: 3, name: :hogefuga, age: 30}]
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:name, :age])
      "name,age\r\nhoge,10\r\nfuga,20\r\nhogefuga,30\r\n"
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:age])
      "age\r\n10\r\n20\r\n30\r\n"
      iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:not_found])
      "not_found\r\n\r\n\r\n\r\n"

  """
  def to_csv(records, fields) do
    ([fields] ++ csv_content(records, fields))
    |> NimbleCSV.RFC4180.dump_to_stream()
    |> Enum.join()
  end

  defp csv_content(records, fields) do
    records
    |> Enum.map(fn record ->
      fields
      |> Enum.map(&Map.get(record, &1))
    end)
  end
end

単に Map.take(fields) とすると、fields 順にならないので Enum.mapMap.get を組み合わせるのがポイントです
(もっとスマートな書き方あったら教えてください)

テストも通りますね :thumbsup:

$ mix test test/paginate_sample/utils/csv_converter_test.exs
.

Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 doctest, 0 failures

Randomized with seed 654005

まとめ

リポジトリはこちらです:pray:

5
2
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
5
2