Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

fukuoka.exのpiacereです
ご覧いただいて、ありがとうございます :bow:

ElixirのDBラッパー「Ecto」は、かなり便利で、SQLチックなDSLでDB操作できたり、RailsのActiveRecordと似たようなテーブル操作とマイグレーションができます

そんなEctoですが、実際のPJでは、往々にして、別システムで作られたDBにアクセスしたり、移行する要件があり、どこまで相互運用できるのか気になるところですよね?

ということで、Ectoでマイグレート外の既存テーブルをいじってみたいと思います

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

本コラムの検証環境、事前構築のコマンド

本コラムは、以下環境で検証しています(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してもいないので、コンテキストフォルダやスキーマモジュールも存在していません
image.png
さて、ここから普通に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

コンテキストフォルダが生成されます
image.png
コンテキストフォルダ配下には、スキーマモジュールができています
image.png
スキーマモジュールの中身は、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で既存テーブルが操作できるようになっています
image.png
image.png

これで、既存テーブルを、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.「いいね」よろしくお願いします

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

piacerex
福岡でプログラマしながらIT商社とIT企業を経営してます。Elixir/Kerasをよく使う。Elixirコミュ#fukuokaex、福岡理学部#FukuokaScienceを主催。プログラマ歴36年/XPer歴19年/デジタルマーケッター/経営者/CTO/技術顧問数社。 シボと重力子放射線射出装置は別腹(^^)
https://github.com/piacerex
fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ
https://fukuokaex.fun/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away