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

[fukuoka.ex](https://fukuokaex.connpass.com/)のpiacereです
ご覧いただいて、ありがとうございます :bow:

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

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

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

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

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

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.8.1、 　　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b04ad6c9-b681-cd90-0fca-e0fff00b3a55.png)
さて、ここから普通にEctoでマイグレートするときと同様の方法で、add_columnというマイグレートファイルを生成します

```
mix ecto.gen.migration add_column

* creating priv/repo/migrations/20190613023153_add_column.exs
```

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

```elixir: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はこちらのコラム](https://qiita.com/piacere_ex/items/b4f57b55663403f9ec8e)、CRUD APIを生成する[mix phx.gen.jsonはこちらのコラム](https://qiita.com/piacere_ex/items/b63417540378ab9cbd18)で、それぞれ紹介しているので、ここでは、あまり馴染みの無いmix phx.gen.schemaで行ってみましょう

作成するテーブルは、[mix phx.gen.jsonのコラムと同じもの](https://qiita.com/piacere_ex/items/b63417540378ab9cbd18)です

```
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b67d4093-7abe-2647-03fc-0f9e2001f7e0.png)
コンテキストフォルダ配下には、スキーマモジュールができています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ddc823cc-b59d-1415-621d-3840741a1466.png)
スキーマモジュールの中身は、mix phx.gen.htmやmix phx.gen.jsonで作成されるものと同じ内容です

```elixir: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アクセスができない状態です

```elixir: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への接続」](https://qiita.com/tuchiro/items/a4808baacea9a07d2409)にあるので、実施します

```
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6b1a6ce9-d30d-1fd5-6d8e-c1117e4f25c6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9095c074-9fdb-d6fc-256a-fa4c603e5eb4.png)


これで、既存テーブルを、Elixir／Phoenix上で、操作できるようになりました

思ったより、スンナリと移行できるということが、実感いただけたら幸いです

# 既存テーブルが「id」のサロゲートキーで無いときは…

今回は、既存テーブルが、「id」のサロゲートキーだったので、特に引っかかりませんでしたが、既存システムのテーブルには、サロゲートキーが無い複合キーが主キーだったり、サロゲートキーだけどカラム名が「id」で無かったり、色々あると思います

そこの解決についても、tuchiro さんのコラム、[「ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続」](https://qiita.com/tuchiro/items/a4808baacea9a07d2409)で、段階的に移行していく手順が掲載されていますので、ご参考ください

# 他にも既存テーブルとElixirを繋ぐ技たち

上記以外にも、既存テーブルとElixirを繋ぐ技が色々あり、以下コラムをご参考ください

<b>■ 複合キーを作りたい</b>
[Ectoで2つの列に独自の制約を作成する](https://codeday.me/jp/qa/20190319/410577.html)

<b>■ 中間テーブルでn:mのリレーションを設定したい</b>
[Ectoでhas_many through](https://qiita.com/hashicco/items/5def933265b4bc1153be)

<b>■ テーブルに悲観的ロック、楽観的ロックをかけたい</b>
[ElixirでSI開発入門 #1 Ectoで悲観的ロック](https://qiita.com/tuchiro/items/7b04b0661cfa4a7af179)
[ElixirでSI開発入門 #2 Ectoで楽観的ロック](https://qiita.com/tuchiro/items/8949cc43ba90f4e68bf3)

<b>■ RailsとElixir／Phoenixで同一テーブルを利用し、共存させたい</b>
[RailsとPhoenixFrameworkでDBを共用する](http://developersnote.jp/elixir/share-db-between-rails-and-phoenix.html)

<b>■ Railsからのモデル移行をしたい</b>
[ElixirでSI開発入門 #8 Railsからのモデルの移行1(FitGap分析)](https://qiita.com/tuchiro/items/2824c5cb0c29efa7cad7)
[ElixirでSI開発入門 #9 Railsからのモデルの移行2(DDLをパースする)](https://qiita.com/tuchiro/items/07bc2ddb58812c62da15)

<b>■ ActiveRecordで性能ダウンする現象を回避したい or DSLでは無く直接SQLを書きたい</b>
[ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する(参照編)](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)
[ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)](https://qiita.com/tuchiro/items/04a0e68222beba91958f)

# 終わり

Ectoでマイグレート外の既存テーブルをいじってみました

他にも、既存テーブルとElixirを繋ぐ技もご紹介しました

これらのテクニックは、既存のシステムからのリプレースや移行などのSI案件で役立つので、ElixirでPJを構成する際の参考にしてください

# p.s.「いいね」よろしくお願いします

ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada: