12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Data PlatformAdvent Calendar 2017

Day 2

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

Last updated at Posted at 2018-05-23

(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の4日目、および「Data Platform Advent Calendar 2017」の2日目です)

はじめに

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

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

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

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

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

昨日は、@piacere_ex さんの「Excelから関数型言語マスター番外編:なぜ関数型言語は習得できないか?」でした

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

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

4/27~5/19までの23日間、毎日お届けした「季節外れのfukuoka.ex Elixir Advent Calendar」 は、Qiitaトップページトレンドランキングに4回、「はてなブックマーク」のホットエントリー「テクノロジー」カテゴリに2回もランクインし、他ランキングも含めると、トータル43回ものランクインを果たしました

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

image.png

開発手順 

プロジェクト〜モデルの作成

プロジェクトは前回の記事で作成した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

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

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

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

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

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

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を起動し、プロジェクトとタスクを登録します。

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
  • tasks
    image.png

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

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で悲観的ロック」参照
※ Ecto.Adapters.SQLに関しては前回記事「ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する」参照

EctoAdaptersSample.Utils.EctoUtil.query の改修

前回実装したEctoAdaptersSample.Utils.EctoUtil.queryを使いますが、今回のような更新処理ではエラーとなります。

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

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で作成した関数を実行します。

iex(7)> Projects.cancel_project(1)

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

  • projecs
    image.png
  • tasks
    image.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を変換する」です

★★★ 満員御礼!Elixir MeetUpを6月末に開催します ★★★
※応募多数により、増枠しました
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します

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

image.png

12
4
1

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
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?