Creating Storage Handling Module using Mnesia(Part I)

Mnesia is a distributed telecommunications DBMS, and it is a built-in module in Erlang, so we don't need to specify any dependencies in our ExShortStorage app. The reason we are using Mnesia here, besides the fact it's built-in is:
1. I can write more stuff about Elixir rather than other services such as redis and sqlite.
2. Mnesia itself is a neat DBMS. it is a distributed system, which means we can deploy our service across multiple machines without the need to maintain a centralized database server. Also, it handles concurrent transactions correctly, which can be tricky to do if we are to use redis or sqlite.

Before writing code to files, let's get ourselves familiar with Mnesia in Elixir's interactive shell (iex):

type "iex" in your terminal to enter the interactive shell, and we will use 5 commands to create a schema, create a table, and save a Link record in it.

iex(1)> :mnesia.create_schema([node()])
:ok
iex(2)> :mnesia.start()
:ok
iex(3)> :mnesia.create_table(Link, attributes: [:slug, :url])
{:atomic, :ok}  
iex(4)> writer = fn -> :mnesia.write({Link, "goog", "https://google.ca"}) end
#Function<20.127694169/0 in :erl_eval.expr/5> 
iex(5)> :mnesia.transaction(writer)
{:atomic, :ok}

Let's walk through what happened here.

On line 1, we created a schema. You can think of it as a database in MySql or a database file in Sqlite. we used the atom :mnesia to call the Erlang module from elixir, and executed the function create_schema with parameter [node()]. Atom is a basic data type in Elixir, similar to symbol in other languages such as javascript. You can think of it as a constant whose value is it's own name (the name of an atom is pretty much all the information you can get from it). Atoms usually starts with ':', but names starting with upper case letters are also atoms, eg, is_atom(A) would return true. Elixir uses atoms to reference modules, that's why we can use :mnesia to call mnesia module. Built in Elixir modules, eg, Emun and List, are all named by atoms. But the convention is that modules in Elixir are CamelCased, whereas native Erlang modules are named by lower case atoms. Node() just returns an atom representing the name of the local node. As a language built with distributed systems in mind, Node is an important concept in Elixir, but we are not going into too much details here. Running this command, Mnesia would create a folder in the current directory where it persists stored data. It returns an atom :ok, meaning the operations is successful.

On line 2, we start the Mnesia application. In a single node configuration, the process is rather simple but with a multi node configuration it actually talks to all the nodes, syncs some data and make sure they can work together. It returns an atom :ok, meaning the operations is successful.

On line 3, we create a table called Link (which is an atom), and specifies the two attributes it has: :slug and :url, in order.

On line 4, we define a inline function with named writer, in the body of which, we wrote database operations. In this case, we are writing a Link record with attributes :slug and :url specified in order. In the example above, we are telling it to create a Link, with url set to "https://google.ca", and slug set to "goog". Inline functions are kinda like lambdas in python and arrow functions in javascript. The syntax to create a inline function is

fn parameters -> ...do_something end

for example, we can do

foo = fn a, b -> a + b end
foo.(1,2)
# returns 3

Note that in order to call inline functions directly, we need to append a dot at the end of it's name, which is different from a function that lives in a module, eg: :mnesia.create_table.

On line 5, we ran the operations specified on line 4 inside a transaction, telling it to apply operations we specified on line 4 and commit changes to database. It returned {:atomic, :ok} signifying that the transaction is successfully applied to database. The benefit of a transaction is that if one of the operations inside a transaction failed, the state of database will rollback, instead of ending up in a "dirty" state.

To confirm that data is indeed stored in the database, we can try to read it back:

iex(6)> reader = fn -> :mnesia.read({Link, "goog"}) end
#Function<20.127694169/0 in :erl_eval.expr/5>
iex(7)> :mnesia.transaction(reader)
{:atomic, [{Link, "goog", "https://google.ca"}]}

As we can see, we are able to find the Link with slug "goog".