Skip to content

Getting Started with the NoSQL Database API

Omer Elhiraika edited this page Dec 6, 2018 · 36 revisions

The goal of this guide is to get you familiarized with the Predix SDK For iOS's NoSQL Local Database API .

Prerequisites

You must have:

  • Completed all tasks from our Getting Started Guide
  • Knowledge of Xcode and Swift programming concepts
  • General understanding of NoSQL Databases

Summary Steps

  1. Opening Databases
  2. Working with Documents
  3. Replicating Data to PredixSync Service
  4. Querying Indexes

Opening Databases

Before any database interactions can take place, the database must be opened, this involves accessing or creating the physical on-device files of the database. To open a database, a Database.OpenDatabaseConfiguration structure is provided to the API. This configuration includes the local file system URL of the database, the database name, and other characteristics. For your convenience, the Database.OpenDatabaseConfiguration structure includes a default configuration that you can use directly or as a starting point for the providing additional configuration details.

Example:

let configuration = Database.OpenDatabaseConfiguration.default

do {
	let database = try Database.open(with: configuration, create: true)
	if let database = database {
		// ... the database is now open, and ready for interactions
	}
} catch let error {
	// ... handle error...
}

Note - A database refers to a physical set of files on the device, multiple calls to open the same database result in returning the same Database object. It is advised to avoid retaining multiple references to the same database. If a database is already open, use the openedWith function to retrieve a reference to it.

To learn more about Databases, see the No SQL Local Database Programming Guide

Working with Documents

Documents are the central data structure of the PredixSDK database. The Document object is a type of dictionary where the dictionary keys must be strings. Accessing values from a dictionary is similar to accessing values from any other Dictionary class in Swift.

Documents can be written to, retrieved from, or deleted from a database.

To read more about Documents and different components of the Document, see No SQL Local Database Programming Guide

1. Write documents to your database using the save , add, or update methods.

The save method is the primary method to write documents to the database. It automatically determines if the document already exists, and will add the document to the database, or update an existing document as needed. However, in some cases a developer may want finer control over adding or updating documents, so the Database object also includes add and update methods that return errors if the operation is not appropriate for the provided Document object.

Example:

let database = Database.openedWith(Database.Configuration())
let document1: Document = ["name": "Example Document", "anInt": 123, "aDouble": 3.14]

database.save(document1) { result in
	switch result {
		case .success(let savedDocument):
			print("Saved document with id: \(savedDocument.id) and name: \(savedDocument["name"])")
		case .failed(let error)
			print("Error saving document: \(error)")
	}
}

2. Retrieve documents from the database using the fetchDocument method, passing the document's unique identifier.

Example:

let database = Database.openedWith(Database.Configuration())
let myDocumentId = "my_document"

database.fetchDocument(myDocumentId) { fetchedDocument in
	if let document = fetcheDocument {
		print("Fetched document with id: \(document.id)")
	} else {
		print("No document with id: \(myDocumentId) exists")
	}
}

3. Delete documents from the database using the delete method, passing the document's unique identifier.

Example:

let database = Database.openedWith(Database.Configuration())
let myDocumentId = "my_document"

database.delete(myDocumentId) { result in
	switch result {
		case .success(let deletedDocumentId):
			print("Deleted document with id: \(deletedDocumentId)")
		case .failed(let error)
			print("Error deleting document: \(error)")
	}
}

Data Replication to PredixSync Service

Prerequisite: Some familiarity with the PredixSync service

Data replication with a PredixSync backend service is a powerful tool that you can use to access Predix data while offline and share data with other system users. There are two primary characteristics of replication:

Repeating — A repeating replication automatically detects changes and replicates them as needed, until explicitly stopped or when the application shuts down. This type of replication is useful when you want to ensure all changes from one system are sent to another system as soon as possible. Non-repeating replication performs the replication process only once.

Bidirectional — A bidirectional replication refers to the changes being sent from the PredixSync server to the client and from the client back to the PredixSync server. Non-bidirectional replication only receives changes from the PredixSync server to the client. This type of replication is useful for read-only systems or systems that want to receive changes immediately but delay sending changes to the server.

To learn more about Replication concepts, see the NoSQL Database Programming Guide

Configuring Replication

The ReplicationConfiguration structure has three pre-configured options to create the most common types of replication:

Creates a repeating, bidirectional replication configuration:

let replicationConfig = ReplicationConfiguration.repeatingBidirectionalReplication(with: myPredixSyncURL)

Creates a non-repeating, bidirectional replication:

let replicationConfig = ReplicationConfiguration.oneTimeBidirectionalReplication(with: myPredixSyncURL)

Creates a non-repeating, non-bidirectional replication:

let replicationConfig = ReplicationConfiguration.oneTimeServerToClientReplication(with: myPredixSyncURL)

Starting Replication

To start the replication process simply call the database startReplication method, passing an appropriate ReplicationConfiguration instance:

Example:

let database = Database.openedWith(Database.Configuration())
let predixSyncURL = Utilities.predixSyncURL
let replicationConfig = ReplicationConfiguration.repeatingBidirectionalReplication(with: predixSyncURL)

database.startReplication(with: replicationConfig)

Note - All replication work happens in a background queue so the startReplication method returns immediately, and ongoing replication will not impact UI responsiveness or performance.

Replication Status Information

Replication uses a standard delegate pattern to provide information on the current replication status. The object associated with the replicationStatusDelegate property of the Database is called for the following replication events:

  • replicationDidComplete
  • replicationIsSending
  • replicationIsReceiving
  • replicationFailed

Information in each of these events allow you to handle errors, update status UI, or know when a data exchange is completed.

See the code documentation for the ReplicationStatusDelegate protocol for more details.

Querying Indexes

Using Indexes and Queries is a fast and efficient way to search and read data from the database. An Index is a copy of selected pieces of data that can be searched very efficiently. A Query is then the search through that Index to pull out selected elements. In order to use a Query, you must first build an Index.

Building Indexes

In super simplified terms, and Index is a copy of parts of a document; think of it like a table, or dictionary. This copy is stored within the database, and updated whenever the data in the database is updated.

An index consists of at least three components:

  • Name: the name of the index
  • Version: the version of the index
  • Map: The mapping closure that defines the data in the index

The Name of the index is how the index is referenced when a query is run upon the index.

The Map is what defines the index. It is a closure whose output makes up the index dictionary that will be queried when a Query is run. There are some specfic rules for this closure:

  1. The closure function must be thread-safe
  2. The same input must always produce the same output.
  3. The closure cannot modify the database

The map closure is defined as:

typealias Map = (_ document: Document, _ addIndexRow: @escaping  AddIndexRow) -> Void

and AddIndexRow is a closure and defined as:

typealias AddIndexRow = (_ key: Any, _ value: Any?) -> Void

So, in the map closure, the code receives a Document and an AddIndexRow closure. The document is then used to determine what rows to add to the index, and those rows are added by calling addIndexRow which provides the index key and the optional value. The key is used to query the index, and the value, along with some other information, is returned when the index is queried.

Example

if I have a set of documents similar to:

let invoiceDocument1: Document = ["customer": "Acme", "invoiceId": "abc123", "itemIds": ["item1", "item2", "item3"], "total": 153.05]

An index that would allow searching of these documents by customer could look like:

// Define a index for getting invoices by customer name
let customerInvoicesMap: Indexer.Map = { document, addIndexRow in
   
   if let name = document["customer"], invoiceId = document["invoiceId"] {
   		addIndexRow(name, items)
   }
}

let customerInvoiceIndex = Index(name: "customerInvoices", version: "1.0", map: map)

// Now open the database including the index
let configuration = Database.OpenDatabaseConfiguration(indexes: [customerInvoiceIndex])
            
let database = try Database.open(with: configuration, create: true)

Note: if the mapping closure code changes, the version string must also change. This will inform the database that the old index is no longer valid.

Indexes have a lot of power and capabilties. For more information see our NoSQL Database Programming Guide

Querying an Index

Once an index has been built, how is it used to extract information from the database?

There are two types of querys: by key, or by range.

Query by Key

Query by key is straightforward; as shown above, the index has defined keys, the query parameters are a subset list of those keys. The key must match exactly.

Example

let query= QueryByKeyList()
query.keys = ["Acme", "AmerTek", "Oscorp"]

database.runQuery(on: "customerInvoices", with: query) { queryEnumerator in 
	print("Found \(queryEnumerator.count) Invoices from companies Acme, AmerTek, and Oscorp:")
	while let result = queryEnumerator.next() {
	   print("customer: \(result.key) : invoice #: \(result.value)")
   }
}

Query by Range

Query by Range allows retreival of query results between a starting key and an ending key. It returns all keys falling within this range, inclusive, as sorted by the index. Sorting rules vary by the data type of the key, so strings are sorted alphabetically, numbers sorted numerically, etc. Additionally, leaving a start key nil indicates that the query should begin at the very first row of the index; a nil end key indicates the results should end at the last row of the index, thus providing a "less than" and "greater than" type query. Keys need not be complete either.

Example

let query= QueryByKeyRange()
query.startKey = "a"
query.endKey = "b"

database.runQuery(on: "customerInvoices", with: query) { queryEnumerator in 
	print("Found \(queryEnumerator.count) Invoices from companies whose names start with a or b")
	while let result = queryEnumerator.next() {
	   print("customer: \(result.key) : invoice #: \(result.value)")
   }
}

To read more about Indexes, Queries, and the additional Map/Reduce function, see the NoSQL Database Programming Guide

Next Steps