前陣子開發團隊為了要方便做 data migration,參考了Migrating Production Data in Elixir,主要是講如何實作一個 migration 的機制,最終用起來的手感就跟 db migration 一樣方便,但每次合併 data_migration PR 後,都要在 slack 上提醒其他人本機端(local)要記得執行 data_migration,難免覺得稍嫌麻煩,覺得是不是能像是 db_migration 一樣,在執行的時候,在 server 的 log 裡面顯示訊息。
像是這樣:
1
2
| Phoenix.Ecto.PendingMigrationError at GET /
there are pending migrations for repo: Falcon.Repo. Try running `mix ecto.migrate` in the command line to migrate it
|
後來透過 PendingMigrationError
找到 Phoenix 是如何實作這個過程,原來是透過 phoenix_ecto 來實現確認功能。
在 Phoenix 專案裡面的 endpoint.ex
會看到這個檔案,裡面有一段 code 是這樣
1
2
3
4
5
6
7
8
| # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :my_app
end
|
其中 Phoenix.Ecto.EheckRepoStatus
就是檢查 db_migrate 的狀態,在這個 plug 你會看的 phoenix 是如何確認 db migration 的狀態
1
2
3
4
5
6
7
8
9
10
11
12
| def call(%Conn{} = conn, opts) do
repos = Application.get_env(opts[:otp_app], :ecto_repos, [])
for repo <- repos, Process.whereis(repo) do
# 檢查 pending 的 migration
unless check_pending_migrations!(repo, opts) do
check_storage_up!(repo)
end
end
conn
end
|
知道 Phoenix 怎麼做的以後,我們就可以按照這個邏輯這樣做
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| defmodule MyApp.CheckDataStatus do
@behaviour Plug
alias Plug.Conn
alias MyApp.DataMigrator
require Logger
def init(opts) do
opts
end
def call(%Conn{} = conn, _opts) do
DataMigrator.check_pending_migrations()
conn
end
end
|
然後在我們自己實作的 data_migrator 就會長這樣子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| defmodule MyApp.DataMigrator do
def check_pending_migrations() do
list_pending_migrations = get_pending_migrations()
if Application.get_env(:my_app, :env) == "local" && list_pending_migrations != [] do
Logger.warning(
"there are pending data migrations. Please run data migrate by: `mix Myapp.data_migrate`."
)
end
end
defp get_pending_migrations() do
already_migrated = get_already_migrated()
Application.app_dir(:falcon, "priv/data_migrations/*")
|> Path.wildcard()
|> Enum.map(&get_migration_info/1)
|> Enum.reject(fn
# reject migrations that have already ran
{version, _} -> Enum.member?(already_migrated, version)
_ -> true
end)
end
defp get_already_migrated() do
from(dm in DataMigration, select: dm.version)
|> Repo.all(data_migration: true)
end
defp get_migration_info(file) do
file
|> Path.basename()
|> Path.rootname()
|> Integer.parse()
|> case do
{integer, _} ->
{integer, file}
_ ->
nil
end
end
end
|
接下來只要在 local 如果沒有執行到最新的 migration,就會出現提示訊息