-
Notifications
You must be signed in to change notification settings - Fork 1
Getting Started with the NoSQL Database API
The goal of this guide is to get you familiarized with the Predix SDK For iOS's NoSQL Local Database API .
You must have:
- Completed all tasks from our Getting Started Guide
- Knowledge of Xcode and Swift programming concepts
- General understanding of NoSQL 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
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)")
}
}
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
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)
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 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.
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.
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:
- The closure function must be thread-safe
- The same input must always produce the same output.
- 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
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 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 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
- See NoSQL Database Programming Guide
- Check out the NoSQL Local Database API Reference documentation
- See the example project
Getting Started Guides
Features
How-To Guides:
- Using Authentication API to Authenticate a User
- Using Online API to make Network Requests
- Using Time Serires API to Fetch Time Series Data
- Disable Usage Analytic tracking
API Documentation: