A while ago, the development team was working on facilitation data migration. We have referred to Migrating Production Data in Elixir, which primarily discusses how to implement a data migration mechanism. The final user experience is as convenient as a db migration. After each data_migration PR is merged. A reminder should be posted on Slack to prompt others to run the data_migration locallly. It is somewhat cumbersome, and I wonder if it could be possible display a message in the server log when running, similar to db_migration.
Something like this:
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
|
Later, by utilizing PendingMigrationError
. I discovered how Phoenix implements this process. It turns out that the confirmation functionality is achieved through phoenix_ecto.
In the Phoenix project, you will find the endpoint.ex
file where you will come across a code snippet like this:
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
|
The Phoenix.Ecto.CheckRepoStatus
is responsible for checking the status of db_migrate. In this plug, you will see how Phoenix confirms the status of 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
|
Now that we know how Phoenix dose it. We can implement it following the same logic.
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
|
Then, in our self-implemented data_migrator. It will look something like this:
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
|
Next, if the local environment hasn’t executed the latest migration, a prompt message will appear.
Cool!