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:
Task
code 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
Task
you 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_type
column 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.ex
file 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.Task
macro, which adds task capability to this file. - To list the task within our application we can run
mix help
in 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
@shortdoc
module 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 compile
andmix help
again 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
run
function. - We are going to write our existing data changing logic inside the
run
function which takes one parameterargs
. We are going to ignore it because we don't need one. We will alias
User
,Repo
and importEcto.Query
to 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_name
and 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
Repo
toMix.EctoSQL.ensure_started
function and also start the Task inside therun
function.Mix.Task.run("app.start", []) Mix.EctoSQL.ensure_started(MyProject.Repo, [])
- Now, on running the task with the command
mix add_user_typ
it 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 end
What about Test?
At the start of the blog while explaining the benefits of using
task
for 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.ex
file and you can write the test for your task. We can cover about writing test formix task
in some other post.I hope you like this post. If you have any questions then please comment below. Thanks for reading 😊.