トランザクションを大きくしすぎないためにマテビューを使いたかった。
対象のシステムではEctoを使っていた。
トランザクションを大きくしたくないだけなので、スキーマの定義などは共用したい。
いいやり方が思いつかなかったので適当に対応した。そのやり方のメモになる。
Ectoについて触れたことがなかったので導入するところから書いておく。
TL;DR
Ectoの from マクロの in にスキーマ定義を渡すのが一般的だが、タプル( {String.t(), Schema.t()} )を渡すとスキーマ定義はそのままにテーブル名だけを上書きできる。
Ecto.Adapters.SQL.query() を使えばSQLを直接渡せる。
要件
項目 |
値 |
OTP アプリケーション名 |
:sample |
テーブル名(書き込み用) |
sample_buffer |
マテビュー名(読み込み用) |
sample |
長い説明
Ectoの流れ。なんというかフレームワークに乗ってる感じがする。(色々やってくれる開発環境ってあんまり好きじゃない)
- ライブラリの依存を設定
- リポジトリを作成
- アプリケーションに登録
- 設定ファイルを記述
- DB作成
- スキーマ定義
- 空マイグレーションコードを生成
- マイグレーションコードを記述
- マイグレーション実施
- 実装
アプリケーショントップにある 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
|