Multi-step Initialization in Elixir Applications
When starting an Elixir application, it can sometimes be necessary to break up initialization into multiple steps. For instance, consider an issue tracker application where users
can submit an issue
that contains one or more tags
. This may feel similar to the classic posts
and tags
example used when defining many-to-many relationships using Ecto, but we want our users to have a default set of tags
available by default.
To do this, let’s first build out the required schemas:
Here we have two schemas that follow pretty simple conventions about many_to_many relationships. We can add issues and associate them with tags. This is exactly what we want!
To provide some default tag values, we’ll add something to the application configuration. We’ll provide these as a list of strings, where each string corresponds to a tag.name
.
With the default names described in the application configuration, we can make sure they exist in the database when the application starts. Since this is application runtime behavior, it makes sense to put it in the application.ex
file for the project.
However, we have a problem: The application starts IssueTracker.Repo
as part of the supervision tree, which means we can’t actually interface with the database until after Application.start
returns (because the IssueTracker.Repo
process hasn’t started until the supervisor can initialize).
Thankfully, the OTP application behavior provides a way to perform initialization steps in multiple phases using the Application.start_phase/3
callback. From the documentation:
This function is called after
start/2
finishes but beforeApplication.start/2
returns. It will be called once for every start phase defined in the application's (and any included applications') specification, in the order they are listed in.
The application phases are defined using the start_phases
keyword in the application
function in your mix.exs
file. The value is another keyword list containing <phase_name> : <phase_args>
pairs. More information can be found here.
Now that we’ve defined a separate phase where our default values can be loaded, we can implement the actual phase in the IssueTracker.Application
module. This is done by implementing the start_phase/3
callback, with the arguments (phase_name, phase_type, phase_args)
.
Now, whenever the application starts, it will attempt to write the initial set of tags into the database. The Ecto.Changeset.unique_constraint/2
call will ensure that duplicates are never written.