Ectoでマイグレート外の既存テーブルにアクセスする(リプレースや移行などのSI案件で役立つ)


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

マイグレートファイルに、列追加のコードを追記します

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で作成されるものと同じ内容です

lib/sample_db/api/post.ex
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アクセスができない状態です

lib/sample_db/api.ex
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ネタを見たいというあなた、私達と一緒に盛り上げてください!