Skip to content
Robbie Hanson edited this page Nov 10, 2019 · 11 revisions

"Hello World", YapDatabase style:

// Create and/or Open the database file
let db = YapDatabase()

// Configure database:
// We're going to store String's in the collection "test".
db.registerCodableSerialization(String.self, forCollection: "test")

// Get a connection to the database
// (You can have multiple connections for concurrency.)
let connection = db.newConnection()

// Write an item to the database
connection.readWrite {(transaction) in
  transaction.setObject("hello", forKey: "world", inCollection: "test")
}

// Read it back
connection.read {(transaction) in
  let str = transaction.object(forKey: "world", inCollection: "test") as? String
  // str == "hello"
}

You can think of YapDB as a "dictionary of dictionaries". Each collection is its own dictionary. You can configure each collection to store different types of classes/structs. And the values you write get stored to disk.

 

Codable

To store your own custom classes & structs in the database, you only need to do 2 things:

  • Make your class/struct conform to Swift's Codable protocol
  • Configure a database collection for your class/struct
class Foo: Codable { // Just implement Codable
	// ...
}
struct Bar: Codable { // Just implement Codable
  // ...
}

// Register it
db.registerCodableSerialization(Foo.self, forCollection: "foos")
db.registerCodableSerialization(Bar.self, forCollection: "bars")

// And you're done.
// Now you can read & write your custom objects with the database.

let foo = Foo()
let bar = Bar()

connection.readWrite {(transaction) in
  transaction.setObject(foo, forKey: "1", inCollection: "foos")
  transaction.setObject(bar, forKey: "2", inCollection: "bars")
}

connection.read {(transaction) in
  let foo = transaction.object(forKey: "1", inCollection: "foos") as? Foo
  let bar = transaction.object(forKey: "2", inCollection: "bars") as? Bar
}

 

Transactions

There are 2 types of transactions:

  • read-only transactions
  • read-write transactions

A read-write transaction is atomic: either ALL changes made to the database within a read-write transaction succeed, or ALL changes fail. For example, consider the following read-write transaction:

connection.readWrite {(transaction) in
  transaction.setObject(foo, forKey: "1", inCollection: "foos")
  transaction.setObject(bar, forKey: "2", inCollection: "bars")
}

What happens if the application crashes during this transaction? Is it possible that foo is written to disk, but bar isn't?

The answer is NO, because the database ensures the transaction is atomic. Either both foo & bar are written to the database, or neither are. The underlying sqlite engine ensures the entirety of the transaction either succeeds or fails.

A read-only transaction is also atomic, in a different sense. Consider the following code:

// In thread 1
connection1.asyncReadWrite {(transaction) in
  transaction.setObject(foo, forKey: "1", inCollection: "foos")
  transaction.setObject(bar, forKey: "2", inCollection: "bars")
}

// In thread 2
connection2.read {(transaction) in
  let foo = transaction.object(forKey: "1", inCollection: "foos") as? Foo
  let bar = transaction.object(forKey: "2", inCollection: "bars") as? Bar
}

Is it possible that connection2 can read foo but not bar?

Again, the answer is NO. A read-only transaction always sees the database at a particular commit. So it will either see the database before connection1's commit, or after connection1's commit. So it will either see both foo & bar, or it will see neither.

 

Concurrency

YapDB is purposefully designed to maximize concurrency. This gives your app a performance boost, while ensuring your data stays in a consistent state.

Concurrency is straight-forward. Here are the rules:

  • You can have multiple connections.
  • Every connection is thread-safe.
  • You can perform multiple read-only transactions simultaneously without blocking.
  • You can perform multiple read-only transactions and a single read-write transaction simultaneously without blocking.
  • There can only be a single read-write transaction at a time. Read-write transactions go through a per-database serial queue.
  • There can only be a single transaction per-connection at a time. Transactions go through a per-connection serial queue.

Here's how simple it is to add concurrency:

override func viewDidLoad() {
  
  connection = database.newConnection() // For main thread
  bgConnection = database.newConnection() // Concurrency w/ background ops
}

func dataDidDownload(_ items: [Sprocket]) {
  
  // Perform updates on background thread.
  // The 'bgConnection' won't block the separate 'connection',
  // so the user can continue scrolling as fast as possible.
  // Furthermore the asyncTransaction makes it dead-simple for us.
  
  bgConnection.asyncReadWrite( {(transaction) in
    
    for item in items {
      transaction.setObject(item, forKey: item.key, inCollection: "sprockets")
    }
    
  }, completion: {[weak self] in
  
    // Back on the main thread, let's update our view
    self?.newDataAvailable()
  })
}

 

Plugins

A collection/key/value database is nice, but it doesn't provide all the functionality you need as an app developer. And that's where extensions come in.

The collection/key/value architecture of YapDB is just the foundation for an advanced plugin system. These plugins are called "extensions". And they integrate rather seamlessly.

For example:

Do you need to display your data in a tableView? Perhaps a subset of it, sorted a certain way, and maybe grouped a certain way too. No problem. A YapDatabaseView is an extension designed to do just that. And it sends notifications that allow you to animate changes to your tableView / collectionView !

Or maybe you need to run queries against the database to find items. No problem. The secondary index extension can help you setup indexes on the properties you want, and can even handle SQL style queries.

Full text search? Gotcha covered. There's an extension built atop the FTS module written by Google.

Every app is different. In fact apps themselves change from version to version, along with the requirements they place on their database. And herein lies the beauty of extensions. They can be swapped in and out whenever you need. Perhaps you didn't need secondary indexes in version 1, but then version 2 comes along. No problem. The database system can set it up for you with just a few lines of code. In fact, extensions can be swapped in and out while the app is running, and while you're using the database. And you get all of this without worrying about making any changes to your objects, or how you store them.

Now that you've got the idea, learn more about YapDatabase by checking out the other wiki articles.