Docker上でElixirのPhoenixとPostgreSQLを使ってみた

# Docker上でElixirのPhoenixとPostgreSQLを使ってみた

Phoenixの勉強をしたくて、手始めにDBからデータをとってきてテーブルで表示するというのをやってみたのでそれの備忘録兼Docker用いた日本語の記事が少ないような気がしたので、記事を投稿しようと思いました。

思ったより長めになってしまったので忙しい人は、こちらからソースコードを各自cloneしてください。

[**phoenix-postgres**](https://github.com/niya1123/phoenix-postgres)

## 1. Dockerfileとdocker-compose.yml

とりあえず、今回のファイル群の中身です。説明も後にしますが、興味ない人はコピペだけでもいいと思います。

- Dockerfile

  ```dockerfile
  FROM elixir
  
  WORKDIR /workspace
  
  RUN apt-get -y update &&\
      mix local.hex --force && \
      mix archive.install hex phx_new --force
  ```

- docker-compose.yml

  ```yml
  version: '3'
  
  services:
    PostgreSQL:
      image: postgres
      container_name: postgres
      ports:
        - 5432:5432
      environment:
        POSTGRES_USER: root
        POSTGRES_PASSWORD: root
        POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      hostname: postgres
      restart: always
      user: root
  
    elixir:
      build: .
      ports:
        - 4000:4000
      container_name: elixir
      volumes:
        - .:/workspace
      depends_on:
        - PostgreSQL
      command: /bin/bash
      tty: true
      stdin_open: true
  ```

  

まずはDockerfileから解説していきます。

- Dockerfile

  Dockerfileに関してはいたってシンプルです。

  From命令でelixirイメージをpullして使用します。

  WORKDIR命令では、Docker上でカレントディレクトリとして使用したいディレクトリを指定しています。デフォルトで指定したディレクトリが存在しない場合、新規に作成してくれます。

  次に、RUN命令ですがとりあえず今後何かしらのコマンドを使用するかもしれないので` apt-get -y update`だけは実行しています。他に何かしら入れたい場合は、以下のように書けばいいでしょう。

  ```dockerfile
  RUN apt-get -y update && apt-get install -y \
  	curl \
  	zip && \
  	mix local.hex --force && \
      mix archive.install hex phx_new --force
  ```

  `\`は改行を表すもので可読性を高めるために用いられます。`&&`はコマンドの区切りに用いられます。

  さて次に、`mix local.hex --force`と`mix archive.install hex phx_new --force`に関してですが、これはPhoenixをインストールするために記述しており、またオプションの`--force`つけています。Dockerの性質上、インストール時のyes/noを実行中に入力できないためこのオプションを付けて実行しています。

- docker-compose.yml

  docker-composeはDBを用いるため少し複雑です。まずこのdocker-composeは`version: '3'`とあるようにバージョンが**3**です。他の記事と合わせてこの記事をみるとき、片方がバージョン2を用いていた場合挙動がおかしくなったりエラーが発生する可能性があるので注意してください。

  さてservicesでは複数のコンテナを管理することが可能です。今回の場合ではelixirのコンテナとpostgresのコンテナを管理しています。ではそれぞれのコンテナについてみていきます。

  - PostgreSQL

    PostgreSQLというのはサービス名です。コンテナの名前ではないことに注意してください。コンテナの名前は`container_name`に示されている通り`postgres`です。

    `image`では使用するイメージを設定します。今回の場合ではpostgresです。

    `container_name`は先ほど説明した通りこのサービスのコンテナ名です。

    `ports`では通信するポートを指定します。文法としては

    ```docke
    ports:
    	- ホスト側（自分のPC本体)のポート番号:コンテナ側のポート番号
    	- 以下例
    	- 4000:4000
    	- 8080:3000
    ```

    となります。

    `environment`ではpostgresの環境設定を行います。今回の場合では、user名とpassword、文字コードの指定をしています。

    `hostname`ではその名の通りホスト名を設定しています。

    `restart`では何らかの問題でこのコンテナが落ちた場合に再接続させるために`always`を設定します。`always`を設定することにより、明示的に `docker stop コンテナ名` とされない限り、終了ステータスに関係なく常に再起動が行われます。

    `user`ではuserを設定します。

  - elixir

    `ports`と`container_name`に関しては先ほど説明したので省きます。

    `build`では`image`とは違いDockerfileを用いてコンテナを作成したいときに用います。

    `build: .`の時はdokcer-composeファイルと同階層にDockerfileを作成する必要がありますが以下のようにすればdocker-composeファイルから見た相対パスでDockerfileを指定できます。

    ```dockerfile
    build:
    	context:./hoge
    		
    ```

    `volumes`の働きはホスト環境のディレクトリとコンテナのディレクトリを紐づけて共有するといったものである。文法は以下のような感じ。

    ```dockerfile
    volumes:
    	- ホストのディレクトリの相対パス若しくは絶対パス（ほとんどの場合相対パス）:コンテナのディレクトリの絶対パス
    	- 以下例
    	- ./hoge:/workspace
    	- ./foo/run.sh:/woorkspace/run.sh
    ```

    ディレクトリを共有するときは共有したいコンテナの場所の親ディレクトリを、

    ファイルを共有したいときは共有したいコンテナの場所に同じ名前のファイルを指定するといった感じで書きます。

    `depends_on`の働きは、サービスの起動順序を決めます。ここではPostgreSQLサービスが立ち上がったのちにelixirサービスが立ち上がります。

    `command`ではコンテナ起動時に、指定されたコマンドを実行します。

    `tty`と`stdin_open`はコンテナを永続化（起動しっぱなし）にするために設定します。



## 2. PhoenixとPostgreSQLを用いた簡単なwebアプリケーション

### 2.1 Phoenixプロジェクトの作成

やっと本題です。サブタイルにはWebアプリケーションと言っていますが、実際はDBに格納されたデータを只表示するだけなので、アプリケーション性は皆無です。

では早速、コマンドプロントを開いて以下のコマンドを実行してください。

```shell
cd /docker-composeの入ったディレクトリ
docker-compose up -d --build
docker attach elixir
```

`docker-compose up -d --build`実行後は色々な処理が発生するので、それが終わるのを待ってから、`docker attach elixir`を実行してください。

この時点でelixirコンテナ内にログインできているのを確認してから以下のコマンドを実行してください。

```shell
mix phx.new postgres_sample --no-webpack
cd postgres_sample
```

途中でyes/noを聞いてくるのですべて**y**を選択してください。

postgres_sampleディレクトリに移動していることを確認したのちに、ディレクトリの階層が以下のようになっていることを確認してください。

```shell
root@c77dbed21542:/workspace/postgres_sample# ls -al
total 18
drwxrwxrwx 2 root root 4096 Aug 21 05:30 .
drwxrwxrwx 2 root root 4096 Aug 21 05:31 ..
-rwxr-xr-x 1 root root  159 Aug 21 05:30 .formatter.exs
-rwxr-xr-x 1 root root  739 Aug 21 05:30 .gitignore
-rwxr-xr-x 1 root root  672 Aug 21 05:30 README.md
drwxrwxrwx 2 root root    0 Aug 21 05:30 config
drwxrwxrwx 2 root root    0 Aug 21 05:30 lib
-rwxr-xr-x 1 root root 1671 Aug 21 05:30 mix.exs
drwxrwxrwx 2 root root    0 Aug 21 05:30 priv
drwxrwxrwx 2 root root    0 Aug 21 05:30 test
```

確認出来たら、config/dev.exsをエディタで開いてください。

```elixir
config :postgres_sample, PostgresSample.Repo,
  username: "postgres",
  password: "postgres",
  database: "postgres_sample_dev",
  hostname: "localhost",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```

初期ではこのようになっているのでこれを以下のように変えます。

```elixir
config :postgres_sample, PostgresSample.Repo,
  username: "root",
  password: "root",
  database: "postgres_sample_dev",
  hostname: "postgres",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```

変更出来たら、コマンドプロントに戻って、以下を実行してください。

```shell
mix ecto.create
iex -S mix phx.server
```

もし、`mix ecto.create`実行時にエラーが出た場合は、`mix deps.get`コマンドを実行してください。

さて、`iex -S mix phx.server`を実行したら、[localhost:4000](http://localhost:4000)に接続してください。

すると、以下のような画面が表示されると思います。

![1566369635326.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274354/fa91d009-b0a2-f5a5-640e-db13a7236167.png)

これが表示されていればとりあえず、Phoenixが最低限動いていることが確認できます。

### 2.2 PostgreSQLのDBの構築

次は、PostgreSQLでDBを構築します。

まずはコマンドプロントで今、elixirが動いているので新しいウィンドウでコマンドプロントを立ち上げるか、`ctrl+p,q`(ctrlを押しながらp,qの順番に押す）を実行すればコンテナを止めることなくホストの環境に戻ることができます。

ではホストの環境で、`docker exec -it postgres psql -U root`と実行してください。

すると以下のように表示されるかと思います。

```shell
psql (11.5 (Debian 11.5-1.pgdg90+1))
Type "help" for help.

root=#
```

まずは、今現在どのようなデータベースが存在するかを確認するために、`\l`と入力してください。すると以下のように表示されるはずです。

```shell
                                  List of databases
        Name         | Owner | Encoding |  Collate   |   Ctype    | Access privileges
---------------------+-------+----------+------------+------------+-------------------
 postgres            | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres_sample_dev | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 root                | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 template0           | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
                     |       |          |            |            | root=CTc/root
 template1           | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
                     |       |          |            |            | root=CTc/root
(5 rows)
```

次に早速データベースを構築していきます。まずは以下を実行してください。

```shell
create database elixir;
```

`\l`コマンドでしっかりと作成されていることを確認します。

```shell
                                  List of databases
        Name         | Owner | Encoding |  Collate   |   Ctype    | Access privileges
---------------------+-------+----------+------------+------------+-------------------
 elixir              | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres            | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres_sample_dev | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 root                | root  | UTF8     | en_US.utf8 | en_US.utf8 |
 template0           | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
                     |       |          |            |            | root=CTc/root
 template1           | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
                     |       |          |            |            | root=CTc/root
(6 rows)

```

一番上にelixirとあるのが確認できます。

次に`\c elixir`で、先ほど作ったelixirデータベースに接続します。

接続すると先ほどまで、`root=#`となっていたところが、`elixir=#`となっていることが確認できます。

確認出来たら、テーブルの作成に移ります。今回は主キーなどの設定は行わず、ごくごく簡単なテーブルを作成します。

以下のコマンドでテーブルを作りましょう。

```sql
create table fruits_datas(id integer, name varchar(255), value integer, region varchar(255));
```

果物を管理するテーブルを今回は作成しました。それぞれの`integer`や`varchar`などの意味は各自で確認してください。

では、テーブルが正しく作成されているか`\z`と`\d fruits_datas`で確認します。前者はテーブル自体の存在確認で、後者はテーブルの定義を確認できます。
結果として、以下のようになると思います。

```shell
elixir=# \z
                                Access privileges
 Schema |     Name     | Type  | Access privileges | Column privileges | Policies
--------+--------------+-------+-------------------+-------------------+----------
 public | fruits_datas | table |                   |                   |
(1 row)

elixir=# \d fruits_datas
                   Table "public.fruits_datas"
 Column |          Type          | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
 id     | integer                |           |          |
 name   | character varying(255) |           |          |
 value  | integer                |           |          |
 region | character varying(255) |           |          |
```

こうなっていれば正しくテーブルは作成されています。

では、実際にこのテーブルに対してデータを与えていきます。以下のコマンドを実行して下さい。

```shell
insert into fruits_datas values(1, 'みかん', 80, '日本'), (2, 'りんご', 120, 'アメリカ'), (3, 'バナナ', 150, 'インド'), (2, 'りんご', 90, '中国'), (3, 'バナナ', 120, 'ブラジル'); 
```

これが正しく、実行されていれば以下のようになるはずです。

```shell
elixir=# select * from fruits_datas;
 id |  name  | value |  region
----+--------+-------+----------
  1 | みかん |    80 | 日本
  2 | りんご |   120 | アメリカ
  3 | バナナ |   150 | インド
  2 | りんご |    90 | 中国
  3 | バナナ |   120 | ブラジル
(5 rows)
```

このようになっていれば今回のdbでの作業は終わりなので`\q`を実行するか`ctrl+p,q`してください。

### 2.3 PhoenixとDBの紐づけ

次は、dbとPhoenixを紐づけていきます。とりあえず再度`docker attach elixir`でコンテナにログインし、`postgres_sample`ディレクトリに移動しておいてください。

と言っても、先ほど`ctrl+p,q`で終了した人は、attachしてもなかなかコマンドプロントが表示されないと思いますが、Enterキーなどを打ってもらうと

```shell
nil
iex(2)>
```

のように表示されると思うので、`ctrl+c`を2回押してもらうと、作業可能状態になります。

では、postgres_sampleディレクトリ下に/lib/util/db.exを作成しましょう。

そして、db.exを以下のように編集します。

PostgresSample.Repoとなっているところはプロジェクト作成時の`mix phx.new postgres_sample --no-webpack`のpostgres_sampleの部分に合わせてください。わからなければ、postgres_sample\config\config.exsやpostgres_sample\config\dev.exsにそれっぽいのが書いているのでそれをコピペしましょう。

```elixir
defmodule Db do
  def query( sql ) when sql != "" do
    { :ok, result } = Ecto.Adapters.SQL.query( PostgresSample.Repo, sql, [] )
    result
  end
  def columns_rows( result ) do
    result
    |> rows
    |> Enum.map( fn row -> Enum.into( List.zip( [ columns( result ), row ] ), %{} ) end )
  end
  def rows( %{ rows: rows } = _result ), do: rows
  def columns( %{ columns: columns } = _result ), do: columns
end
```

次に、postgres_sample以下にlib\postgres_sample_web\templates\page\index.html.eexを作成して以下のように編集します。

```html
<%
result = Db.query( "select * from fruits_datas" )
data = result |> Db.columns_rows
%>
<table border="1">
<tr>
  <th>ID</th>
  <th>名前</th>
  <th>価格</th>
  <th>生産国</th>
</tr>
<%= for record <- data do %>
<tr>
    <td><%= record[ "id" ] %></td>
    <td><%= record[ "name" ] %></td>
    <td><%= record[ "value" ] %></td>
    <td><%= record[ "region" ] %></td>
</tr>
<% end %>
</table>
```
次に、config/dev.exsのdatabaseの部分を`postgres_sample_dev`から`elixir`に変更します。

```elixir
config :postgres_sample, PostgresSample.Repo,
  username: "root",
  password: "root",
  database: "elixir",
  hostname: "postgres",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
```
では、動くかどうか確認しましょう。

`iex -S mix phx.server`を実行してください。

実行したら[localhost:4000](http://localhost:4000)に接続してください。

すると成功していれば以下のような画面が表示されるはずです。

![1566375102305.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274354/b5fa86f8-f6e1-525d-0ccf-7afe1cfa78a9.png)

さきほどDBで作成したデータがきちんと表示されていますね。

以上で基本的なことは終了です。お疲れさまでした。

## 終わりに

これから、自分もこの知識を生かし次のステップに進もうと思います。またある程度知識がつけば記事にしたいと思っています。長々とありがとうございました。

## 参考文献

[Excelから関数型言語マスター3回目：WebにDBデータ表示【PostgreSQL or MySQL編】](https://qiita.com/piacerex/items/a7558adc6856e3577dc6)