備忘録

働きたくないでござる

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

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 などの進化速度の速さに驚いています。 前情報ではエコシステムの小ささがネックと聞いていましたが、今のところあまり不満は感じていないです。

Elixir チュートリアル (3) Phoenix 導入

過去記事 giraphme.hatenablog.com giraphme.hatenablog.com

今回は Phoenix を使えるところまで進めたいと思います。

Phoenix のインストール

公式の言う通り これで最新版のアーカイブを取得できます。

$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez

もちろん、任意のバージョンを探してきて取得することも可能です。
GitHub - phoenixframework/archives: Holds archives for released Phoenix versions

プロジェクトを作成

Mix でプロジェクトを作成します。

$ mix phoenix.new sample
* creating sample/config/config.exs
* creating sample/config/dev.exs
* creating sample/config/prod.exs
* creating sample/config/prod.secret.exs
* creating sample/config/test.exs
* creating sample/lib/sample.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 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

この状態で mix phoenix.server しても自分の環境では

[error] Postgrex.Protocol (#PID<0.3406.0>) failed to connect: ** (Postgrex.Error) FATAL 28000 (invalid_authorization_specification): role "postgres" does not exist

と言われて動かすことができないので、 PostgreSQL の設定をするか MySQL を使えるようにしましょう。

MySQL の設定

私の場合、普段の業務で MySQL を使用しており、環境が整っているので MySQL を使えるように設定しました。

まずは /mix.exsdeps に以下を追記

  defp deps do
    [
      {:phoenix, "~> 1.3.0-rc"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      ...
      {:mariaex, "~> 0.1"}, # ここ
      ...
    ]
  end

そして config/dev.exs# Configure your database の箇所を以下のように変更します。

config :sample, Sample.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "ユーザー名",
  password: "パスワード",
  database: "DB名",
  hostname: "localhost",
  pool_size: 10

その後、以下のコマンドを実行してください。

$ mix deps.get
$ mix phoenix.server

これで http://0.0.0.0:4000 にアクセスすると Web サーバーが起動していることを確認できるはずです。

所感

dev, prod など中途半端に省略されていることが多くて気持ち悪い……。

Elixir チュートリアル (2) 用語編

前回: giraphme.hatenablog.com

とりあえず動かすことができたので、今回は Elixir を勉強するにあたって出会ったワードたちについてまとめていこうと思います。 ちなみに、一覧性を重視して深いことは書かないので、詳細はググッてください。決して深いことを書いてマサカリ投げられたくないからではないです。 どうせ後から増えていくので随時追加予定です。

Erlang

細かいことを書き出すと怖い人からマサカリ投げつけられそうなので簡単な紹介にしておきますが、 Wikipedia 曰く並行処理指向のプログラミング言語および実行環境らしいです。 簡単にプロセス生やせてめっちゃ早いって偉い人が言ってました。

Erlang VM

Elixir の説明を見ていると、「Erlang VM(BEAM)上で動く Ruby っぽい関数型言語」っと説明されていることが多いような気がします。
Erlang VM とは Erlang で書かれた仮想マシンで、Erlang/OTP で書かれたプログラムはこの仮想マシン上で動作させます。

Erlang/OTP

OTP is 何

Elixir

公式曰く

Elixir is a dynamic, functional language designed for building scalable and maintainable applications.

らしいです。
Elixir 自体については後日記事を書きながら紹介していきます。

iex

Elixir の REPL です。 Ruby で言う irb や pry。
brew で elixir インストールすると勝手にインストールされます。

mix

Ruby でいう Bundler 的なやつだという説明をよく見ますが、使っているところを見る限りでは Bundler よりも高機能なようです。
こいつも brew で elixir インストールすると勝手にインストールされます。

追記: Bundler はパッケージ管理ツールですが、 Mix はプロジェクト管理ツールのようです。 rails コマンドのようにプロジェクト作成などをできます。

Hex

Elixir 界隈ってググりにくいワード多すg(ry
Elixir, Erlang のパッケージ管理ツールらしいです。

Phoenix

Elixir の Web フレームワークです。

Plug

Web アプリケーションを構成するための仕様と、 Erlang VM 上で動作する Web サーバーとのアダプターらしいです。 Ruby では Rack が RailsWebrick 等の Webサーバーとの間を取り持ってくれますが、同じような役割を果たしてくれると認識しています。

Cowboy

Erlang VM 上で動作する Web サーバーです。

Ecto

Elixir の ORM です。軽量 ORM らしいけど、どこらへんが軽量なのか使ってみてから書きます。

Poison

所感

新しいこと勉強してると積本とブラウザのタブが大量増殖しますよね。
とりあえず、 すごい E 本読まないと感ヤバい。(図書館で予約した。)

Elixir チュートリアル (1) 導入編

Rails 5.1 の記事を書いていましたが、中身がないし旬もすぎてるクソ記事だったので Elixir に方向転換。

まずは導入から。

Elixir を導入

rbenv 風の elixir-buildexenv を使おうと思ったのですが、3年ほどメンテナンスされておらず地雷感満載だったので普通にドキュメントどおりにやりました。 複数バージョン試す必要があったら Docker を使おうと思います。

$ brew update
$ brew install elixir

これで最新版の Elixir がインストールされるはずです(執筆時 v1.5.1)
Releases · elixir-lang/elixir · GitHub

$ elixir --version
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.5.1

Mix をインストール

Mix については後日説明しますが、 Rubyrubygems や Nodejs の npm のようなものだと認識していただけると。

$ mix local.hex

使ってみる

Elixir は iex コマンドで対話式に使ってみることができます。

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

Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IO.puts 'Hello World'
Hello World
:ok

所感

安定の中身のないクソ記事でしたが、 Phoenix を使って GraphQL な API 作るところまで書けたらいいなと思ってます。

Rails 5.1 で始める SPA (2) rails g

前回
Rails 5.1 で始める SPA (1) rails new, bundle install - giraphme tech blog


タイトルの通り今回はひたすら rails g するだけの簡単なお仕事です。
といってもこのあたりは他に優秀な記事やドキュメントがたくさんあるので Rails tutorial でも見といてください。

無理やり注意をひねり出すとしたら、以下の点ぐらいでしょうか。

  • Rails 5 系から DB のマイグレーション実行のコマンドが bundle exec rails db:* になった。 (これまで通り bundle exec rake db:* も使用できますが、今後 rails コマンドで統一されていくようです。)
  • Rails 5 系からマイグレーションファイルに comment を記載できるようになっているのでちゃんと書く
  • 後から utf8mb4 対応するのは地味面倒なので、 string のカラムに limit: 180 を付けておく(MySQL 側の設定で回避する場合は不要)
  • その他 Rails4 までの知識で DB 設計せず、新しい機能を導入できないか検討すること(Rails5から使えるActiveRecord便利機能 - Qiita
  • この時点でモデルのロジックをサービス層に逃がせる仕組みを準備しておくとなお良い(これについては別で記事にできたらいいなぁ……。)

個人的な意見ですが、 rails g scaffold ではゴミファイルやコメントアウトが大量に作られてしまってつらいので、 rails g コマンドではモデルのみ作成して、コントローラーや View は Rails の規約に則って手動で作るようにしています。


それでは今日はこのへんで。 なんの中身もないクソ記事だった……。