(この記事は、「Elixir or Phoenix Advent Calendar 2017」の12日目です)

昨日は、@piacere さんの「ExcelでElixirマスター3回目:WebにDBデータ表示でした

本連載の記事はこちら
|> ElixirでSI開発入門 #1 Ectoで悲観的ロック
|> ElixirでSI開発入門 #2 Ectoで楽観的ロック
|> ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続

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

image.png

はじめに

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

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

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

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

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

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

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

こちらも公式にサポートされており、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

ルーティングを追加

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の項目はない

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
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であることが暗黙的に実装されているのだ

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
) ;

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

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

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

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

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 が主キーのテーブルが作成されている。

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

URLも主キー文字列でアクセスできる
http://localhost:4000/items/X-01

ちなみに、文字列型であれば2バイト文字の主キーも登録可能
http://localhost:4000/items/%E4%BC%8A%E3%81%AE6%E5%8F%B7

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

まとめ

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

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

明日は、@twinbee さんの「Elixir並列処理「Flow」の2段ステージ構造を理解する」です。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.