ElixirのDB設定をURLで指定（Gigalixirでの指定も）

[fukuoka.ex](https://fukuokaex.connpass.com/)代表のpiacereです
ご覧いただいて、ありがとうございます :bow:

ElixirのDBラッパーである「Ecto.Repo」は、Rails同様、DB設定をURL形式で記述できます

これを応用することで、環境変数でもDB設定が行え、本番環境での設定はもちろん、テスト環境やステージング環境でのDB設定切替が容易になります（ただし、configで設定するため、アプリ再起動が必要です）

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

# configの「url」でDB設定を書く

通常の設定（Phoenixで自動生成された場合も含む）は、以下のように記述されます

（なお、「adapter」が未記載ですが、Phoenix 1.4からは、configからRepoモジュール内に移動しています）

```elixir:config/dev.exs
…
# Configure your database
config :sample_db, SampleDb.Repo,
	username: "postgres",
	password: "password",
	database: "sample_db_dev",
	hostname: "localhost",
	port: 5432,
	timeout: 1000,
	pool_size: 10
```

上記と同じ設定を「url」で書くと、以下のようになります

```elixir:config/dev.exs
…
# Configure your database
config :sample_db, SampleDb.Repo,
	url: "ecto://postgres:password@localhost:5432/sample_db_dev?timeout=1000&pool_size=10"
```

書式は、以下の通りです

```
ecto://【ID】:【パスワード】@【ホスト名】:【ポート番号】/【DB名】?【オプション1】&【オプション2】&…
```

SSL設定等のオプションも、上記同様、URLパラメータとして記述できます

# 環境変数でのDB設定

URL形式での指定を応用して、環境変数にDB設定のURLを書いておき、configで読み込めます

これを使うと、本番環境のDBパスワード等を秘匿するのに便利です

prod.secret.exsは、レポジトリにデフォルトで登録されず不便なため、環境変数の方が使いやすいと思います（[Gigalixirでもprod.secret.exsは削除が推奨されています](https://gigalixir.readthedocs.io/en/latest/main.html#configuration-and-secrets)）

また、DB設定がコード書換無で可能となる（ただし、アプリ再起動は必要）ので、テスト環境やステージング環境でのDB設定切替の運用を安全に行えます

```:windows
set DATABASE_URL=ecto://postgres:password@localhost:5432/sample_db_dev?timeout=1000&pool_size=10
```

```:windows以外
export DATABASE_URL=ecto://postgres:password@localhost:5432/sample_db_dev?timeout=1000&pool_size=10
```

configでは、System.get_env()を使って、環境変数を読み込みます

```elixir:config/dev.exs
…
# Configure your database
config :sample_db, SampleDb.Repo,
	url: System.get_env( "DATABASE_URL" ), 
	…
```

# Gigalixirでの設定の書き方

Gigalixirでは、以下のように設定を書きます

```elixir:config/prod.exs
…
# Configure your database
config :sample_db, SampleDb.Repo,
	url: "${DATABASE_URL}",
	…
```

${DATABASE_URL}に、どんな設定がされているかは、以下コマンドで確認できます

```
gigalixir config
{
  "DATABASE_URL": "ecto://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX-user:pw-XXXXXXXXXXXXXXXX@postgres-XXXXXXXX.gigalixir.com:5432/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
```

設定を変更する場合は、以下コマンドで行えます

```
gigalixir config:set DATABASE_URL="ecto://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX-user:pw-XXXXXXXXXXXXXXXX@postgres-XXXXXXXX.gigalixir.com:5432/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
```

# オマケ：DB設定のパース

[Ecto.Repo.Supervisor.parse_url()](https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/repo/supervisor.ex#L95)でURLがパースされています

```elixir:/ecto/lib/ecto/repo/supervisor.ex
defmodule Ecto.Repo.Supervisor do
  …
  @doc """
  Parses an Ecto URL allowed in configuration.

  The format must be:

      "ecto://username:password@hostname:port/database?ssl=true&timeout=1000"

  """
  def parse_url(""), do: []

  def parse_url(url) when is_binary(url) do
    info = URI.parse(url)

    if is_nil(info.host) do
      raise Ecto.InvalidURLError, url: url, message: "host is not present"
    end

    if is_nil(info.path) or not (info.path =~ ~r"^/([^/])+$") do
      raise Ecto.InvalidURLError, url: url, message: "path should be a database name"
    end

    destructure [username, password], info.userinfo && String.split(info.userinfo, ":")
    "/" <> database = info.path

    url_opts = [username: username,
                password: password,
                database: database,
                hostname: info.host,
                port:     info.port]

    query_opts = parse_uri_query(info)

    for {k, v} <- url_opts ++ query_opts,
        not is_nil(v),
        do: {k, if(is_binary(v), do: URI.decode(v), else: v)}
  end

  defp parse_uri_query(%URI{query: nil}),
    do: []
  defp parse_uri_query(%URI{query: query} = url) do
    query
    |> URI.query_decoder()
    |> Enum.reduce([], fn
      {"ssl", "true"}, acc ->
        [{:ssl, true}] ++ acc

      {"ssl", "false"}, acc ->
        [{:ssl, false}] ++ acc

      {key, value}, acc when key in @integer_url_query_params ->
        [{String.to_atom(key), parse_integer!(key, value, url)}] ++ acc

      {key, value}, acc ->
        [{String.to_atom(key), value}] ++ acc
    end)
  end

  defp parse_integer!(key, value, url) do
    case Integer.parse(value) do
      {int, ""} ->
        int

      _ ->
        raise Ecto.InvalidURLError,
              url: url,
              message: "can not parse value `#{value}` for parameter `#{key}` as an integer"
    end
  end
…
```

# p.s.「いいね」よろしくお願いします

ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada: