ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)


（この記事は、「[fukuoka.ex(その2) Elixir Advent Calendar 2017](https://adventar.org/calendars/2871")」の4日目、および[「Data Platform Advent Calendar 2017」](https://qiita.com/advent-calendar/2017/dataplatform)の2日目です）

# はじめに

Elixirで実際にプロダクト開発した経験からサンプルコードを交えて解説する本連載
今回は前回に引き続き、EctoでSQLのビルドを行わずに任意のSQLを実行する実装です。
今回は、バッチなどので想定される一括更新処理を実装してみます。

今回は以下の実装を例に考えます

- Phoenixでプロジェクトモデルを実装する
- プロジェクトに紐づくタスクモデルを実装する
- プロジェクト １：多 タスク　の関係である
- プロジェクトがキャンセルされた場合、プロジェクトのステータスをキャンセルにすると共に、タスクのステータスを全てキャンセルにする処理を実装する

また、以下の環境で実装しました

- Elixir     v1.6.1
- Phoenix v1.3.2
- Ecto      v2.2.10
- PostgreSQL v10.2


昨日は、@piacere_ex さんの[「Excelから関数型言語マスター番外編：なぜ関数型言語は習得できないか？」](https://qiita.com/piacere_ex/items/8fad067885aed73bf789)でした

本連載の記事はこちら
|> [ElixirでSI開発入門 #1 Ectoで悲観的ロック](https://qiita.com/tuchiro/items/7b04b0661cfa4a7af179)
|> [ElixirでSI開発入門 #2 Ectoで楽観的ロック](https://qiita.com/tuchiro/items/8949cc43ba90f4e68bf3)
|> [ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続](https://qiita.com/tuchiro/items/a4808baacea9a07d2409)
|> [ElixirでSI開発入門 #4 本番パスワードを環境変数に持たせる]
(https://qiita.com/tuchiro/items/4ccba7e210c596c383af)
|> [ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する(参照編)](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)
|> ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)

*お礼：各種ランキングに43回のランクインを達成しました*

4/27～5/19までの23日間、毎日お届けした[「季節外れのfukuoka.ex Elixir Advent Calendar」](https://qiita.com/advent-calendar/2017/elixir-or-phoenix) は、[Qiitaトップページトレンドランキング](https://qiita.com/trend)に4回、[「はてなブックマーク」のホットエントリー「テクノロジー」カテゴリ](https://b.hatena.ne.jp/hotentry/it)に2回もランクインし、他ランキングも含めると、トータル43回ものランクインを果たしました

Qiita「いいね」数は合計349件もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、みなさまの暖かい応援に励まされていますので、引き続き、[「季節外れのfukuoka.ex(その2) Elixir Advent Calendar」](https://adventar.org/calendars/2871)でも応援お願いします

[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/b3880591-5315-f686-24ae-e02115c2f0c3.png)](https://qiita.com/advent-calendar/2017/elixir-or-phoenix)

# 開発手順　
## プロジェクト〜モデルの作成

プロジェクトは[前回の記事](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)で作成したecto_adapters_sampleを使用します。

モデルを追加作成します。

```
> mix phx.gen.html Projects Project projects title content status
> mix phx.gen.html Projects Task tasks project_id:references:projects title content status
```

ルーティングを追加します。

```elixir:lib/ecto_adapters_sample_web/router.ex
defmodule EctoAdaptersSampleWeb.Router do
  use EctoAdaptersSampleWeb, :router

  〜中略〜

  scope "/", EctoAdaptersSampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/items", ItemController
    resources "/item_prices", ItemPriceController
    resources "/projects", ProjectController # <-- ルーティングを追加
    resources "/tasks", TaskController  # <-- ルーティングを追加

  end

  〜中略〜

end
```

マイグレーションします。

```
> mix ecto.migrate
```

プロジェクトのステータスの定数定義をマップとして追加します。
ステータスは初期値を設定し、必須項目から除外します。

```elixir:lib/ecto_adapters_sample/projects/project.ex
defmodule EctoAdaptersSample.Projects.Project do
  use Ecto.Schema
  import Ecto.Changeset


  schema "projects" do
    field :content, :string
    field :status, :string, default: "new" # <-- 初期ステータスは新規
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(project, attrs) do
    project
    |> cast(attrs, [:title, :content, :status])
    |> validate_required([:title, :content]) # <-- ステータスを必須項目から削除
  end

  def status() do
    %{
      new: "new", #新規
      working: "working", #作業中
      completed: "completed", #完了
      canceled: "canceled", #キャンセル
    }
  end

end
```

同じく、タスクのステータスも追加します。

```elixir:lib/ecto_adapters_sample/projects/task.ex
defmodule EctoAdaptersSample.Projects.Task do
  use Ecto.Schema
  import Ecto.Changeset


  schema "tasks" do
    field :content, :string
    field :status, :string, default: "new" # <-- 初期ステータスは新規
    field :title, :string
    field :project_id, :id

    timestamps()
  end

  @doc false
  def changeset(task, attrs) do
    task
    |> cast(attrs, [:title, :content, :status, :project_id])
    |> validate_required([:title, :content]) # <-- ステータスを必須項目から削除
  end

  def status() do
    %{
      new: "new", #新規
      working: "working", #作業中
      completed: "completed", #完了
      canceled: "canceled", #キャンセル
    }
  end
end
```

iexを起動し、プロジェクトとタスクを登録します。

```elixir
iex(1)> alias EctoAdaptersSample.Projects
iex(2)> Projects.create_project(%{title: "Elixir Advent Calendar 2017", content: "Elixirに関する記事を続々投稿します"})
iex(3)> Projects.create_task(%{project_id: 1, title: "Phoenixプロジェクト作成", content: "mix phx.newする"})
iex(4)> Projects.create_task(%{project_id: 1, title: "モデルを作成", content: "mix phx.gen/htmlする"})
iex(5)> Projects.create_task(%{project_id: 1, title: "キャンセル実装", content: "キャンセル処理を実装する"})
iex(6)> Projects.create_task(%{project_id: 1, title: "記事におこす", content: "Qiitaの記事をかく"})
```
- projects
![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/df47b5c9-b9df-39f4-7279-803169a703e1.png)
- tasks
![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/c088fb27-bfde-c0db-157c-6849fd4385e9.png)

## キャンセル処理をEcto.MultiとEcto.Adapters.SQLでキャンセル処理を実装

```elixir
defmodule EctoAdaptersSample.Projects do
  @moduledoc """
  The Projects context.
  """

  import Ecto.Query, warn: false
  alias EctoAdaptersSample.Repo

  alias EctoAdaptersSample.Projects.Task
  alias EctoAdaptersSample.Utils.EctoUtil
  alias Ecto.Multi

〜中略〜

def cancel_project(project_id) do

    Multi.new
    |> Multi.run(:cancel_project_and_tasks, EctoAdaptersSample.Projects, :cancel_project_and_tasks, [project_id])
    |> Repo.transaction()

  end

  def cancel_project_and_tasks(%{}, project_id) do

    project = get_project!(project_id)
    {:ok, project} = update_project(project, %{status: Project.status.canceled})

    sql = "update tasks set status = 'canceled' where project_id = $1"
    result = EctoAdaptersSample.Utils.EctoUtil.query(EctoAdaptersSample.Repo, sql, [project_id])
    {:ok, result}
  end
```
※ Ecto.Multiに関しては「 [ElixirでSI開発入門 #1 Ectoで悲観的ロック](https://qiita.com/tuchiro/items/7b04b0661cfa4a7af179)」参照
※ Ecto.Adapters.SQLに関しては前回記事「[ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)」参照

## EctoAdaptersSample.Utils.EctoUtil.query の改修

[前回](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)実装したEctoAdaptersSample.Utils.EctoUtil.queryを使いますが、今回のような更新処理ではエラーとなります。

原因は戻りをmap化する処理で使用したcolumns,rowsが更新SQLの場合nilとなることです。
こちらに対応するようにEctoUtilを修正します。
columnsがnilの場合はmapに更新件数num_rowsだけputして返します。

```elixir:lib/ecto_adapters_sample/ecto_util.ex
defmodule EctoAdaptersSample.Utils.EctoUtil do
  @moduledoc """
  Ecto関連の操作機能
  """
  import Ecto.Query, warn: false

@doc """
  SQL直接実行
  ## information
  EctoのSQLビルダを使用せずにSQLを直接実行する
  Ecto.Adapters.SQL.query()のWrapper関数
  戻り値を[%{row1},%{row2}...]形式で返す。

  ## Examples
      iex> EctoUtil.query(MyApp.Repo, "select * from users", params)
      [${id: => 1, name: => "userA name" },${id: => 2, name: => "userB name" }]
  """
   @spec query(Repo, string, [list]) :: [list]
  def query(repo, sql, params) do
    Ecto.Adapters.SQL.query(repo, sql, params)
    |> result_to_map_list()
  end

  defp result_to_map_list({:ok, result}) do

    columns = result.columns
    case columns do
      nil ->
        [num_rows: result.num_rows]
      _ ->
        rows = result.rows
        list_maps = Enum.map(rows, fn row -> row_columns_to_map(row, columns) end)
    end

  end

  defp row_columns_to_map(row, columns) do
    map_result =
      Enum.map(Enum.with_index(row, 0), fn {k, i} -> [Enum.at(columns, i), k] end)
      |> Enum.map(fn [a, b] -> {String.to_atom(a), b} end)
      |> Map.new()
  end
end
```

## 実行してみる

iexで作成した関数を実行します。

```elixir
iex(7)> Projects.cancel_project(1)
```

全てのデータが同一トランザクション上で更新されているます。
※ tasksの更新に失敗した場合は、projectsのステータス更新もRallbackされ不整合が起こらない

- projecs
![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/5e1fb518-543d-f99d-a9ff-e2960c40d6b9.png)
- tasks
![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/ac2c00c1-d42c-2f52-4b45-e603d2f414b4.png)

# まとめ

- Ecto.Adapters.SQLはselecti以外の更新クエリでも有効
- 更新系SQLの場合、Ecto.Adapters.SQLの戻りには、更新件数のみが格納される。
- Ecto.Multi.runで呼び出す関数内でもEcto.Adapters.SQLは使用可能

今回の例ではSQLの構造が非常にシンプルな為ピンとこない方もいるかもしれませんが、
複数のテーブルをJOINして一括更新をかけたい場合など、Ecto.MultiとEcto.Adapters.SQLの組み合わせは非常に強力です。
Ectoのクエリビルドで詰まった場合や更新性能に問題がでた場合はぜひ思い出してください。

明日は、@twinbee さんの「[Elixirから簡単にRustを呼び出せるRustler#4 SHIFTJISを変換する](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)」です


<b>★★★　満員御礼！Elixir MeetUpを6月末に開催します　★★★</b>
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します

私もこの連載で取り上げてない、開発ネタを出す予定ですのでぜひ奮ってご応募ください。

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)