Chapter 4. Data Storage

Opa manages all aspects of applications and services within a single language semantic. Storing and querying data is, not surprisingly, one of the core features of Opa.

In most frameworks, we use APIs and connectors to communicate between the language and a database. Both of these “speak different languages,” with some form of mapping in between them.

The Opa approach is slightly different, with the database operations being very tightly integrated into the language and the mapping being performed fully transparently by the compiler. The layer that performs transparent mapping of data is known as DbGen.

In this chapter you will learn about Opa’s approach to storing basic types. Then we will discuss how to handle a slightly more complex data type: maps.

CRUD (Create, Read, Update, and Delete)

To get started, let’s look at a very simple database declaration, containing only a single int, a counter of sorts:

database db {
  int /counter = 0;
}

As you can see, Opa features a database block with a given name and a list of declarations of database values enclosed in curly braces.

Note

Opa programs can handle multiple database connections, even through different database engines. At the time of this writing, support of MongoDB is much more advanced, but CouchDB is also supported and PostgreSQL support is in progress.

Every declaration consists of a type (here, int), a path (here, /counter), and optionally, a default value (here, 0).

The default value is used when you attempt to read a path’s value that does not exist. In cases where the path was never written or was removed, the default value is returned.

Note

Omitting initialization values will cause Opa to use a default value, which is 0 for int, 0.0 for float, and "" for string values.

Locations in Opa’s database are called paths, as they bear a strong similarity to filesystem paths. Every value is available at a path consisting of the database name, followed by the path of the value, in our case /db/counter. You can read a given value by simply writing its path, as in:

counter_value = /db/counter

Note

There is an alternative read operation, prefixed with a question mark: ?/db/counter. The difference occurs in read operations on paths that were never written into. The regular variant in this case will just supply the default, whereas an operator prefixed with a question mark returns optional value, with the value present only if it was explicitly written into the path. You will learn more about optional values in Opa in Polymorphic Types.

Similarly, you can write the value using path <- value notation:

/db/counter <- 42;

A few extra operators are also available for manipulating int paths:

/db/counter++;
/db/counter += 10;
/db/counter -= 3;

The last element in CRUD is Delete, which is also very easy with Opa. To delete the counter, you write:

Db.remove(@/db/counter)

Of course, this is just the beginning of the “database story” in Opa. You will learn more as we go along.

To illustrate the usage of the database, let’s extend our simple Opa program from Writing and Running the Code and add a database to it. We’ll also use a function that involves the onclick attribute from Dynamic Content to count clicks.

database int /counter = 0;
function action(_) {
    /counter++;
    #msg = <div>Thank you, user number {/counter}!</div>
}
function page() {
    <h1 id="msg">Hello</h1>
    <a onclick={action}>Click me</a>
}
Server.start(Server.http,
  [ { resources: @static_resource_directory("resources") },
    { register: {css:["/resources/css/style.css"]} },
    { title: "Database Demo", page: page }
  ]
)

Compile and run this application in your terminal:

Demo:~ ida$ opa opa_database_demo.opa --

You will get a result similar to the screenshot shown in Figure 4-1.

Opa database demo app
Figure 4-1. Opa database demo app

You will learn more about databases in the following chapters. But first, let’s take a look at maps.

Maps (Dictionaries)

The data model for the wiki app you will build is quite simple: you want a collection of topics. A topic is represented by a string and it should be associated with content. You will use the Markdown markup format for the content. We will explain how to handle this format in Markdown, but for now, all you need to know is that Markdown is internally represented as a string.

For your data model you need a mapping from strings (topics) to strings (Markdown content). This is what maps are for.

Map is an abstract data type that associates keys with values. It is often called a dictionary or an associative array, and is, in many programming languages, implemented using hash tables.

All you need to know for now is that in Opa, the type of the dictionary is map(key, val), where key is the type of keys and val is the type of values. For instance, map(int, string) is a type of dictionary mapping int keys to string values.

In memory, it is simple to play with maps. The only thing to remember is that they are used in a functional way [for a refresher, refer to Functional Programming].

You can, for example, store values in successive versions of maps and retrieve them like so:

m0 = Map.empty
m1 = Map.add(1, "Paris", m0)
m2 = Map.add(2, "London", m1)

// result is an option
result = Map.get(2, m2)
value = Option.default("Not found", result)

Note that although m0, m1, and m2 are separate values, they point to one another and the final data structure is stored efficiently in memory.

Storing maps in databases is even easier than manipulating them in memory, thanks to the DbGen automation layer that Opa provides. For the wiki, you will want a database mapping from strings to strings, which you can obtain with the following declaration:

database wiki {
  map(string, string) /page
}

The read/write notation that we discussed earlier has a variation that allows you to easily index a given map element by providing its key in square brackets. So read and write operations on maps become:

Paris_content = /wiki/page["Paris"]   // read
/wiki/page["Paris"] <- Paris_content  // write

At this point, you know enough to write two useful functions for data manipulation in the wiki; save_data(topic, source) saves source as new content for topic:

function save_data(topic, source) {
  /wiki/page[topic] <- source;
}

And load_data(topic) retrieves content for topic:

function load_data(topic) {
  /wiki/page[topic];
}

What will happen when you try to load data for a nonexisting page? Remember our discussion about default values in CRUD (Create, Read, Update, and Delete)? This notion extends to maps as well: if you ask for nonexisting data, you will get the default value, which is an empty string.

It is possible to change this default value, although the syntax will be slightly different, as you would be providing a default for an individual element in a map, not the map itself. You will need to add a new line in your database definition:

/page[_] = "This page is empty. Double-click to edit."

Note

The underscore (_) here means “any value.” We will demonstrate more uses of the underscore later in the book.

The final database declaration for the wiki app looks like this:

database wiki {
  map(string, string) /page
  /page[_] = "This page is empty. Double-click to edit."
}

Summary

In this chapter you learned the basics of handling data storage in Opa. You should now know how to:

  • Store and manipulate basic values in the database
  • Store maps, or associations from keys to values

In the following chapter we will look in more detail at the topic of utilizing HTML and CSS to build user interfaces (UIs) in Opa.

Get Opa: Up and Running now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.