Introduction to Server Side API in NoSQL Database - NosDB

In the first article, we discussed the .NET client API of NosDB in great detail. As NosDB offers vast capabilities for server-side code handling, this article presents a brief tutorial to help understand key server side features of NosDB. Since it is .NET based, the server side API runs in process to the database which boosts performance considerably. Because you can write and deploy code directly on your database servers, you also get full control of server side activity as well.

The following is a list of the server-side programming offered in NosDB:

  • CLR Triggers
  • CLR User Defined Functions
  • MapReduce

CLR Triggers

Common Language Runtime Triggers are functions that are registered against database operations. They are executed before or soon after the operations are performed. NosDB offers collection-level triggers to perform client-defined actions (prior to or soon after an operation has been performed). You can only have a single trigger registered per collection; however, the trigger itself may contain multiple actions. There is an IDatabaseTrigger interface provided in NosDB which needs to be implemented and deployed on the server-side.

Triggers are primarily used to corral the infinite flexibility a NoSQL database provides. Since Triggers launch whenever data is updated, added or deleted in the database, they allow the .NET developer to implement data constraints. In concept, NosDB Triggers work the same way as in a relational database.

To write Trigger .Net code, include the Alachisoft.NosDB.Common.Triggers namespace in your project to implement the IDatabaseTrigger interface. This interface consists of two methods, PostTrigger() and PreTrigger() where you add your post and pre logic respectively. Both of them take a TriggerContext as input which consists of the following members:

  • EventDocument: implements an instance of a JSON Document upon which the operation will be performed and the trigger fired.
  • TriggerAction: It is an enum that identifies the trigger action to be performed.

A sample implementation is given below for reference:


// APPLY ON ORDERS COLLECTION ONLY
public class Validation : IDatabaseTrigger
{
    // After document changes
    public void PostTrigger(TriggerContext context)
    {
        // ... 
    }

    // Before document changes
    public bool PreTrigger(TriggerContext context)
    {
        IJSONDocument document = context.EventDocument;

        // Operation type
        switch (context.TriggerAction)
        {
            case TriggerAction.PreInsert:

                if (!document.Contains("RequiredDate") ||
                    !document.Contains("CustomerID") ||
                    !document.Contains("EmployeeID") ||
                    !document.Contains("ShipperID") ||
                    document["RequiredDate"] == null ||
                    document["RequiredDate"].ToString().Length == 0)
                {
                    return false;
                }

                if (!document.Contains("OrderDate") ||
                    document["OrderDate"] == null ||
                    document["OrderDate"].ToString().Length == 0)
                {
                    // Update JSON Document
                    document["OrderDate"] = DateTime.Now;
                }
                break;
            case TriggerAction.PreDelete:
                // Insert code here 
                break;
            case TriggerAction.PreUpdate:
                // Insert code here
                break;
        }
        return true;
    }
}


This code sample has been taken from an article "Triggers in a .NET NoSQL Database". In this example the developer is ensuring the document will have some declared attributes and will not be null.

CLR User Defined Functions

NosDB also provides support for CLR User Defined Functions (UDF). It allows users to provide their own logic to be executed inside SQL queries. Although NosDB already provides some basic UDFs by default, developers can write their own UDFs to enhance querying capabilities. UDFs usually come in handy when you have to perform calculations within an SQL query, but you put those calculations in .NET and simply call them up in your queries.

Note: For a quick look at the built-in UDF functions check out the SQL cheat sheet.

UDFs are database-level routines which can be used in SELECT queries like the built-in functions for projection and comparison. You can implement a UDF either by the annotation [UserDefinedFunction] or by implementing the IUserDefinedFunctionProvider interface. Be sure to include the Alachisoft.NosDB.Common.Queries.UserDefinedFunction namespace in your project and to deploy these implementations using NosDB Management Studio. Samples for both these methods are given below:

IUserDefinedFunctionProvider:

The IUserDefinedFunctionProvider interface contains two methods:

  • GetUdfInfo(): the logic contained in this method returns a list of all those UDFs that have implemented the IUserDefinedFunctionProvider interface within that class.
  • Execute(): this method takes the UDF method name as input and returns the results after executing the specified UDF. If there are multiple UDFs implemented, the method switches between them depending on the name of the function.

public class UDFWithInterface : IUserDefinedFunctionProvider 
{ 
    public string[] GetUdfInfo() 
    { 
        return new string[2] { "CalculateChurn", "GoldSubscribedMonths" };    } 

    public object Execute(string functionName, object[] parameters) 
    { 
       switch (functionName) 
{      
	case "CalculateChurn": 
		return CalculateChurn((IJSONDocument)parameters[0]); 
	case "GoldSubscribedMonths": 
		return GoldSubscribedMonths((int)parameters[0]); 
} 
return null;
    }
}


[UserDefinedFunction]:

NosDB also provides the option of working with tags instead of implementing an interface. Follow the example below to see what this annotation method has to offer:


public class UDFWithTags 
{ 
    [UserDefinedFunction("CalculateChurn")] 
    public static IJSONDocument CalculateChurn(IJSONDocument customer) 
    { 
        //perform calculations 
    } 
    [UserDefinedFunction("GoldSubscribedMonths")] 
    public static int GoldSubscribedMonths(int customerID) 
    { 
         //perform calculations 
    }
    // ...

}


Then you can call these UDFs inside your SQL query:


SELECT GoldSubscribedMonths(23143) FROM Customers

MapReduce

NosDB offers comprehensive support for MapReduce. Since MapReduce is executed at the collection level, you can perform MapReduce tasks spanning multiple collections. NosDB offers simple interfaces for the Mapper, the Combiner and the Reducer. You deploy their libraries on the server side; the Mapper interface is compulsory but the Combiner and Reducer implementations are optional. Having deployed your selected libraries, you initialize the MapReduce parameters and then execute the task.

One basic example is to calculate the number of cities in flight data. Typically, you would map the city in every document inside the Mapper class and then send it over to the Reducer. The Reducer then accumulates the number of unique cities names and calculates the top occurring cities. The following code snippets will walk you through the procedures but don't forget to use the Combiner class for further optimization:

Mapper: Code written in the Mapper class is executed on every shard. The Mapper 'maps' the data in the collections and forwards the data to the Reducer. Please refer to the following code:


public class Mapper : IMapper 
{ 
    public void Map(JSONDocument document, IOutputMap context) 
    { 
        if(document["Origin"].ToString() == "STL") 
        { 
             context.Emit(document["City"], 1); 
        }
    } 

    public void Dispose() 
    { 
        //provide optional implementation 
    } 
}


Reducer: The items mapped by the Mapper, running on every server, are collected by the Reducer.


public class Reducer : IReducer 
{ 
    // ...
    
    public void Reduce(object value) 
    {
        Dictionary combinedCities = (Dictionary)value; 
        foreach (KeyValuePair item in combinedCities) 
        { 
        	if (cities.ContainsKey(item.Key)) 
        	{ 
        		cities[item.Key] += item.Value; 
        	} 
        	else 
        	{ 
        		cities[item.Key] = item.Value; 
        	} 
        }
    }
    
    public object FinishReduce() 
    {
        //Combine result
  var sortedDict = cities.OrderByDescending(entry => entry.Value)
	.Take(3) .ToDictionary(pair => pair.Key, pair => pair.Value); 
  return new KeyValuePair("Top3Cities",sortedDict);
    }

}


Execute Task: Once you implement the interfaces, and deploy them on the server-side (refer to the 'Deploying Providers' section in either the NosDB management studio guide or the PowerShell guide), you can execute the task from the client-side. For that, you need to initialize the parameters and then proceed towards executing the task.

NosDB provides a MapReduceInitParams class (in the Alachisoft.NosDB.Common.MapReduce name space) to initialize the task parameters.

There are two compulsory input parameters:

    • OutputOption: You have the option to either persist the output of the task in a collection or store it in-memory. It takes an instance of MROutputOption which provides the following options to choose from:
    • CUSTOM_COLLECTION (user-defined collection)
    • DEFAULT_COLLECTION (default collection)
    • IN_MEMORY (no persistence)
    • CustomCollectionName: If you choose to store your output in a custom collection, here you need to specify its name.

The following code snippet demonstrates the initialization of the parameters:


//FQN of deployed assemblies 
MapReduceInitParams initParams = new MapReduceInitParams("CalculateCities", "CalculateCities.dll"); 
//Output to be persisted in a custom collection 
initParams.OutputOption = MROutputOption.CUSTOM_COLLECTION; 
//in case of custom collection 
initParams.CustomCollectionName = "OutputCollection"; 
//List of collections to run the MapReduce task over 
List collections = new List(new string[] { "FlightData" }); 
//Execute task on all collections 
ITrackableTask task = db.ExecuteTask(initParams, collections); 


Having initialized the parameters, you can proceed to the next and final step of executing the MapReduce task. Include the Alachisoft.NosDB.Client namespace in your application to utilize the related methods.

The ExecuteTask() method executes the task, the result of which is returned in an instance of ITrackableTask interface which can be implemented to track the execution of a task. These tasks can be executed on all the collections or on a query set result (NosDB provides separate overloads for both of them). The following example demonstrates how to execute the task on a query set result:



//Get result 
ITaskResult result = task.GetResult(); 
//Obtain enumerator to enumerate over task result 
ICollectionReader reader = result.GetEnumerator(); 
while (reader.ReadNext()) 
{ 
//perform operations 
} 


Another server-side feature supported by NosDB is the Aggregator. Like MapReduce, it is executed at the collection level so you can use the Aggregator with multiple collections and is used for further optimizations and ease of use. For more in-depth information on MapReduce and Aggregators make sure to check out the documentation.This marks the end of the tutorial on Server Side APIs; providing you with a walk-through of server-side code handling in NosDB. For more details on Server Side API features, please refer to on-line NosDB coding documentation where they are discussed both programmatically and in theory.