Services as Databases pattern

svc_expr services can function as databases, which can be updated or queried by operations on them.

A database is defined as a state variable on the service. Operations on the service can read from this database or update it.

Such a state variable can be any Erlang term. For example, an Erlang map or list.

The expr.init.[Var] property can be used to define an initial map. If the state is updated, a new map is returned.

Table 1 shows an example using a service as a database.

The list below shows sample database constructions.

  • Records (Erlang map)
    #{0 => "foo",
      1 => "bar"}
  • Records with keys (Maps within a map)
    #{
      0 => #{
        key => 0,
        first_name => "Bill",
        last_name => "Door"},
    
      1 => #{
        key => 1,
        first_name => "King",
        last_name => "Kong"}
    }
  • Listed records (List of maps)
    [
      #{
        name => "Bob",
        dob => {1976,10,18},
        gender => male},
      #{
        name => "Mary",
        dob => {1999,11,07},
        gender => female}
    ]
Table 1. Services as databases - Sample mixWe initialise a service with an Erlang map, which contains a number of records. Each record comprises a unique ID and a string type value. The operations on the service can look up, add or remove records.
Example Description
<service 
  name="Database" 
  provision="expr">
  <prop 
    name="expr.state" 
    Table="NewTable" 
    NextID="NewNextID"/>
  <prop 
    name="expr.init.Table" 
    content-type="text/x-erlang"><![CDATA[
#{0 => "foo",
  1 => "bar"}
  ]]></prop>
  <prop 
    name="expr.init" 
    NextID="2"/>
</service>
The Database service is provisioned using the Expressions extension.

The following state variables are specified for Database:

  • Table, which can be updated by NewTable
  • NextID, which can be updated by NewNextID
Note: Variable names must start with a capital letter.

The Table state variable is initialised with the expr.init.[Var] property. When Database is started, it comes up with an Erlang map (#{…}), which comprises the following records:

  • {0, "foo"}
  • {1, "bar"}
Important: Do not use a full stop at the end of the term, like in the case of expressions contained in expr.src.
The records have an ID, a key in other words, and a value.

The NextID variable is initialised to 2. It will be used as the key of the next record.

<request 
  name="Insert" 
  service="Database" 
  fields="INSERT name">
  <prop 
    name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Name = get("name"),

NewTable =
  Table#{
    NextID => Name},

put("id", NextID),

NewNextID = 
  NextID + 1,
"Ok".
  ]]></prop>
  <reply 
    name="Ok" 
    fields="id"/>
</request>
The Insert operation updates the Table variable, and thus creates a new map.

The new map contains:

  • All previous records
  • The new record added through the Insert operation

The input and output fields of Insert are bound to the Name and NextID variables.

The expressions in Insert:

  1. Creates a new record by
    • Specifying NextID as the key of the new record
    • Specifying Name, which was bound to the input field name, as the value of the new record
  2. Updates the NextID variable, adding one to it
  3. Sends the Ok reply

The Ok reply is sent with the id field, which was bound to the NextID variable.

Note: Do not manually type the <![CDATA...]]> delimiters. The Editor automatically renders text content as XML CDATA sections.
<request 
  name="Get" 
  service="Database" 
  fields="GET id">       
  <prop 
    name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Id = get("id"),

case maps:is_key(Id, Table) of
  true ->
    Name = maps:get(Id, Table),
    put("name", Name),
    "Ok";
  false ->
    put("error", not_found),
    "Error"
end.
  ]]></prop>
  <reply 
    name="Ok" 
    fields="name"/>
  <reply 
    name="Error" 
    fields="error"/>
</request>
The Get operation returns a record based on its key, if the key matches any record.

The key is the ID of the record, which is declared as the Id variable, bound to the id field.

The value of the record is declared as the Name variable, bound to the name field.

The Error variable is bound to the error field.

The expressions in Get:

  1. Decides if a record matches the key
  2. Based on the outcome of the matching, it sends either:
    • The Ok reply with the value corresponding to the key
    • The Error reply, with the value not_found
<request 
  name="Delete" 
  service="Database" 
  fields="DELETE id">
  <prop 
    name="expr.src" 
    content-type="text/x-erlang"><![CDATA[
Id = get("id"),
NewTable = maps:remove(Id, Table),
"Ok".
  ]]></prop>
  <reply 
    name="Ok" 
    fields="OK"/>
</request>
The Delete operation updates the Table variable, and thus creates a new map.

The new map contains all previous records, except the one deleted.

The input field of the operation, id, is bound to the Id variable. It is the key of the record.

The expressions in Delete:

  1. Removes any record that matches the key
  2. Sends the Ok reply

In this case, the Ok reply is sent irrespective of whether the key existed or not.