Building an app with elixir-2
previous post , next post, source code
Disclaimer: I’m a complete noob in elixir, so this is not an ideal way of doing things in elixir. I’m just writing my experience on learning elixir.
In the previous article we prepared our project, in this tutorial we will write some function to do basic CRUD operation to our database. First we’ll define an application module callback. What application module callback does is that when the application starts it call start function of that module defined in the mix.exs file. Open mix.exs file in your favorite editor and add module callback like below
def application do
[mod: {LanguageTrends, []},
extra_applications: [:logger]]
end
The first key in keyword list is mod which defines the module callback, and have a tuple as value. First item in the tuple is the name of the module which in our case is LanguageTrends and the second item is a list that you can use as argument inside the start function, in this case we are sending nothing hence an empty list. And the second key is extra_applications which defines a list of any other application that our main application needs and run those before our application starts. Now we need to add the Application module in our LanguageTrends module. Add this to your lib/language_trends.ex file
defmodule LanguageTrends do
use Applicationdef start(_type, _args) doend
We also added start function which’ll be run at start time. Now what we want to do when our application starts is to connect to the rethinkdb server and keep that connection alive. Now in elixir, or more particularly in erlang (I know nothing about erlang), the philosophy in case of error or exception is ‘let it crash’ as opposed to ‘let’s catch the exception’ in other language. In elixir we have supervisor which, as the name suggests, supervise or monitor a process and when it crashes the supervisor recovers it by starting a new one. So we’ll add a supervisor to monitor our database connection. Let’s change our start function
def start(_type, _args) do
import Supervisor.Spec children = [worker(LanguageTrends.Database, [[host: 'rethinkdb', port: 28015, db: "language_trends"]])]
opts = [strategy: :one_for_one, name: LanguageTrends.Supervisor]
Supervisor.start_link(children, opts)
end
First we import Supervisor.Spec which provides us some helpful function to use. We define a children variable which is a list containing only one function worker from Supervisor.spec. This function takes name of a module to supervise with a list of arguments. We passed LanguageTrends.Database module which we’ll create in a while, with a list of arguments containing host(hostname of the rethinkdb server, which is rehinkdb in the docker network), port and db(name of a default database we want to use.) Next we defined a opts variable which is keyword list with options for sending to the supervisor. The first one is strategy which defines the strategy of restarting the process when it crashes, in our case is :one_for_one meaning it will restart only one child process. (Why it’s child process? Supervisor itself is a process and all the process it watches are child process of the supervisor.) and the second item is the name of the supervisor we want to give. Finally we’ll call the start_link function of Supervisor with children and opts, this’ll start the supervisor when our application starts. Only thing remaining is the Database module. Create a file language_trends/database.ex and add below code
defmodule LanguageTrends.Database do
use RethinkDB.Connection
end
We defined a module Database within LanguageTrends namespace (which is inside language_trends folder, all modules under this namespace should be inside language_trends folder) and we use the RethinkDB.Connection which will create a default connection.
Now we’ll write our functions for crud operation. First a create_database function to create a database and we’ll also import the RethinkDB.Query module from rethinkdb-elixir library to run our queries.
defmodule LanguageTrends do
use Application
import RethinkDB.Query...
...
def create_database(name) do
db_create(name) |> LanguageTrends.Database.run()
end
we defined a function create_database with one argument name. Then in the body we are calling db_create function by passing the name argument and piping it to LanguageTrends.Database.run as an argument. The pipeline(|>) operator is used to connect a series of function in reverse order. So the output of db_create function will be used as first argument of LanguageTrends.Database.run function.
Traditional way
D(C(B(A())))Elixir way
A() |> B() |> C() |> D()
Let’s try to run this function. Enter the iex shell
$ iex -S mix
iex(1)> LanguageTrends.create_database("language_trends")
{:ok,
%RethinkDB.Record{data: %{"config_changes" => [%{"new_val" => %{"id" => "d0e2306c-b475-4e97-b9e7-5c7bd9ee0974",
"name" => "language_trends"}, "old_val" => nil}], "dbs_created" => 1},
profile: nil}}
We ran LanguageTrends.create_database function with “language_trends” as name which returned an tuple containing the id and name of the database.
So we have successfully ran our first function and database query with or application. Let’s add other function for CRUD operation.
def create_table(name) do
table_create(name) |> LanguageTrends.Database.run()
end def drop_table(name) do
table_drop(name) |> LanguageTrends.Database.run()
end def get_document(name) do
table(name) |> LanguageTrends.Database.run()
end def insert_document(name, data) do
table(name) |> insert(data) |> LanguageTrends.Database.run()
end def update_document(name, id, data) do
table(name) |> get(id) |> update(data) |> LanguageTrends.Database.run()
end def delete_document(name, filter) do
table(name) |> filter(filter) |> delete() |> LanguageTrends.Database.run()
end def delete_all(name) do
table(name) |> delete() |> LanguageTrends.Database.run()
end def filter_document(name, filter) do
table(name) |> filter(filter) |> LanguageTrends.Database.run()
end
All of the functions are self explanatory. Documentation of all the functions used inside our own module functions can be found here. Now quit the iex by pressing ctrl + c twice and then enter again to load all our changes
$ iex -S mix
iex(1)> LanguageTrends.create_table("tweets")
{:ok,
%RethinkDB.Record{data: %{"config_changes" => [%{"new_val" => %{"db" => "language_trends",
"durability" => "hard", "id" => "f54fe2bf-7940-4671-bc7e-8301e415ad48",
"indexes" => [], "name" => "tweets", "primary_key" => "id",
"shards" => [%{"nonvoting_replicas" => [],
"primary_replica" => "elixir_elixir_local_v5q",
"replicas" => ["elixir_elixir_local_v5q"]}],
"write_acks" => "majority"}, "old_val" => nil}],
"tables_created" => 1}, profile: nil}}
We created a tweets table in our language_trends database. Now let’s insert data into table
iex(2)> LanguageTrends.insert_document("tweets", %{user: "nhasnayeen", tweet: "My first tweet"})
{:ok,
%RethinkDB.Record{data: %{"deleted" => 0, "errors" => 0,
"generated_keys" => ["851f49d2-0caf-4048-977c-1d8d1b51192d"],
"inserted" => 1, "replaced" => 0, "skipped" => 0, "unchanged" => 0},
profile: nil}}
We passed a map with two keys, user and tweet with values. Map is a key-value collection. In the result we see generated_keys which is the id of our inserted data (id is the primary key). Let’s get all the data from tweets table
iex(3)> LanguageTrends.get_document("tweets")
{:ok,
%RethinkDB.Collection{data: [%{"id" => "851f49d2-0caf-4048-977c-1d8d1b51192d",
"tweet" => "My first tweet", "user" => "nhasnayeen"}], profile: nil}}
We can see we get an RethinkDB.Collection containing all our documents(documents are equivalent to RDBMS rows). Let’s update our tweet
iex(4)> LanguageTrends.update_document("tweets", "851f49d2-0caf-4048-977c-1d8d1b51192d", %{tweet: "My updated tweet", likes
: 15})
{:ok,
%RethinkDB.Record{data: %{"deleted" => 0, "errors" => 0, "inserted" => 0,
"replaced" => 1, "skipped" => 0, "unchanged" => 0}, profile: nil}}
We passed the id and a map with our update tweet. Notice that there is also a key likes which wasn’t our original document. If we retrieve the document again
iex(5)> LanguageTrends.get_document("tweets")
{:ok,
%RethinkDB.Collection{data: [%{"id" => "851f49d2-0caf-4048-977c-1d8d1b51192d",
"likes" => 15, "tweet" => "My updated tweet",
"user" => "nhasnayeen"}], profile: nil}}
you’ll see value of key tweet has been updated and also there is a new key likes with a value. Now lets delete the document
iex(10)> LanguageTrends.delete_document("tweets", %{user: "nhasnayeen"})
{:ok,
%RethinkDB.Record{data: %{"deleted" => 1, "errors" => 0, "inserted" => 0,
"replaced" => 0, "skipped" => 0, "unchanged" => 0}, profile: nil}}
We passed the table name and a filter instead of a id. If we send an id it’ll delete only one document matching the id. Here we send a map with user key and a value, this’ll delete all the document matching the value of user key.
So that’s all for today. Let me know your feedback or corrections in the comments below.
You can find the source code here.
Happy brewing some elixir.