備忘録

働きたくないでござる

phx.new のオプションについて

Rails では細かくオプションを指定してプロジェクト作成することができますが、 Phoenix Framework でもいくつかのオプションがあるようです。
最新の v1.3 のオプションについて記載されている記事が見当たらなかったのでまとめてみたいと思います。

公式のドキュメントについて

phx.new に関するドキュメントは、私が見た限りでは Hexdocs のこのページぐらいかなと思います。(他にあったら教えてください!)
Installation – Phoenix v1.3.0

phx.new/lib/mix/tasks に配置されていないので Hexdocs のサイドバーの MIX TASKS にはドキュメントがありません。
プログラム本体はここらへんにあるようなのでこのコードを読みながら記事を書いていきたいと思います。 https://github.com/phoenixframework/phoenix/tree/master/installer/lib

コマンド

phx.new

普通に Phoenix プロジェクトを作成するコマンドです。 Ecto も Web も両方インストールされます。
Umbrella プロジェクトではなく、単体で動作させるプロジェクトを作るときに使うコマンドですね。(もちろん、 Umbrella 下でも使えます。)

phx.new.ecto

Umbrella プロジェクト下での モデル用 Phoenix プロジェクトを作成するコマンドです。 mix phx.new PROJECT_NAME --no-brunch --ecto コマンドとやってることは同じようですが、Umbrella プロジェクトでないと使用できない点に注意する必要があります。

phx.new.web

Umbrella プロジェクト下での Web 用 Phoenix プロジェクトを作成するコマンドです。 Umbrella プロジェクトじゃないと使えないようです。

オプション

ぶっちゃけここのコメントに書いてあることが全てです。
https://github.com/phoenixframework/phoenix/blob/master/installer/lib/mix/tasks/phx.new.ex

--umbrella

Umbrella プロジェクトとして Phoenix プロジェクトを作成します。
具体的には、モデル(ドメイン)用のプロジェクトと、 Web用のプロジェクトの2つを作ります。
mix phx.new hello --umbrella と実行した場合、以下のように作られます。

hello_umbrella/   Hello.Umbrella
  apps/
    hello/        Hello
    hello_web/    HelloWeb

--app

OTP アプリケーション名を指定できます。
--module と似ていますが、 --module はファイル名はアプリケーション名でモジュール名が指定された値になるのに対し、 --app はファイル名もモジュール名も指定された値になります。
--app はダウンケース・スネークケースで指定する必要があります。
この説明だけだとわかりにくいですが、実行するとこうなります。

$ mix phx.new sample_app --app hoge_app
...
* creating sample_app/lib/hoge_app.ex
...

sample_app/lib/hoge_app.ex

defmodule HogeApp do
end

--module

モジュール名やモジュールの接頭辞を指定できます。
--module はキャメールケースで指定する必要があります。
実行するとこうなります。

$ mix phx.new sample_app2 --module HogeApp2
...
* creating sample_app2/lib/sample_app2.ex
...

sample_app2/lib/sample_app2.ex

defmodule HogeApp2 do
end

--app と違ってファイル名とモジュール名が異なってしまうので、 --app になにかトラップめいたものがなければ使わないほうがわかりやすいかなと思っています。

--database

使用する RDB を指定することができます。
デフォルトは PostgreSQL で、他には MySQLMicrosoft SQL Server, SQLite, MongoDB などを指定できます。

Ecto – Phoenix v1.3.0

--no-brunch

Phoenix ではフロントエンドのビルドツールとして Brunch をインストールしますが、このオプションで除外することができます。 API サーバーや Brunch 以外のビルドツールを使用したい場合に便利ですね。

--no-ecto

Ecto を除外することができます。
phx.new.ecto--ecto のオプションと書きましたが、 --ecto なんてオプションについて記載されていません。
これは phx.new.ecto --no-ecto と矛盾したコマンドを実行されたときに上書きするためのオプションではないかなと思います。(未検証)

--no-html

静的 HTML の生成を除外できます。
ちなみに --no-brunch だと静的ファイルがビルドされないので表示崩れするのではないかなと思ったのですが、そこらへん上手くやっているようですね。 https://github.com/phoenixframework/phoenix/blob/v1.3.0/installer/lib/phx_new/web.ex#L88

--binary-id

Ecto で使用するデータベースのプライマリキーを id 以外のものにしていいできるらしいです。
あまり興味ないので調べてもいないし検証もしてません。

phx.gen.* とオプションの関係について

phx.gen.* では Ecto に依存したコンテキストファイルを作成するため、 --no-ecto オプションを使用すると期待通りの動作をしなくなる場合があるらしいです。 この問題を回避するためには、 phx.gen.* コマンド実行時に --no-context オプションを指定することでコンテキストファイルの作成をスキップできます。 同様に --no-html でも重要な HTML コンポーネントが生成されないので phx.gen.html コマンドが壊れる可能性があるようです。

所感

コメントの劣化和訳のようになってしまいましたが、間違い等あれば教えてください!!!

Elixir の Umbrella プロジェクトで Phoenix アプリケーションを作成 (4)

前回: giraphme.hatenablog.com

前回は Phoenix アプリケーションの追加と Cowboy サーバー起動までをやったので、今日は REST API を作成していきます。

構成

恒例の構成を再掲。

$ tree .
.
├── apps
│   ├── us_core # DB関連のビジネスロジックをもった Elixir アプリケーション
│   └── us_api # REST API を提供する Phoenix アプリケーション ← イマココ
│   └── us_web # React を利用した View を提供する Phoenix アプリケーション
│   └── us_admin_web # 管理画面を提供する Phoenix アプリケーション

API Endpoints

今回は読み込み系2つのみの作成です。(Rails の resources 的には index と show )

GET /api/v1/articles

OUT

{
  "articles": [
    {
      "id": 1234,
      "title": "EMT",
      "body": "エミリアたんマジ天使",
      "created_at": "2017-09-21 00:00:00",
      "updated_at": "2017-09-21 00:00:00",
    }
  ]
}

GET /api/v1/articles/:id

OUT

{
  "article": {
    "id": 1234,
    "title": "EMT",
    "body": "エミリアたんマジ天使",
    "created_at": "2017-09-21 00:00:00",
    "updated_at": "2017-09-21 00:00:00",
  }
}

ジェネレーターコマンド

コマンドで一通りのファイルを作成することができますが、今回はモデルは不要で Controller と View だけが必要です。 以下のコマンドを実行してヘルプを見てみましょう。

$ mix help phx.gen.json

                                mix phx.gen.json

Generates controller, views, and context for an JSON resource.

    mix phx.gen.json Accounts User users name:string age:integer

...

## Generating without a schema or context file

In some cases, you may wish to bootstrap JSON views, controllers, and
controller tests, but leave internal implementation of the context or schema to
yourself. You can use the --no-context and --no-schema flags for file
generation control.

...

抜粋しましたが、 --no-context --no-schema オプションを付けると良さそうですね。 というわけで私の場合はこんなコマンドを実行しました。

$ mix phx.gen.json UsCore Article articles  --no-context --no-schema

コマンド実行後 apps/us_api/lib/us_api_web/* などにジェネレートされます。
なお、私のプロジェクトの場合は us_api_web を消してしまった関係で、それぞれ手動でファイル移動する必要がありました。(出力先のパスを指定するオプションは存在しないっぽい)

それと、ジェネレートされたファイル内のモデル呼び出しやモジュール名(UsApiWeb など)は適宜変更してください。

Controller, Routing

今回は以下のようにコントローラーとルーティングを定義しました。

apps/us_api/lib/controllers/article_controller.ex

defmodule UsApi.ArticleController do
  use UsApi, :controller

  action_fallback UsApi.FallbackController

  def index(conn, _params) do
    articles = UsCore.list_articles()
    render(conn, "index.json", articles: articles)
  end

  def show(conn, %{"id" => id}) do
    article = UsCore.get_article!(id)
    render(conn, "show.json", article: article)
  end
end

apps/us_api/lib/router.ex

defmodule UsApi.Router do
  use UsApi, :router

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

  scope "/api/v1", UsApi do
    pipe_through :api

    resources "/articles", ArticleController, only: [:index, :show]
  end
end

UsCore のインターフェイス

今回、 UsApi 等外部サービスへのインターフェイスapps/us_core/lib/us_core.ex にまとめる予定です。
どう考えても肥大化して Fat になるので、分散方法については後日運用しながら記事にしたいと思います。

apps/us_core/lib/us_core.ex

defmodule UsCore do
  import Ecto.Query
  import UsCore.Repo

  alias UsCore.Article

  def get_article!(id) do
    Article |> get(id)
  end

  def list_articles() do
    Article |> all
  end
end

アクセスしてみる

とりあえずアクセスできる状態になったはずなので、アクセスしてみましょう。

$ curl http://0.0.0.0:4000/api/v1/articles | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    19  100    19    0     0   1067      0 --:--:-- --:--:-- --:--:--  1117
{
  "data": [
    {
      "id": 1
    }
  ]
}

希望するレスポンス構造になっていませんが、リクエストの発行とレスポンスの取得に成功しました。

レスポンス構造の定義

これまでずっと Rails を触ってきたので、 json.jbuilder 的な DSL 記法をできそうなファイルを探していたのですが、Phoenix ではまじめにモジュールで書く必要があるようです。
Partial が各所にばらまかれている jbuilder に比べて、個人的にはこっちのほうが好みだなと思いました。

apps/us_api/lib/views/article_view.ex

defmodule UsApi.ArticleView do
  use UsApi, :view
  alias UsApi.ArticleView

  def render("index.json", %{articles: articles}) do
    %{articles: render_many(articles, ArticleView, "article.json")}
  end

  def render("show.json", %{article: article}) do
    %{article: render_one(article, ArticleView, "article.json")}
  end

  def render("article.json", %{article: article}) do
    %{
      id: article.id,
      title: article.title,
      body: article.body,
      created_at: article.created_at,
      updated_at: article.updated_at,
    }
  end
end
$ curl http://0.0.0.0:4000/api/v1/articles | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   161  100   161    0     0   2012      0 --:--:-- --:--:-- --:--:--  2037
{
  "articles": [
    {
      "updated_at": "2017-09-28T15:14:59.000000",
      "title": "EMT",
      "id": 1,
      "created_at": "2017-09-28T15:14:59.000000",
      "body": "エミリアたんマジ天使"
    }
  ]
}
$ curl http://0.0.0.0:4000/api/v1/articles/1 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   158  100   158    0     0   4327      0 --:--:-- --:--:-- --:--:--  4388
{
  "article": {
    "updated_at": "2017-09-28T15:14:59.000000",
    "title": "EMT",
    "id": 1,
    "created_at": "2017-09-28T15:14:59.000000",
    "body": "エミリアたんマジ天使"
  }
}

なんだか雑なコードなので不安はありますが、要件は満たしているので今日はこれぐらいにしといてやろう......。
余談ですが、レコードを大量に作って LIMIT をつけて速度を見てみた感じ、100件で 3ms, 1,000件で 20ms, 5,000件で 160ms, 10,000件で 376ms のレスポンスタイムでした。
実際のプロジェクトでこれほど単純なデータを扱うことは中々ないですが、すごく速いですね。

以上です。

Elixir の Umbrella プロジェクトで Phoenix アプリケーションを作成 (3)

前回: giraphme.hatenablog.com

今回はお待ちかね、 Phoenix アプリケーションを追加したいと思います。

構成

恒例(?) の構成を再掲。

$ tree .
.
├── apps
│   ├── us_core # DB関連のビジネスロジックをもった Elixir アプリケーション
│   └── us_api # REST API を提供する Phoenix アプリケーション ← イマココ
│   └── us_web # React を利用した View を提供する Phoenix アプリケーション
│   └── us_admin_web # 管理画面を提供する Phoenix アプリケーション

Phoenix アプリケーションを作成

構成にも書いていますが、 apps 下に us_api という名前で Phoenix アプリケーションを作成します。
なお、 archive.install や hex, rebar をインストールしている前提です。
(このブログでも以前取り上げましたが、クソ記事なのでリンクは載せません...)

$ cd apps
$ mix phx.new us_api

Database の設定を削除

us_api にはデータベースへの接続権限を持たせたくないので、自動で作られた設定を消していきます。
Phoenix 1.5.0 の時点では --database の設定を消すことはできないようです。phoenix/generator.ex at v1.3 · phoenixframework/phoenix · GitHub )

Phoenix のバージョンに依存して大きく作業手順がことなりそうなのと、 diff が大きすぎて書ききれないので要点だけ書くと、

  • apps/us_api/lib/us_api は不要なので全て消す。
  • apps/us_api/lib/us_api_web の中身は apps/us_api/lib/ に移動し、 UsApiWeb や us_api_web を UsApi 等に置換する。
  • Ecto で検索して適宜削除

地味面倒なので、試したいだけなら放置して進めてもいいかも......?

サーバー起動

一通り DB の設定を消せたかなと思ったら、以下のコマンドを入力してビルド・サーバーを起動してみましょう。

$ mix deps.get
$ cd assets && npm install
$ cd ..
$ mix compile
$ mix phx.new
=INFO REPORT==== 28-Sep-2017::00:11:30 ===
    application: logger
    exited: stopped
    type: temporary
** (Mix) Could not start application us_api: exited in: UsApi.Application.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function UsApi.Application.start/2 is undefined (module UsApi.Application is not available)
            UsApi.Application.start(:normal, [])
            (kernel) application_master.erl:273: :application_master.start_it_old/4

おそらくこの状況ではこのようなエラーが出ると思うので、以下のように修正していきます。

apps/lib/us_api/mix.exs

  def application do
    [
      mod: {UsApi.Application, []},
-     extra_applications: [:logger, :runtime_tools]
+     extra_applications: [
+       :logger, :gettext, :runtime_tools,
+       :phoenix, :phoenix_html, :cowboy,
+       :us_core,
      ]
    ]
  end

apps/blog_web/lib/application.ex

defmodule UsApi.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec

    children = [
      supervisor(UsApi.Endpoint, []),
      # worker(UsApi.Worker, [arg1, arg2, arg3]),
    ]

    opts = [strategy: :one_for_one, name: UsApi.Supervisor]
    Supervisor.start_link(children, opts)
  end

  def config_change(changed, _new, removed) do
    UsApi.Endpoint.config_change(changed, removed)
    :ok
  end
end

``

  defp deps do
    [
      {:phoenix, "~> 1.3.0"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
-     {:cowboy, "~> 1.0"}
-     {:cowboy, "~> 1.0"},
+     {:us_core, in_umbrella: true}
    ]
  end
$ mix phx.new
$ mix phx.server
[info] Running UsApi.Endpoint with Cowboy using http://0.0.0.0:4000
00:22:06 - info: compiled 6 files into 2 files, copied 3 in 902 ms

めでたく phx.server が起動するようになりました。 ちなみに mix phx.server コマンドは、 apps/us_api でもプロジェクトのルートディレクトリでも実行可能です。


正直、DB の設定消すのとてもつらかった......。 でもプロジェクト内のファイルに一通り目を通すいい機会にもなったので、ぜひ一度お試しあれ(死亡フラグ

Elixir の Umbrella プロジェクトで Phoenix アプリケーションを作成 (2)

前回: giraphme.hatenablog.com

前回はモデル作ったので今回はテストを書いていきます。 とはいっても、普通の Elixir アプリケーションのテスト作成とあまり違いはないですが.....。

構成

前回も書きましたが、構成を再掲。

$ tree .
.
├── apps
│   ├── us_core # DB関連のビジネスロジックをもった Elixir アプリケーション ← イマココ
│   └── us_api # REST API を提供する Phoenix アプリケーション
│   └── us_web # React を利用した View を提供する Phoenix アプリケーション
│   └── us_admin_web # 管理画面を提供する Phoenix アプリケーション

DB の準備

テストの前に DB を作っておきます。

$ MIX_ENV=test mix ecto.create
$ MIX_ENV=test mix ecto.migrate

そして、テストのたびに Truncate しないでいいよう、 Ecto.Adapters.SQL.Sandbox の設定をしていきます。

apps/us_core/config/config.exs

if Mix.env == :test do
  config :us_core, UsCore.Repo,
    pool: Ecto.Adapters.SQL.Sandbox
end

apps/us_core/test/test_helper.exs

+ Ecto.Adapters.SQL.Sandbox.mode(UsCore.Repo, :manual)
  ExUnit.start()

apps/us_core/config/config.exs

if Mix.env == :test do
  config :us_core, UsCore.Repo,
    pool: Ecto.Adapters.SQL.Sandbox
end

この状態で、各テストモジュールの中に以下を書いてあげればサンドボックスモードで DB を扱うことができます。

      setup do
        :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
      end

設定を共通化

テストファイルを増やすたびに alias やら setup をコピペするのは DRY ではないので、共通化して 1モジュールをインポートするだけでいいように変更します。

apps/us_core/test/test_helper.exs

  Ecto.Adapters.SQL.Sandbox.mode(UsCore.Repo, :manual)

+ {:ok, files} = File.ls("./test/support")
+
+ Enum.each files, fn(file) ->
+   Code.require_file "support/#{file}", __DIR__
+ end

  ExUnit.start()

apps/us_core/test/support/basic_settings_helper.exs

defmodule BasicSettingsHelper do
  defmacro __using__(_) do
    quote do
      use ExUnit.Case, async: true
      alias UsCore.Article
      alias UsCore.Repo

      setup do
        :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
      end
    end
  end
end

テストファイル作成

さて、本題のテストファイルを作ってみましょう。

apps/us_core/test/us_core/article_test.exs

defmodule UsCore.ArticleTest do
  use BasicSettingsHelper
  doctest UsCore.Article

  test "レコードを作れること" do
    %Article{
      title: "EMT",
      body: "エミリアたんマジ天使"
    } |> Repo.insert

    assert Article |> Repo.aggregate(:count, :id) == 1
  end
end

テスト実行

2回実行してみて、失敗したらサンドボックスモードの設定に失敗しているのかもしれません。

$ mix test
==> us_core

21:17:03.182 [debug] QUERY OK db=1.3ms
INSERT INTO `articles` (`body`,`title`,`created_at`,`updated_at`) VALUES (?,?,?,?) ["エミリアたんマジ天使", "EMT", {{2017, 9, 27}, {12, 17, 3, 163903}}, {{2017, 9, 27}, {12, 17, 3, 165464}}]

21:17:03.191 [debug] QUERY OK source="articles" db=1.8ms
SELECT count(a0.`id`) FROM `articles` AS a0 []
.

Finished in 0.07 seconds
1 test, 0 failures

Randomized with seed 106344

書き忘れていることがあるかもしれないので、もしテストに失敗するようなら教えていただけると嬉しいです。
今日は軽めの内容でしたがこれくらいで。

Elixir の Umbrella プロジェクトで Phoenix アプリケーションを作成 (1)

Umbrella のプロジェクト作成は mix new の時に --umbrella を付けるだけで作成できます。
あとは apps ディレクトリに cd したあと $ mix phx.new PROJECT_NAME などで Phoenix のアプリケーションなどを作っていきましょう。
Umbrella プロジェクト自体に関する説明は公式リファレンスやプログラミング Elixir などをご覧ください。

構成

さて、今回は以下のような構成を想定しています。

$ tree .
.
├── apps
│   ├── DB関連のビジネスロジックをもった Elixir アプリケーション
│   └── REST API を提供する Phoenix アプリケーション
│   └── React を利用した View を提供する Phoenix アプリケーション
│   └── 管理画面を提供する Phoenix アプリケーション

将来的に自社サービスをマイクロサービス化する予定なのですが、 apps 下のアプリケーションたちをそれぞれ独立して動く状態にした上で gitsubmodule 等で管理していきたいなと考えています。
このままでは扱いづらいので以降はこのように呼称します。

  • us_core : DB関連のビジネスロジックをもった Elixir アプリケーション
  • us_api : REST API を提供する Phoenix アプリケーション
  • us_web : React を利用した View を提供する Phoenix アプリケーション
  • us_admin_web : 管理画面を提供する Phoenix アプリケーション

設計

何を作ろうか悩ましいですが、今回はサーバーサイドアプリケーションのカークラス的存在の、ブログ作成を想定して進めて行きます。

DB

articles:
  id:integer
  title:string
  body:text
  created_at:datetime
  updted_at:datetime

API Endpoints

GET /api/v1/articles

OUT

{
  "articles": [
    {
      "id": 1234,
      "title": "EMT",
      "body": "エミリアたんマジ天使",
      "created_at": "2017-09-21 00:00:00",
      "updated_at": "2017-09-21 00:00:00",
    }
  ]
}

ブログ本体

  • GET /articles(?p=\d+)? 記事一覧
  • GET /articles/:id 記事詳細ページ

管理画面

  • GET /admin/articles(?p=\d+)? 記事一覧
  • GET /admin/articles/:id 記事詳細ページ
  • GET /admin/articles/new 記事作成ページ
  • POST /admin/articles/:id 記事登録
  • GET /admin/articles/:id/edit 記事編集ページ
  • PATCH /admin/articles/:id 記事編集
  • DELETE /admin/articles/:id 記事削除

何の変哲もない、リレーションすらないつまらないプロジェクトですがシンプルさを保つためにこのような設計にしています。

us_core : DB関連のビジネスロジックをもった Elixir アプリケーション

まずは複雑さが少ないただの Elixir アプリケーションを作っていきましょう。

$ cd apps
$ mix new us_core
$ cd us_core

MySQL を使えるように

apps/us_core/mix.exs を以下のように変更し、 deps.get を実行

  def application do
    [
-     extra_applications: [:logger]
+     extra_applications: [:logger, :mariaex, :ecto]
    ]
  end

  defp deps do
    [
-     # {:dep_from_hexpm, "~> 0.3.0"},
-     # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
-     # {:sibling_app_in_umbrella, in_umbrella: true},
+     {:mariaex, "~> 0.8.3"},
+     {:ecto, "~> 2.2.4"}
    ]
  end
$ mix deps.get

Repo を作成

apps/us_core/lib/us_core/repo.ex を作成し、設定をいくつか記述していきます。 以下のコマンドを実行して指示通り設定を書いていきましょう。 supervisor(UsCore.Repo, []) の部分は Supervisor について自分の理解が浅かったので Github で公開されているプロジェクト等を参考に設定しました。

$ mix ecto.gen.repo -r UsCore.Repo
* creating lib/us_core
* creating lib/us_core/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/us_core/application.ex):

    supervisor(UsCore.Repo, [])

And to add it to the list of ecto repositories in your
configuration files (so Ecto tasks work as expected):

    config :us_core,
      ecto_repos: [UsCore.Repo]

apps/us_core/lib/application.ex

defmodule UsCore.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    Supervisor.start_link([
      supervisor(UsCore.Repo, [])
    ], [
      strategy: :one_for_one, name: UsCoreSupervisor
    ])
  end
end

apps/us_core/mix.exs

  def application do
    [
-     extra_applications: [:logger, :mariaex, :ecto]
+     extra_applications: [:logger, :mariaex, :ecto],
+     mod: { UsCore.Application, [] }
    ]
  end

apps/us_core/config/config.exs

+ config :us_core,
+   ecto_repos: [UsCore.Repo]

DB へのアクセス情報の書き方ですが、単体で動作させたい & Umbrella プロジェクト側からアクセス先を上書きさせたいため、 apps/us_core/config/config.exs に以下の様な設定を書いてみました。

cond do
  File.exists?("#{__DIR__}/../../../config/database.#{Mix.env}.exs") ->
    import_config "#{__DIR__}/../../../config/database.#{Mix.env}.exs"
  File.exists?("#{__DIR__}/database.#{Mix.env}.exs") ->
    import_config "#{__DIR__}/database.#{Mix.env}.exs"
end

これで

  • /config/database.#{env}.exs
  • /apps/us_core/config/database.#{env}.exs

が読み込まれるようになります。(すべて gitignore します。)
/config/database.dev.exs/apps/us_core/config/database.dev.exs を作って以下の内容を書き込み、試しにデータベースを作成します。

use Mix.Config

config :us_core, UsCore.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "root",
  password: "",
  database: "us_core_dev",
  hostname: "localhost",
  pool_size: 10
$ mix ecto.create
==> us_core
Compiling 2 files (.ex)
Generated us_core app
The database for UsCore.Repo has been created

モデルとマイグレーションの作成

ecto はマイグレーションの作成コマンドはあるけど、モデルの作成コマンドはない(なんで......)ので、マイグレーションファイルだけコマンドで作成して、モデルは温かみのある手書きで作る必要があります。
ちなみに、 Phoenix を使っている場合は phx.gen.model でモデルとマイグレーションをまとめて作れます。(これ、 Phoenix じゃなくて ecto の方に欲しい)

$ mix ecto.gen.migration create_articles_table

priv/repo/migrations/20170922110639_create_articles_table.exs

defmodule UsCore.Repo.Migrations.CreateArticlesTable do
  use Ecto.Migration

  def change do
    create table(:articles) do
      add :title, :string
      add :body, :text
      timestamps(inserted_at: :created_at)
    end
  end
end
$ mix ecto.migrate

f:id:giraphme:20170922201307p:plain

手前味噌ですが、 created_at, updated_at のカラム名を使いたい場合はこの記事をご参照ください。 Elixir, Phoenix でデフォルトで created_at を使う方法 - 備忘録

モデルはこんな感じです。

defmodule UsCore.Article do
  use Ecto.Schema

  schema "articles" do
    field :title, :string
    field :body, :string
    timestamps(inserted_at: :created_at)
  end
end

モデルを単体で使ってみる

さっそく iex で使ってみます。

$ iex -S mix
Erlang/OTP 20 [erts-9.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Compiling 4 files (.ex)
Generated us_core app
Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> import Ecto.Query
Ecto.Query
iex(2)> import UsCore.Repo
UsCore.Repo
iex(3)> alias UsCore.Article
UsCore.Article
iex(4)> Article |> all

20:25:59.297 [debug] QUERY OK source="articles" db=1.9ms
SELECT a0.`id`, a0.`title`, a0.`body`, a0.`created_at`, a0.`updated_at` FROM `articles` AS a0 []
[]
iex(5)> %Article{title: "EMT", body: "エミリアたんマジ天使"} |> insert

20:30:28.892 [debug] QUERY OK db=6.5ms
INSERT INTO `articles` (`body`,`title`,`created_at`,`updated_at`) VALUES (?,?,?,?) ["エミリアたんマジ天使", "EMT", {{2017, 9, 22}, {11, 30, 28, 884588}}, {{2017, 9, 22}, {11, 30, 28, 885999}}]
{:ok,
 %UsCore.Article{__meta__: #Ecto.Schema.Metadata<:loaded, "articles">,
  body: "エミリアたんマジ天使",
  created_at: ~N[2017-09-22 11:30:28.884588], id: 1, title: "EMT",
  updated_at: ~N[2017-09-22 11:30:28.885999]}}
iex(6)> Article |> get(1)

20:30:46.248 [debug] QUERY OK source="articles" db=17.2ms
SELECT a0.`id`, a0.`title`, a0.`body`, a0.`created_at`, a0.`updated_at` FROM `articles` AS a0 WHERE (a0.`id` = ?) [1]
%UsCore.Article{__meta__: #Ecto.Schema.Metadata<:loaded, "articles">,
 body: "エミリアたんマジ天使",
 created_at: ~N[2017-09-22 11:30:29.000000], id: 1, title: "EMT",
 updated_at: ~N[2017-09-22 11:30:29.000000]}

今日のところはこれぐらいにしといてやるか......。

Elixir, Phoenix の開発環境を Docker で構築

あまりにもわからないこと多すぎて、しばらくコードを書くのを諦めて読書してました。

とりあえず Elixir 触るなら プログラミング Elixir は必読かなという感覚です。

プログラミングElixir

プログラミングElixir

というわけで本格的に開発に入っていきたいのですが、その前に開発環境を Docker で作成したいと思います。

事前準備

macOS 上に環境構築するので Docker for mac をインストールします。
あと、ローカル環境で mix 動かしたりするので Elixir, mix, hex, phoenix あたりもインストールします。 ちなみに RDBMySQL を想定しています。

プロジェクト作成

プロジェクト作成はローカル環境で行います。
Docker 上でプロジェクト作成から Docker 上でやるのはちょっと面倒だなと思ったのだけど、スマートな方法ってあるのだろうか……?

$ mix local.hex
$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
$ mix phx.new docker_sample --database mysql
...
* creating docker_sample/assets/static/robots.txt
* creating docker_sample/assets/static/images/phoenix.png
* creating docker_sample/assets/static/favicon.ico

Fetch and install dependencies? [Yn] Y

ファイルたちを準備

以下をコピペしつつ環境に合わせて微調整してください。

Dockerfile

FROM elixir:latest

ARG app_path
ARG app_port
ENV APP_PORT $app_port

WORKDIR $app_path
EXPOSE $app_port

RUN \
  set -x && \
  apt-get update && \
  apt-get install -y \
    nodejs \
    npm \
    mysql-client \
    inotify-tools \
    git \
    --no-install-recommends && \
  rm -rf /var/lib/apt/lists/* && \
  npm cache clean && \
  npm install n -g && \
  n stable && \
  ln -sf /usr/local/bin/node /usr/bin/node && \
  apt-get purge -y nodejs npm

COPY . $app_path

RUN mix local.hex --force
RUN mix local.rebar --force

CMD ["mix", "phx.server"]

docker-compose.yml

version: '3.1'
services:
  mysql:
    image: mysql:5.7.10
    environment:
      MYSQL_ROOT_PASSWORD: secret
    volumes:
      - ./datadir/mysql/:/var/lib/mysql
  app:
    build:
      context: .
      args:
        app_path: /var/src/app
        app_port: 4000
    ports:
      - '4000:4000'
    volumes:
      - .:/var/src/app
    links:
      - mysql

.dockerignore

Dockerfile を編集する度に COPY で毎回キャッシュが使われなくてうるさいので……。

Dockerfile
docker-compose.yml
.git
.gitignore

config/dev.exs

config :docker_sample, DockerSample.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "root",
- password: "",
+ password: "secret",
  database: "docker_sample_dev",
- hostname: "localhost",
+ hostname: "mysql",

build, up

$ docker-compose build
$ docker-compose run app mix deps.get
$ docker-compose up -d
$ docker-compose exec app mix ecto.create

これで完了のはず?

課題

  • ローカルで docker-compose run app mix deps.get するのいけてない印象がある。
    (docker-compose build するだけで mix.lock が更新されてしまう可能性があるのも気持ち悪い気がするので、悩ましい)
  • umbrella プロジェクトを使ってみたいと思っているので、その時にまた作り直す必要がある。
  • MySQLレプリケーションをしたいのだけど、 複数の Dockerfile をどういうディレクトリ構造で配置するのがベストなのかわからない。

参考

Dockerfile の作り方勉強させていただきました。 感謝 m( )m

qiita.com

h3poteto.hatenablog.com

Elixir, Phoenix でデフォルトで created_at を使う方法

Rails から Elixir に移行しようと思っているので、 inserted_atではなく created_at を使いたいと思っています。
ただ、 Ecto で created_at を使うにはマイグレーションとモデルに timestamps(inserted_at: :created_at) にわざわざ書かないといけないので事故りそうです。

めんどうなのでデフォルトで created_at を使うようにしたいのですが、結論から書くとは現行の安定バージョンではできないようです。 しかし、 elixir_ecto の v2.2.0-rc.1 では migration_timestamps というパラメーターが追加されています。
ecto/migration.ex at v2.2.0-rc.1 · elixir-ecto/ecto · GitHub

現状の Phoenix では

Failed to use "ecto" (version 2.2.0-rc.1) because
  phoenix_ecto (versions 3.2.0 to 3.2.3) requires ~> 2.1 *
  mix.exs specifies ~> 2.2.0-rc.1

と怒られるので使用できませんが、今後使えるようになるようですね。

ちなみに使用する時は config/config.exs に以下のように書けばいいような雰囲気を感じています。

config :sample, Sample.Repo,
  migration_timestamps: [inserted_at: :created_at]

migration_primary_key も追加されているのでプライマリーキーを変更することもできそうですし、タイムスタンプnaive_datetimeutc_datetime に変更することもできるようです。

所感

Elixir を初めてまだ2日目ですが、 Elixir や Phoenix などの進化速度の速さに驚いています。 前情報ではエコシステムの小ささがネックと聞いていましたが、今のところあまり不満は感じていないです。