ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続

（この記事は、「[Elixir or Phoenix Advent Calendar 2017](https://qiita.com/advent-calendar/2017/elixir-or-phoenix "Elixir or Phoenix Advent Calendar 2017")」の12日目です）

昨日は、@piacere さんの[「ExcelでElixirマスター3回目：WebにDBデータ表示](https://qiita.com/piacere/items/a7558adc6856e3577dc6)でした

# はじめに

Elixirで実際にプロダクト開発した経験からサンプルコードを交えて解説する本連載
前回までに引き続きDB関連のお話です。

Elixirで実装された軽量ORマッパーであるEctoはデフォルトでidというサロゲートキーを主キーとするスキーマ実装とマイグレーションファイルを生成します。

ORマッパーが一般的でなかった時代からDB設計をやっていたおじさんからすると、そもそも"id"という味もそっけもない主キーが気持ち悪くもあるのですが、モダンなフレームワークの潮流なのでそこは慣れるとして・・・

既存のシステムで作成されたデータベースに繋ぎたい場合ってありますよね？

- 創り込んじゃった基幹系を全改修するお金はないけど、追加開発する情報系はサーバーコストもかけたくないからElixirで・・・とか
- IOTからデータ流れるサーバーが既存言語で耐えられなくなってるのでElixirで・・・とか
- あとは自分が単にElixirやってみたいから！とか（これエンジニアにとっては大事なモチベーションですよね）

でも既存DBは主キーが item_code:stringとかだったりします。
今回はそんな id:integer じゃない主キーを設定する方法についてです。

本連載の記事はこちら
|> [ElixirでSI開発入門 #1 Ectoで悲観的ロック](https://qiita.com/tuchiro/items/7b04b0661cfa4a7af179)
|> [ElixirでSI開発入門 #2 Ectoで楽観的ロック](https://qiita.com/tuchiro/items/8949cc43ba90f4e68bf3)
|> ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続

:stars::stars::stars::stars::stars:<b>　お知らせ　</b>:stars::stars::stars::stars::stars:
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します
私も本連載で出してないSI開発ネタを出す予定ですので、
Elixirでプロダクト開発最前線に興味ある方はぜひぜひご応募ください！

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)

# カスタム・プライマリキー設定で主キーを変更

こちらも公式にサポートされており、phoenixframework.orgにわかりやすい記事があがっています。

http://phoenixframework.org/blog/custom-primary-key

# 実装の前提

今回は以下の実装を例に考えます

- item_code:stringを主キーにもつitemsテーブルに接続する
- 今回はサンプル実装のためにマイグレーションファイルも生成する

また、以下の環境で実装しました

- Elixir     v1.6.1
- Phoenix v1.3.2
- Ecto      v2.2.10
- PostgreSQL v10.2

# 開発手順　
## プロジェクト〜モデルの作成

PhoenixプロジェクトとDBを作成

```
> mix phx.new ecto_custome_primary_key_sample --no-brunch
> cd ecto_custome_primary_key_sample
> mix ecto.create
```

モデルを作成

```
> mix phx.gen.html Posts Comment comments name comment lock_version:integer
```

※既存DBに接続する場合は、マイグレーションは不要なので**--no-schema**オプションを指定すると不要なマイグレーションファイルが作成されない

```
> mix phx.gen.html Posts Comment comments name comment lock_version:integer --no-schema
```

ルーティングを追加

```Ruby:lib/ecto_custome_primary_key_sample_web/router.ex

defmodule EctoCustomePrimaryKeySampleWeb.Router do
  use EctoCustomePrimaryKeySampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", EctoCustomePrimaryKeySampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/items", ItemController # <-- 作成したモデルのルーティングを追加
  end

end
```

## デフォルトの実装

一旦マイグレーションを実行して、出来上がった実装を確認してみる

```
> mix ecto.migrate
```

スキーマにもマイグレーションファイルにもidの項目はない

```Ruby:lib/ecto_custome_primary_key_sample/items/item.ex

defmodule EctoCustomePrimaryKeySample.Items.Item do
  use Ecto.Schema
  import Ecto.Changeset

  schema "items" do
    field :category, :string
    field :item_code, :string
    field :name, :string

    timestamps()
  end

  〜中略〜
end
```

```Ruby:priv/repo/migrations/yyyyMMddhhmmss_create_items.exs
defmodule EctoCustomePrimaryKeySample.Repo.Migrations.CreateItems do
  use Ecto.Migration

  def change do
    create table(:items) do
      add :item_code, :string
      add :category, :string
      add :name, :string

      timestamps()
    end

  end
end
```
が、出来上がったテーブルには"id"が主キーとして追加されている。
主キーがid:integerであることが暗黙的に実装されているのだ

```sql
CREATE TABLE public.items (
	id bigserial NOT NULL,
	item_code varchar(255) NULL,
	category varchar(255) NULL,
	name varchar(255) NULL,
	inserted_at timestamp NOT NULL,
	updated_at timestamp NOT NULL,
	CONSTRAINT items_pkey PRIMARY KEY (id)
)
WITH (
	OIDS=FALSE
) ;
```

## カスタム・プライマリキーの設定

スキーマに主キー項目を指定する記述を追加する

```Ruby:lib/ecto_custome_primary_key_sample/items/item.ex
defmodule EctoCustomePrimaryKeySample.Items.Item do
  use Ecto.Schema
  import Ecto.Changeset


  @primary_key {:item_code, :string, []} # <-- 主キー項目と型を指定する
　@derive {Phoenix.Param, key: :item_code} # <-- Phoenixがパラメータキーとして何を使用して変換するかを指定する
  schema "items" do
    field :category, :string
    # field :item_code, :string   <-- 重複定義となるためfield指定は削除するかコメントアウト
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(item, attrs) do
    item
    |> cast(attrs, [:item_code, :category, :name])
    |> validate_required([:item_code, :category, :name])
  end
end
```

マイグレーションを行う場合はマイグレーションファイルにも主キー設定を追加する

```Ruby:priv/repo/migrations/yyyyMMddhhmmss_create_items.exs
defmodule EctoCustomePrimaryKeySample.Repo.Migrations.CreateItems do
  use Ecto.Migration

  def change do
    create table(:items, primary_key: false) do　# <-- ", primary_key: false"を追加
      add :item_code, :string, primary_key: true # <-- ", primary_key: true"を追加
      add :category, :string
      add :name, :string

      timestamps()
    end

  end
end
```

テーブルを再作成する。
一旦DBに接続してるセッションを全て切断し（DBクライアントツール、phoenix、iexなど）ecto.resetを実行するとDB再作成＆シードデータの登録(priv/repo/seeds.exs)が再実行される。

```
> mix ecto.reset
```

改めて確認すると、item_code:string が主キーのテーブルが作成されている。

```sql

CREATE TABLE public.items (
	item_code varchar(255) NOT NULL,
	category varchar(255) NULL,
	name varchar(255) NULL,
	inserted_at timestamp NOT NULL,
	updated_at timestamp NOT NULL,
	CONSTRAINT items_pkey PRIMARY KEY (item_code)
)
WITH (
	OIDS=FALSE
) ;
```

# 動かしてみる

Phoenixサーバーを起動する。

```
> mix ecto.migrate
> mix phx.server
```

以下のURLにアクセスし、適当にItemを登録してみる。

http://localhost:4000/items
<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/c1c749ef-644d-ea91-f016-a25fc4916d28.png" width=50% >

URLも主キー文字列でアクセスできる
http://localhost:4000/items/X-01
<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/1fce2d85-7447-6ac4-f64a-232d4766bb7e.png" width=50% >

ちなみに、文字列型であれば２バイト文字の主キーも登録可能
http://localhost:4000/items/%E4%BC%8A%E3%81%AE6%E5%8F%B7
<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/f8198cff-1f59-da44-3561-5e8dd8d5fb14.png" width=50% >

まあ、SIでは基本避けますが・・・

# まとめ

- idが主キーでない時にはCustom Primary Keys
- @primary_key、 @derive で主キーを指定
- --no-schemaオプションでマイグレーション対象外

いかがだったでしょうか。
これで既存システムのElixirリプレイスもバッチリですね！

明日は、@twinbee さんの「[Elixir並列処理「Flow」の2段ステージ構造を理解する](https://qiita.com/twinbee/items/43b767520da666ee960d)」です。