備忘録

働きたくないでござる

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]}

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