Phoenix/Ectoでデータベースのmaster/slave構成に対応する方法

マスター1台、スレーブ複数台という構成は、DBサーバーの負荷分散方式として一般的だと思いますが、Phoenix/Ectoでこの構成に対応する方法を調査/検討してみました。

# 調査

[こちら](https://github.com/elixir-lang/ecto/issues/488)のissue内では、Elixirの作者であるJosé Valimさんが下記のようにコメントしています。

> You just define multiple repositories: MyApp.Repo, MyApp.Repo.ReadSlave1, MyApp.Repo.ReadSlave2 up to 4 (or any power of 2). Their configuration is going to be similar and you can easily share them in the config/config.exs file.

> Now, every time you need to do a read, you just need to do:

> ```ex
def repo do
  # size needs to be power of 2 that you care about
  <<at::integer-size(2), _::bits>> = :crypto.rand_bytes(1)
  Enum.fetch! [MyApp.Repo.ReadSlave1, ..., ], at
end
```

> Alternatively you can create a facade repository that does exactly that:

> ```ex
defmodule Sample.Repo do
  def all(query, options \\ []) do
    read_repos.all(query, options) # Read goes through the random above
  end
  def update(changes, options \\ []) do
    Sample.Repo.Master.update(changes, options) # Those go to master
  end
end
```

また[こちら](https://edmz.org/personal/2016/03/09/multiple_databases_with_ecto_and_phoenix.html)の記事では、新たにRepoを作成する場合はconfigファイルとsupervisionツリーにそれぞれ設定を追加する必要があるということが説明されています。

> I added the credentials to dev.exs for the other database:

> ```ex
# notice the module name
config :dual_ecto, DualEcto.RepoReadOnly,
  adapter: Ecto.Adapters.MySQL,
  username: "readonly_user",
  password: "foo",
  database: "data",
  hostname: "slave_server",
  pool_size: 10
```

> First, add the module to the supervision tree of your main module, which in my case is called DualEcto:

> ```ex
children = [
  # Start the endpoint when the application starts
  supervisor(DualEcto.Endpoint, []),
  # Start the Ecto repository
  supervisor(DualEcto.Repo, []),
  supervisor(DualEcto.RepoReadOnly, []), # <- new line!
  # Here you could define other workers and supervisors as children
  # worker(DualEcto.Worker, [arg1, arg2, arg3]),
]
```

どうやらRepoを追加すること自体は比較的簡単なようです。

しかし、Joséさんの紹介している手法だと、`all`や`update`等の、`Ecto.Repo`で定義されている関数を全て再定義する必要がありますし、edmzで紹介されている方法だと、既存のコードを大幅に書き換える必要が発生してしまいます。

# 対策

ということで、**追加するコードの量と既存コードの改修をできるだけ最小限にする**、ということを目標にして、下記のようなパッケージを作成してみました。

[read_repos](https://github.com/kenta-aktsk/read_repos)

# 使用方法

configファイル(dev.exs等)に設定を追加します。

```ex
# config/dev.exs
config :my_app, MyApp.ReadRepo0,
  adapter: Ecto.Adapters.MySQL,
  database: "my_app",
  hostname: "192.168.0.2",
  ...

config :my_app, MyApp.ReadRepo1,
  adapter: Ecto.Adapters.MySQL,
  database: "my_app",
  hostname: "192.168.0.3",
  ...
```

アプリケーションのsupervisionツリーに、動的に生成されるslave用のRepoを挿入するコードを追加します。

```ex
# lib/my_app.ex
defmodule MyApp do
  use Application

  def start(_type, _args) do
    children = [
      ...
    ]
    # add
    children = children ++ Enum.map(MyApp.Repo.slaves, &supervisor(&1, []))
    ...
  end
end
```

マスター用のRepoモジュールに、上記の`ReadRepos`を`use`するコードを追加します。

```ex
# lib/my_app/repo.ex
defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app
  # add
  use ReadRepos
end
```

この設定だけで、下記のようにスレーブ用Repoを使用してDBにアクセスすることが出来ます。configファイルにスレーブDBの設定が複数記述されている場合、アクセス先のDBは毎回ランダムに選択されます。

```ex
MyApp.Entry |> MyApp.Repo.slave.all
MyApp.Entry |> MyApp.Repo.slave.get(1)
```

マスターDB用のRepoは通常通り`MyApp.Repo.insert`のようなコードで関数が実行出来ますので、スレーブ用DBを使用したい箇所だけ`MyApp.Repo.slave`を使うようにコードを書き換えるだけでオッケーです。

スレーブDB用の設定がconfigファイルに存在しない場合、`MyApp.Repo.slave`は自動的にマスター用Repoを返しますので、スレーブ用DBが存在する環境でも存在しない環境でも、同じコードが使用可能です。