Elixir::EctoでMATERIALIZED VIEWを使う

トランザクションを大きくしすぎないためにマテビューを使いたかった。 対象のシステムではEctoを使っていた。

トランザクションを大きくしたくないだけなので、スキーマの定義などは共用したい。 いいやり方が思いつかなかったので適当に対応した。そのやり方のメモになる。

Ectoについて触れたことがなかったので導入するところから書いておく。

TL;DR

Ectofrom マクロの in にスキーマ定義を渡すのが一般的だが、タプル( {String.t(), Schema.t()} )を渡すとスキーマ定義はそのままにテーブル名だけを上書きできる。 Ecto.Adapters.SQL.query() を使えばSQLを直接渡せる。

要件

項目
OTP アプリケーション名 :sample
テーブル名(書き込み用) sample_buffer
マテビュー名(読み込み用) sample

長い説明

Ectoの流れ。なんというかフレームワークに乗ってる感じがする。(色々やってくれる開発環境ってあんまり好きじゃない)

  1. ライブラリの依存を設定
  2. リポジトリを作成
  3. アプリケーションに登録
  4. 設定ファイルを記述
  5. DB作成
  6. スキーマ定義
  7. 空マイグレーションコードを生成
  8. マイグレーションコードを記述
  9. マイグレーション実施
  10. 実装

アプリケーショントップにある mix.exs に以下を追加する。 バージョン指定はよしなに。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
defmodule Sample.MixProject do
# ...
  defp deps do
    [
      # ...
      {:ecto, "~> 2.1"},
      {:postgrex, ">= 0.0.0"}
    ]
  end
end

リポジトリの実装はこんな感じ。

1
2
3
defmodule Sample.Repo do
  use Ecto.Repo, otp_app: :sample
end

リポジトリをOTPアプリケーションの監視下に入れる。 application.ex ファイルで *Applicationモジュールが定義されているのでその &start/2 関数内で呼んでいる Supervisor.start_link() の引数を書き換える。

1
2
3
4
5
6
7
8
9
  def start(_type, _args) do
  # ...
    import Supervisor.Spec

    children = [
      supervisor(Sample.Repo, []),
    ]
  # ...
  end

設定ファイルはこんな感じにした。DBマイグレーションコードはデフォルトで priv/migrations に置かれるが変更したかったので priv フィールドで指定している。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use Mix.Config

config :sample, Sample.Repo
  adapter: Ecto.Adapters.Postgres,
  database: "sample",
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  priv: "priv/repo/sample"

config :sample, ecto_repos: [Sample.Repo]

またecto_repos にリポジトリを登録しないとマイグレーションが実施できないため指定した。

テーブル定義前にデータベースの作成などをする。

1
2
3
$ mix ecto.create  -r Sample.Repo# DBを作成
$ mix ecto.destroy -r Sample.Repo# DBを消す
$ mix ecto.create  -r Sample.Repo # 必要なので再作成

スキーマ定義を行う。 schema マクロでフィールドを定義する。 (ここでのモジュール名は不自然だしあまりよくない。) スキーマ名は書き込みに利用するテーブル名を指定する。ここでは _buffer が付いている。 field(:name, :type)マクロがテーブルのカラムに対応している。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
defmodule Sample.SampleTable do

  use Ecto.Schema

  import Ecto.Changeset

  @primary_key false
  schema "sample_buffer" do
    field(:sample_id, :integer, primary_key: true)
    field(:name, :string)
    field(:score, :integer)
  end

  def snapshot(), do: "sample"
  def snapshot_table(), do: {snapshot(), __MODULE__}

  def changeset(params \\ %{}) do
    %__MODULE__{}
    |> cast(params, [:sample_id, :name, :score])
  end
end

@primary_key属性でプライマリキーを設定できる。ここで false を指定すると field() のオプションを通してプライマリキーを指定できるので利用した。

changeset() は挿入や削除に用いる関数で hash, keyword を挿入用のデータに変換している。 import Ecto.Changeset によって cast() が有効になっている。他に validate() なども幽王になりデータチェックなどに使える。

snapshot(), snapshot_table() がマテビューを利用するために工夫した部分。 実際にクエリを生成する部分の時に説明する。

空のマイグレーションコードを生成する。

1
mix ecto.gen.migration -r Sample.Repo create_sample_tables

これにより priv/repo/sample/migrations/ 配下にマイグレーション用の exs が生成される。 マイグレーションコードを書き換えて、テーブル・マテビュー・マテビュー用インデックスを作らせる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
defmodule Sample.Repo.Migrations.CreateSampelTables do
  use Ecto.Migration

  def change do
    create table("sample_buffer") do
      add :sample_id, :bigint
      add :name, :text
      add :score, :bigint
    end

    execute """
    CREATE MATERIALIZED VIEW sample AS
    SELECT * FROM sample_buffer
    """

    execute """
    CREATE UNIQUE INDEX sample_uniq
        ON sample (sample_id)
    """
  end
end

マイグレーションを実行する。

1
mix ecto.migrate -r Sample.Repo

利用するコードを書いてみる。 lib/sample 配下に query.ex ファイルを置く事にする。 使い方は下のようにする。 Repo の関数を利用するのを外に出して良いと思っている。 Ecto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
defmodule Sample.Query do
  import Ecto.Query, warn: false

  alias Sample.SampleTable
  alias Sample.Repo

  def items do
    from(i in SampleTable.snapshot_table())
    |> Repo.all()
  end

  def buffer_items do
    from(i in SampleTable)
    |> Repo.all()
  end

  def refresh do
    Ecto.Adapters.SQL.query(Repo, "REFRESH MATERIALIZED VIEW sample")
  end
end
comments powered by Disqus