2017.03.31

Elixir & Phoenix framework の始め方


riak

次世代システム研究室の データストア 好きの Y.I. です。

今回はデータストアから離れて、プログラミング言語 Elixir と Phoenix Framework についてまとめたいと思います。すでに Elixir の記事 「Elixir & Phoenix は意外と広告向けサーバに向いてる?」、 「GraphQL Relay 対応サーバを Elixir で作る」、「VR環境に構築したプレゼンテーションルーム」 がありますが、始め方はあった方が良いだろうということで、今回は Elixir と Phoenix Framework の特徴や学び方、そして PostgreSQL を使った CRUD Web アプリケーションを作るところまでまとめたいと思います。

■ Elixir 特徴

  • Rubyの文法に似た関数型言語
    • Rails のコミッター José Valim 氏が 主要コミッター
  • 2012年に登場
  • 30年以上の歴史がある ErlangVM 上で動作
    • ネットワークが繋がるノード間で容易にプロセス間通信可能
    • スケールアウトしやすい
  • Elixir で Erlang 言語の代わりに ErlangVM アプリケーションを作成できる
  • Erlang eco system を利用可能
  • 軽量スレッド(Erlangでは軽量プロセスと呼ばれる)
    • デフォルト設定で1ノードに26万プロセスまで起動可能
    • WhatsApp では1ノードで200万を達成(over 2 million tcp connections)
  • Full GC (Stop the world) がない
    • 軽量プロセス毎の GC のため他プロセスに影響しない
  • Actor モデル
    • 軽量プロセス間の連携はメッセージ送受信による実施
    • 疎結合
    • プロセス間通信に利用できる
  • Supervisor による信頼性向上
    • 例外発生時は crush させて自動リトライなど事前に定めた戦略に沿ってリカバー可能
  • マクロを使って独自の文法がつくれる
  • ビルドツール mix がある
    • Clojure の Leiningen や Scala の sbt のようなビルドツール
  • REPL iex でインタラクティブに開発可能

■ Phoenix Framework 特徴

  • Web フレームワーク
  • Ruby on Rails に似たフルスタックフレームワーク
    • Chris McCord 氏や Rails のコミッター José Valim 氏が 主要コミッター
  • Ruby on Rails よりも処理速度が速い
  • Scaffold によりコード雛形生成
  • ActiveRecord のような Ecto
  • WebSocketサーバを簡単に実現可能
一部ですが、上記のような特徴があります。

■ 学び方

▼ Elixir

Elixirの学び方は、やはり公式サイトが一番です。公式サイトの GUIDES メニューを中心に文法や各種機能を学べます。公式サイトは英語ですが日本語訳を公開してくれているサイトもあります。

  Elixir 公式サイト:http://elixir-lang.org/
  Elixir 公式サイト GUIDES メニュー:http://elixir-lang.org/getting-started/introduction.html
  Elixir 公式サイト日本語訳:http://elixir-ja.sena-net.works/

Elixir School サイトもおすすめです。Twitter社の Scalar School にインスパイアされたサイトです。

  Elixir School:https://elixirschool.com/
  Elixir School 日本語訳:https://elixirschool.com/jp/

チートシートもおさえておくと記号や関数などすぐに分かるので便利です。
  チートシート:https://media.pragprog.com/titles/elixir/ElixirCheat.pdf

こちらは「elixir cheatsheet」で検索して見つけたクイックリファレンスです。図やコードを多用した分かりやすいサイトです。
  github itsgreggreg/elixir_quick_referencehttps://github.com/itsgreggreg/elixir_quick_reference/blob/master/README.md

書籍について、
Elixir では「プログラミング Elixir / Dave Thomas著」 が最初の日本語 Elixir 本になると思います。翻訳本独特の言い回しがありますがおすすめです。

Elixir に慣れてきたら Erlang についても学んだほうが有利です。 Elixir から Erlang コードをシームレスに実行できますし、Elixir のカンファレンスなどに参加するとカジュアルに Erlang 周りに話が出てくることがあります。
「すごい Erlang ゆかいに学ぼう!」:https://estore.ohmsha.co.jp/titles/978427406912P こちらの本では Erlang 全般の他、 Elixir 本に比べて Supervisor、 Actor モデルや OTP などの理解が深まります。

▼ Phoenix Framework

Phoenix Framework では、
  Phoenix Framework 公式サイト
  「Programming Phoenix: Productive |> Reliable |> Fast」がおすすめです。日本語訳はまだ出版されていませんが読みやすい英語で書かれていると思います。

■ Code Editor

Elixir をサポートしているエディターは複数あり開発しやすい環境は整っています。
公式サイトの情報を一部ですがご紹介すると
  Emacs Mode
  Vim Elixir
  Sublime Plugin
  Atom Package
  Intellij Elixir
  Visual Studio Elixir
などがあります。私は Atom editor で開発しています。

■ 独特(?)なお作法

個人的に分かりづらかった事を紹介します。

|> (パイプライン演算子)

処理の連結。式の結果を次の式の第一パラメータとして渡す。Elixirで一番特徴的な演算子。
conn |> f1(a) |> f2(b)
# conn を評価して次の式に渡す、 次の式ではf1(conn, a)として評価して次の式に渡す、 次の式ではf2(f1(conn, a)のresult, b)として評価される

/n (スラッシュ 数値)

パラメータ数を表します。
Elixirを始めて暫くの間何を表しているのか分かりませんでした。。
is_atom/1
# パラメータが1つである is_atom function のこと

is_atom/2
# パラメータが2つである is_atom function のこと

Atom (アトム)

定義した名前がそのまま値となるコンスタント値。定数やMapのKeyとしてよく利用されます。文字列とは異なります。
iex(1)> :a
:a
iex(2)> :a == "a"
false
# atomのaと文字列のaを比較するとfalseとなります。

Tuple(タプル)

配列やListに近くあらゆる型の値を保持できます。各要素を連続してメモリに保持するのでIndex指定アクセスやSizeの取得がListに比べて高速です。要素の追加など変更は全ての要素をコピーして新しいTupleを作成するので低速です。Atomの:ok/:ngを先頭要素にした形でFunctionの戻り値としてよく利用されます。
iex(1)> tuple = {:ok, "hello"}
{:ok, "hello"}
iex(2)> tuple_size(tuple)
2

説明はこのくらいにして、 Elixir と Phoenix Framework で PostgreSQL への CRUD Webアプリケーションを作成してみましょう。

■ CRUD アプリケーションの作成

Phoenix 公式サイトを参考にします。
http://www.phoenixframework.org/docs/ecto-models

今回作成する DB テーブルはこちらになります。データベースやテーブルの作成は Phoenix Framework の ecto を使って作成します。直接 PostgreSQL を操作するのは Create User のみです。
CREATE TABLE users (
    id integer NOT NULL,
    name character varying(255),
    email character varying(255),
    bio character varying(255),
    number_of_pets integer,
    inserted_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone NOT NULL
);

事前に DB へユーザを作成しておきましょう。
CREATE USER postgres;
ALTER USER postgres PASSWORD 'postgres';
ALTER USER postgres WITH SUPERUSER;

では、CRUD アプリケーションを作成していきましょう。

プロジェクト「phoenix_ecto_sample」の作成
$ mix phoenix.new phoenix_ecto_sample
* creating phoenix_ecto_sample/config/config.exs
* creating phoenix_ecto_sample/config/dev.exs
・・・(省略)
* creating phoenix_ecto_sample/web/views/page_view.ex

Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build

We are all set! Run your Phoenix application:

    $ cd phoenix_ecto_sample
    $ mix phoenix.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phoenix.server

Before moving on, configure your database in config/dev.exs and run:

    $ mix ecto.create

作成されたPJへ移動します
$ cd phoenix_ecto_sample

config ファイルの DB 接続定義を確認しましょう
$ vim config/dev.exs
# Configure your database
config :phoenix_ecto_sample, PhoenixEctoSample.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "phoenix_ecto_sample_dev",
  hostname: "localhost",
  pool_size: 10

※ DB 定義初期値(->上記の定義を変更する事で作成するデータベース名やユーザを変更できます)
※ user/pass [postgres]
※ database [phoenix_ecto_sample_dev] mix phoenix.new で指定したプロジェクト名(dev環境は末尾が_dev)になります。

では DB を 作成します
$ mix ecto.create
==> connection
Compiling 1 file (.ex)
Generated connection app
・・・(省略)・・・
Generated phoenix_ecto_sample app
The database for PhoenixEctoSample.Repo has been created

DB 作成結果を psql コマンドで PostgreSQL へ接続して確認してみます。
$ psql -U postgres
postgres=# \l
                                       List of databases
           Name           |   Owner    | Encoding | Collate | Ctype |     Access privileges
--------------------------+------------+----------+---------+-------+---------------------------
 phoenix_ecto_sample_dev  | postgres   | UTF8     | C       | C     |

※ phoenix_ecto_sample_dev データベースが作成されています

次に users テーブル用の CRUD コードの作成(Scaffold)を行います
$ mix phoenix.gen.html User users name:string email:string bio:string number_of_pets:integer
以下のコードが作成されます。これだけで users テーブルに対する CRUD コードが作成されます。
* creating web/controllers/user_controller.ex
* creating web/templates/user/edit.html.eex
* creating web/templates/user/form.html.eex
* creating web/templates/user/index.html.eex
* creating web/templates/user/new.html.eex
* creating web/templates/user/show.html.eex
* creating web/views/user_view.ex
* creating test/controllers/user_controller_test.exs
* creating web/models/user.ex
* creating test/models/user_test.exs
* creating priv/repo/migrations/20170330062028_create_user.exs

Add the resource to your browser scope in web/router.ex:

    resources "/users", UserController

※ こちら↑の通りに後ほど router.ex ファイルに resources ... を追加します。

Remember to update your repository by running migrations:

    $ mix ecto.migrate

テーブル作成に使用される web/models/user.ex を確認してみましょう。
defmodule PhoenixEctoSample.User do
  use PhoenixEctoSample.Web, :model

  schema "users" do
    field :name, :string
    field :email, :string
    field :bio, :string
    field :number_of_pets, :integer

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio, :number_of_pets])
  end
end

※ schema "users" do の後に mix phoenix.gen.html コマンドで指定した テーブル名やカラムが定義されています。
※ timestamps() はマクロで inserted_at/updated_at カラムを追加します。


router.ex に HTTP アクセスエンドポイントを追加します。
$ vim web/router.ex
defmodule PhoenixEctoSample.Router do
  use PhoenixEctoSample.Web, :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 "/", PhoenixEctoSample do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index

    ※ 追加
    resources "/users", UserController
  end


  # Other scopes may use custom stacks.
  # scope "/api", PhoenixEctoSample do
  #   pipe_through :api
  # end
end

※ [resources "/users", UserController]を追加しました。
※ resources は GET/POST/PUT等のメソッドを追加します。


追加したエンドポイントを確認してみましょう。
$ mix phoenix.routes

※ このコマンドでエンドポイントが全て表示されます(便利ですね)
Compiling 8 files (.ex)
page_path  GET     /                PhoenixEctoSample.PageController :index
user_path  GET     /users           PhoenixEctoSample.UserController :index
user_path  GET     /users/:id/edit  PhoenixEctoSample.UserController :edit
user_path  GET     /users/new       PhoenixEctoSample.UserController :new
user_path  GET     /users/:id       PhoenixEctoSample.UserController :show
user_path  POST    /users           PhoenixEctoSample.UserController :create
user_path  PATCH   /users/:id       PhoenixEctoSample.UserController :update
           PUT     /users/:id       PhoenixEctoSample.UserController :update
user_path  DELETE  /users/:id       PhoenixEctoSample.UserController :delete

※ user_path となっている8つのエンドポイントが resources により追加されました。


次に users テーブルを ecto で作成しましょう。
$ mix ecto.migrate
15:54:56.283 [info]  == Running PhoenixEctoSample.Repo.Migrations.CreateUser.change/0 forward
15:54:56.283 [info]  create table users
15:54:56.317 [info]  == Migrated in 0.0s


作成されたテーブルを psql コマンドで確認してみます。
$ psql -U postgres phoenix_ecto_sample_dev

phoenix_ecto_sample_dev=# \d
                List of relations
 Schema |       Name        |   Type   |  Owner
--------+-------------------+----------+----------
 public | schema_migrations | table    | postgres
 public | users             | table    | postgres
 public | users_id_seq      | sequence | postgres
(3 rows)

※ users テーブルや sequence などが作成されています。

phoenix_ecto_sample_dev=# \d users
                                       Table "public.users"
     Column     |            Type             |                     Modifiers
----------------+-----------------------------+----------------------------------------------------
 id             | integer                     | not null default nextval('users_id_seq'::regclass)
 name           | character varying(255)      |
 email          | character varying(255)      |
 bio            | character varying(255)      |
 number_of_pets | integer                     |
 inserted_at    | timestamp without time zone | not null
 updated_at     | timestamp without time zone | not null
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

※ users テーブルに nameやemailなど指定したカラムが作成されています。

ここまでの操作、router.ex への 1行の実装のみで CRUD Web アプリケーションが完成しました。


では、 Web アプリケーションを起動してみましょう。
$ mix phoenix.server
[info] Running PhoenixEctoSample.Endpoint with Cowboy using http://localhost:4000
30 Mar 15:59:50 - info: compiled 6 files into 2 files, copied 3 in 1.5 sec

アプリケーションが起動したのでブラウザでアクセスしてみましょう。
http://localhost:4000/users

一覧画面が表示されます。
Hello_PhoenixEctoSample_01list


[New user]リンクからユーザ情報を作成してみましょう。
Hello_PhoenixEctoSample_02c
各項目を入力した状態です。
[Submit]ボタンをクリックして登録してみます。


一覧画面で情報が登録されていることが確認できます。
Hello_PhoenixEctoSample_03list


psql で DB を直接確認してもレコードが登録されています。
phoenix_ecto_sample_dev=# select * from users;
 id |   name    |     email     |    bio    | number_of_pets |        inserted_at         |         updated_at
----+-----------+---------------+-----------+----------------+----------------------------+----------------------------
  1 | user name | mail@mail.com | biography |              1 | 2017-03-30 07:04:31.541726 | 2017-03-30 07:04:31.562391
(1 row)


■ 最後に

Elixir/Phoenix Framework を使って簡単に CRUD Web アプリケーションを作成出来る事が分かるかと思います。個人的な意見ですが、 Scala と比較すると Elixir の方が学習コストがかなり低いと感じます。 Ruby よりも処理速度が速くて、 ErlangVM の堅牢さの上で動作する事はメリットが大きいのではないでしょうか。

Elixir/Phoenix Framework は登場して数年しか経過していませんが、確実に利用者が増えてきていて、本番サービスでの利用も多数出てきています。 2017/4/1に Elixir Conf Japan 2017 もありますし、日本語書籍も増えてきています。今年はさらに注目が高まることでしょう!

次回は Elixir/Phoenix で WebSocket アプリケーションを作成した話(VR環境に構築したプレゼンテーションルーム)についてまとめる予定です。


次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。