Writing Mix Task in Elixir Phoenix

Imagine you are working on an application that has multiple models (Tables) and each table has some columns. Your manager came up with a requirement to add a new column in one table, say User table. The new column you have asked to add is user_type of string type. You will write the migration to add the new column and as per the new column, you added this new field in form as well. Everything works fine, but what about the records that already exist in the database on Production or another environment. That might can give unexpected results. The solution to this problem is task in Phoenix. It is somewhat inspired by the rails rake task. But the question arises we can directly run SQL queries on the database as soon as we run the migration before deploying other changes that are going to change new data. Why there was a need to come with a new concept like task to change the data.
There are few benefits of using the Elixir task:
Taskcode is nothing but pure Elixir code with which you can use to write Ecto Queries (which is more readable and expressive) to do changes on a database.- Since, it is Elixir code it is Testable. Imagine, you are writing some task to do changes to 1 million data. You need to be an SQL ninja and have a strong heart to directly run SQL queries directly on data. With
Taskyou can write a Test case and be 100% sure that your query is going to work on the production. - Also, if you have multiple environments you have to run the script one by one on all environments. With task, you will have just one file which you have to call with just one command.
Let's write one!!
- Continuing with the
user_typecolumn example I explained in the introduction. We will add that column to the User struct. - You will add migration something like below
def change do alter table(:user) do add :user_type, :string end end - As per the new field you will add a field in the user form.
- Everything looks fine, now you are able to store value in this form but what about the old data?
- As discussed we will be writing
task. - For writing a task add a file at
lib/mix/tasks/add_user_type.ex. - In the
add_user_type.exfile we will add the following code.
defmodule Mix.Tasks.AddUserType do
use Mix.Task
def run(_args) do
# Write your task logic here
end
end
- To create our task, we’ll need to start the module name with
Mix.Tasks. So, we will name our moduleMix.Tasks.AddUserType. - We have added
use Mix.Taskmacro, which adds task capability to this file. - To list the task within our application we can run
mix helpin the terminal of the project. You will get the list of the tasks but you'll not find the task that we created. It is because in order to list our task with a description we need to add the
@shortdocmodule attribute. We will add one.defmodule Mix.Tasks.AddUserType do use Mix.Task @shortdoc "Add user_type value to existing user data" def run(_args) do # Write your task logic here end end- Now on running
mix compileandmix helpagain you can see the task we created (see the second line in the image below).

- So, far so good. Now to write the logic to change our data. It should be written in the
runfunction. - We are going to write our existing data changing logic inside the
runfunction which takes one parameterargs. We are going to ignore it because we don't need one. We will alias
User,Repoand importEcto.Queryto write some SQL.defmodule Mix.Tasks.AddUserType do use Mix.Task @shortdoc "Add user_type value to existing user data" alias MyProject.{ Repo, User } import Ecto.Query def run(_args) do # Logic to change existing data. from(u in User, where: is_nil(u.user_type)) |> Repo.update_all(set: [some_condition]) end end- We will implement some logic to transform our old existing user data. Now, we will run the task.
- To run any task we the command
mix task_nameand in our case it will bemix add_user_type. - On running the task we will get this error
(RuntimeError) could not lookup MyProject.Repo because it was not started or it does not exist. - It is because our Repo isn’t started when we run our task, so we’ll need to do that manually.
- To run the task we need to pass
RepotoMix.EctoSQL.ensure_startedfunction and also start the Task inside therunfunction.Mix.Task.run("app.start", []) Mix.EctoSQL.ensure_started(MyProject.Repo, []) - Now, on running the task with the command
mix add_user_typit will run the task without any issue. Refer below complete running code
defmodule Mix.Tasks.AddUserType do use Mix.Task @shortdoc "Add user_type value to existing user data" alias MyProject.{ Repo, User } import Ecto.Query def run(_args) do Mix.Task.run("app.start", []) Mix.EctoSQL.ensure_started(MyProject.Repo, []) from(u in User, where: is_nil(u.user_type)) |> Repo.update_all(set: [some_condition]) end endWhat about Test?
At the start of the blog while explaining the benefits of using
taskfor writing the query. I emphasized writing the test to make sure our query is working fine. For writing the test you can add a test file in the foldertest/mix/task/add_user_type_test.exfile and you can write the test for your task. We can cover about writing test formix taskin some other post.I hope you like this post. If you have any questions then please comment below. Thanks for reading 😊.




