fukuoka.exのpiacereです
ご覧いただいて、ありがとうございます
ElixirのDBラッパー「Ecto」は、かなり便利で、SQLチックなDSLでDB操作できたり、RailsのActiveRecordと似たようなテーブル操作とマイグレーションができます
そんなEctoですが、実際のPJでは、往々にして、別システムで作られたDBにアクセスしたり、移行する要件があり、どこまで相互運用できるのか気になるところですよね?
ということで、Ectoでマイグレート外の既存テーブルをいじってみたいと思います
内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします
本コラムの検証環境、事前構築のコマンド
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.8.1、 ※最新版のインストール手順はコチラ
- Ecto 3.1.1 ※Phoenix最新版(1.4.8)とphoenix_ecto最新版(4.0.0)のdependency
- Phoenix 1.4.8 ※Ecto利用だけなら不要
既存テーブルに後差しでマイグレートできるか?
例として、以下の既存テーブルに、migrate_idという新システム移行用の列追加が、Ectoでマイグレート可能か試します
# \d taxes;
Table "public.taxes"
Column | Type | Collation | Nullable | Default
----------------+-----------------------------+-----------+----------+-----------------------------------
id | bigint | | not null | nextval('taxes_id_seq'::regclass)
name | character varying(255) | | |
tax_category | character varying(255) | | |
tax_rate | numeric | | |
start_datetime | timestamp without time zone | | |
end_datetime | timestamp without time zone | | |
lock_version | bigint | | |
inserted_id | bigint | | |
inserted_at | timestamp without time zone | | not null |
updated_at | timestamp without time zone | | not null |
Indexes:
"taxes_pkey" PRIMARY KEY, btree (id)
"taxes_inserted_id_index" btree (inserted_id)
"taxes_name_index" btree (name)
別システムで作られたDBなので、当然、schema_migrationsテーブルは存在していません
Phoenixでmix phx.gen.html/mix phx.gen.jsonしてもいないので、コンテキストフォルダやスキーマモジュールも存在していません
さて、ここから普通にEctoでマイグレートするときと同様の方法で、add_columnというマイグレートファイルを生成します
mix ecto.gen.migration add_column
* creating priv/repo/migrations/20190613023153_add_column.exs
マイグレートファイルに、列追加のコードを追記します
defmodule SampleDb.Repo.Migrations.AddColumn do
use Ecto.Migration
def change do
alter table( :taxes ) do
add :migrate_id, :string
end
end
end
マイグレートします
mix ecto.migrate
[info] == Running 20190613023153 SampleDb.Repo.Migrations.AddColumn.change/0 forward
[info] alter table taxes
[info] == Migrated 20190613023153 in 0.0s
エラーも無く、普通に走るので、テーブルを確認します
# \d taxes;
Table "public.taxes"
Column | Type | Collation | Nullable | Default
----------------+-----------------------------+-----------+----------+-----------------------------------
id | bigint | | not null | nextval('taxes_id_seq'::regclass)
name | character varying(255) | | |
tax_category | character varying(255) | | |
tax_rate | numeric | | |
start_datetime | timestamp without time zone | | |
end_datetime | timestamp without time zone | | |
lock_version | bigint | | |
inserted_id | bigint | | |
inserted_at | timestamp without time zone | | not null |
updated_at | timestamp without time zone | | not null |
migrate_id | character varying(255) | | |
Indexes:
"taxes_pkey" PRIMARY KEY, btree (id)
"taxes_inserted_id_index" btree (inserted_id)
"taxes_name_index" btree (name)
migrate_idの追加が確認できました
このように、Ectoのマイグレータは、最初からEctoで作ったテーブルでは無い、既存テーブルに対しても、マイグレートを行うことができます
いつコンテキストフォルダやスキーマモジュールは生成されるのか?
ところで、上記でマイグレートできても、コンテキストフォルダやスキーマモジュールは生成されません
これらは、mix phx.gen.html/mix phx.gen.jsonもしくは>mix phx.gen.schemaで初めて作られます
CRUD HTMLを生成するmix phx.gen.htmはこちらのコラム、CRUD APIを生成するmix phx.gen.jsonはこちらのコラムで、それぞれ紹介しているので、ここでは、あまり馴染みの無いmix phx.gen.schemaで行ってみましょう
作成するテーブルは、mix phx.gen.jsonのコラムと同じものです
mix phx.gen.schema Api.Post posts title:string body:text
* creating lib/sample_db/api/post.ex
* creating priv/repo/migrations/20190613033823_create_post.exs
Remember to update your repository by running migrations:
$ mix ecto.migrate
コンテキストフォルダが生成されます
コンテキストフォルダ配下には、スキーマモジュールができています
スキーマモジュールの中身は、mix phx.gen.htmやmix phx.gen.jsonで作成されるものと同じ内容です
defmodule SampleDb.Api.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :body, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end
マイグレートすれば完了です
mix ecto.migrate
Generated sample_db app
[info] == Running 20190613034859 SampleDb.Repo.Migrations.CreatePosts.change/0 forward
[info] create table posts
[info] == Migrated 20190613034859 in 0.1s
DB操作モジュールを既存テーブルにバインドするには?
ここまでの操作では、以下のようなDB操作モジュールは生成されないため、既存テーブルに、mix phx.gen.htmやmix phx.gen.jsonで実現できるDBアクセスができない状態です
defmodule SampleDb.Api do
@moduledoc """
The Api context.
"""
import Ecto.Query, warn: false
alias SampleDb.Repo
alias SampleDb.Api.Post
@doc """
Returns the list of posts.
## Examples
iex> list_posts()
[%Post{}, ...]
"""
def list_posts do
Repo.all(Post)
end
@doc """
Gets a single post.
Raises `Ecto.NoResultsError` if the Post does not exist.
## Examples
iex> get_post!(123)
%Post{}
iex> get_post!(456)
** (Ecto.NoResultsError)
"""
def get_post!(id), do: Repo.get!(Post, id)
@doc """
Creates a post.
## Examples
iex> create_post(%{field: value})
{:ok, %Post{}}
iex> create_post(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
…
これを解決するための方法が、fukuoka.exアドバイザーズ tuchiro さんのコラム、「ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続」にあるので、実施します
mix phx.gen.html Api Tax taxes name:string tax_category:string tax_rate:float start_datetime:datetime lock_version:integer inserted_id:integer migrate_id:integer
You are generating into an existing context.
The LiveviewPjWithDb.Api context currently has 6 functions and 1 files in its directory.
* It's OK to have multiple resources in the same context as long as they are closely related
* If they are not closely related, another context probably works better
If you are not sure, prefer creating a new context over adding to the existing one.
Would you like to proceed? [Yn] Y
* creating lib/liveview_pj_with_db_web/controllers/tax_controller.ex
* creating lib/liveview_pj_with_db_web/templates/tax/edit.html.eex
* creating lib/liveview_pj_with_db_web/templates/tax/form.html.eex
* creating lib/liveview_pj_with_db_web/templates/tax/index.html.eex
* creating lib/liveview_pj_with_db_web/templates/tax/new.html.eex
* creating lib/liveview_pj_with_db_web/templates/tax/show.html.eex
* creating lib/liveview_pj_with_db_web/views/tax_view.ex
* creating test/liveview_pj_with_db_web/controllers/tax_controller_test.exs
* creating lib/liveview_pj_with_db/api/tax.ex
* creating priv/repo/migrations/20190613052745_create_taxes.exs
* injecting lib/liveview_pj_with_db/api.ex
* creating test/liveview_pj_with_db/api_test.exs
* injecting test/liveview_pj_with_db/api_test.exs
Add the resource to your browser scope in lib/liveview_pj_with_db_web/router.ex:
resources "/taxes", TaxController
Remember to update your repository by running migrations:
$ mix ecto.migrate
router.exにルーティングを追加した後、通常だと、ここでマイグレートを行いますが、既存テーブルが存在するため、マイグレートは不要です
また、追々マイグレートする際に、マイグレートファイルが邪魔になるので、手動で削除します(ファイル名は、実際に作られたファイル名を確認して削除してください)
rm priv/repo/migrations/20190613053003_create_taxes.exs
Phoenixを起動します
iex -S mix phx.server
ブラウザでhttp://localhost:4000/taxes/
を見ると、CRUD HTMLで既存テーブルが操作できるようになっています
これで、既存テーブルを、Elixir/Phoenix上で、操作できるようになりました
思ったより、スンナリと移行できるということが、実感いただけたら幸いです
既存テーブルが「id」のサロゲートキーで無いときは…
今回は、既存テーブルが、「id」のサロゲートキーだったので、特に引っかかりませんでしたが、既存システムのテーブルには、サロゲートキーが無い複合キーが主キーだったり、サロゲートキーだけどカラム名が「id」で無かったり、色々あると思います
そこの解決についても、tuchiro さんのコラム、「ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続」で、段階的に移行していく手順が掲載されていますので、ご参考ください
他にも既存テーブルとElixirを繋ぐ技たち
上記以外にも、既存テーブルとElixirを繋ぐ技が色々あり、以下コラムをご参考ください
■ 複合キーを作りたい
Ectoで2つの列に独自の制約を作成する
■ 中間テーブルでn:mのリレーションを設定したい
Ectoでhas_many through
■ テーブルに悲観的ロック、楽観的ロックをかけたい
ElixirでSI開発入門 #1 Ectoで悲観的ロック
ElixirでSI開発入門 #2 Ectoで楽観的ロック
■ RailsとElixir/Phoenixで同一テーブルを利用し、共存させたい
RailsとPhoenixFrameworkでDBを共用する
■ Railsからのモデル移行をしたい
ElixirでSI開発入門 #8 Railsからのモデルの移行1(FitGap分析)
ElixirでSI開発入門 #9 Railsからのモデルの移行2(DDLをパースする)
■ ActiveRecordで性能ダウンする現象を回避したい or DSLでは無く直接SQLを書きたい
ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する(参照編)
ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)
終わり
Ectoでマイグレート外の既存テーブルをいじってみました
他にも、既存テーブルとElixirを繋ぐ技もご紹介しました
これらのテクニックは、既存のシステムからのリプレースや移行などのSI案件で役立つので、ElixirでPJを構成する際の参考にしてください
p.s.「いいね」よろしくお願いします
ページ左上の や のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!