Basic Operations

Basic Operations

Key-Value Store

Riak is a key-value store, which means that all data is stored as a key and a value. To be able to organize data better, Riak requires all key-value-pairs to belong to one bucket. A bucket is similar to a dictionary or hashtable, where binary value-chunks are stored under string keys. The fundamental operations in uGameDB for interacting with Riak is Set(), Get() and Remove(). All three of these methods lie in the the Bucket class, which is a client-side representation of a Riak bucket.

Asynchronous Operations

When interacting with an external database it is not certain that requests will return immediately. The request needs to be sent over the network and received by the database server. Then the value needs to be stored or fetched from disk and returned over the network again. Even though Riak's distributed design makes it much faster than a transaction-oriented database, there are still many factors that can affect the time one request will take, such as network condition and the load on the responding node. Because of this, uGameDB handles requests asynchronously, so that when you send a request from your code, it will be executed on a separate background thread. In order to keep the user from having to manage these threads, uGameDB provides two patterns to interact with these asynchronous operations.
  • Unity Coroutines - By sending your requests from coroutines you can write your database interaction sequences as if they were synchronous. This pattern produces the simplest and most maintainable code, and you should use it as much as you can.
  • Callbacks - You can also assign callbacks for success and failure when you send your requests. This pattern is probably most familiar to non-Unity developers and have been included for this reason.
Both of these patterns can be used in combination, but some caution should be taken not to mix them too much or you will end up with code that is very confusing to follow.
Both of these patterns are powerful programming models because they make you more or less independent of the database response time. The easy access to asynchronous operations ensures that long-running requests will never decrease the server tick rate. This is crucial for any game server that performs a physics simulation that need to be updated at an even frequency to be correct. For other game types, for example social games and turn-based games, this is not crucial but still a very convenient programming style because of the tiny amount of code needed.
Now we will try to illustrate the difference between the two patterns by performing a simple database operation sequence using both of them. The following code examples demonstrate a set-get-sequence in which a high-score entry for a player is first written to the database and, when the request completes successfully, it is read back again. Try the scripts in the editor by in turn adding them to an empty game object in a scene, together with the utility script uGameDBConnection to provide a database connection. The utility script is located in Plugins/uGameDB/Utility Scripts.

The Coroutine Pattern

The CoroutineExample component only contains a Start() method which is a coroutine, as denoted by the IEnumerator return type. Because it is one of the default Unity callbacks, there is no need to call StartCoroutine(). When the component starts, it sends a set request to the database. Then it uses a coroutine yield to wait for the request to complete. Request.WaitUntilDone() returns a Coroutine object of its own that the Start() coroutine can wait for. This causes the Start() coroutine to wait until the Request.isDone property is true, which means that a response (either success or error) has returned from the database.
Start() then prints an error message if the request failed, or continues to make a get request to the database to read the previously written value. It then yields in the same manner as for the set request and waits until the get request has finished, and finally prints the result.
using System.Collections; using UnityEngine; using uGameDB; public class CoroutineExample : MonoBehaviour { private Bucket scoresBucket = new Bucket("high-scores"); private IEnumerator Start() { // Write a high-score entry to the database. var setRequest = scoresBucket.Set("easy-steve", 6100, Encoding.Json); // Wait for the write operation to complete. yield return setRequest.WaitUntilDone(); if (setRequest.hasFailed) { Debug.LogError("Write high-score failed! " + setRequest + ", " + setRequest.GetError() + ", " + setRequest.GetErrorString()); yield break; } // Read a high-score entry from the database. var getRequest = scoresBucket.Get("easy-steve"); // Wait for the read operation to complete. yield return getRequest.WaitUntilDone(); if (getRequest.hasFailed) { Debug.LogError("Read high-score failed! " + getRequest + ", " + getRequest.GetError() + ", " + getRequest.GetErrorString()); yield break; } Debug.Log("Read high-score " + getRequest.GetValue<int>() + " for player '" + getRequest.key + "'."); } }

The Callback Pattern

The CallbackExample component performs the exact same sequence as CoroutineExample and produces the exact same output. However, it only uses callback methods to piece together this sequence. This means that the sequence is split over several methods and it can be tricky for someone else to see the sequence flow without some documentation.
When Start() is called, the set request is sent, and two callbacks are assigned to the request, one for a successful result and one for an error result. The error callback simply prints the error log message, while the success callback issues the get request. The get request has similarly defined callbacks, a success callback that prints the result, and an error callback that prints the error message.
using UnityEngine; using uGameDB; public class CallbackExample : MonoBehaviour { private Bucket scoresBucket = new Bucket("high-scores"); private void Start() { // Write a high-score entry to the database. // When the write operation completes, OnSetSuccess or OnSetError will be called. scoresBucket.Set("easy-steve", 6100, Encoding.Json, OnSetSuccess, OnSetError); } private void OnSetSuccess(SetRequest<int> request) { // Read a high-score entry from the database. // When the read operation completes, OnGetSuccess or OnGetError will be called. scoresBucket.Get("easy-steve", OnGetSuccess, OnGetError); } private void OnSetError(SetRequest<int> request) { Debug.LogError("Write high-score failed! " + request + ", " + request.GetError() + ", " + request.GetErrorString()); } private void OnGetSuccess(GetRequest request) { Debug.Log("Read high-score " + request.GetValue<int>() + " for player '" + request.key + "'."); } private void OnGetError(Request request) { Debug.LogError("Read high-score failed! " + request + ", " + request.GetError() + ", " + request.GetErrorString()); } }
These examples illustrate the fact that the same methods are used to send the request regardless of which pattern you use. The callback arguments to Bucket.Set(), Bucket.Get(), etc, are optional. If you don't specify callback, they will default to null, which means that the callback will not be invoked. It is also possible to omit one of them. To make the set request with only a success callback you can call
scoresBucket.Set("easy-steve", 6100, Encoding.Json, OnSetSuccess, null);
or if you want to make the same request with only an error callback you can call the following.
scoresBucket.Set("easy-steve", 6100, Encoding.Json, null, OnSetError);
It is possible to mix the two patterns and if you need to do this, then it is worth noting that Request.isDone does not become true until any callback has finished. This means that if you use both yield to wait for completion and a callback then the callback will be completed before the coroutine resumes. It also means that if your callback does something that takes a long time to finish, the coroutine will not resume until this is done. Consider this when you design your operations.
Together, the coroutine and callback patterns place a lot of power and flexibility at your fingertips, and give you the tools you need to write even the most intricate database interaction sequences.