blob: f53ac5d0afdc4abeaa02af32a203f874fba48c90 [file] [log] [blame] [view]
---
layout: post
title: Prebuilt databases with PouchDB
author: Nolan Lawson
---
A common scenario when writing an app is that you have some data in a database,
and you want it to be immediately available when the app starts up. Your users
might be able to modify the data once it's loaded, but either way, they should
be able to quickly start from a predetermined state.
PouchDB offers two ways to do this:
1. As a prebuilt SQLite file (appropriate for hybrid apps)
2. As a "dump" file (appropriate for web apps)
In this tutorial, I'll give step-by-step instructions for both techniques.
Prebuilt SQLite files
---
{% include alert/start.html variant="info" %}
{% markdown %}
**Note:** this method only works for hybrid apps, i.e. apps built with tools like Cordova,
PhoneGap, and Ionic.
{% endmarkdown %}
{% include alert/end.html %}
The advantage of bundling a SQLite file with your app is that the database is instantly
available. There's no need to process any data to convert it to a usable format; the SQLite
file is already ready for PouchDB to use.
### Creating the SQLite file
To start, we'll need some data. Let's say I have a CouchDB or a PouchDB Server that looks like this:
{% include img.html src="turtles_db.png" alt="A database with some Teenage Mutant Ninja Turtles" %}
We have four documents in this database, which I've named `turtles`.
Next, we need to convert this data into a SQLite file. If we started PouchDB Server like so:
```bash
$ pouchdb-server --sqlite
```
Then we're already done. There will be a file called `turtles` in the directory where we ran PouchDB Server, which is already a SQLite file.
You can verify this for yourself by opening the file with the `sqlite3` command:
```
$ sqlite3 turtles
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite> .tables
attach-seq-store by-sequence local-store
attach-store document-store metadata-store
sqlite> select * from 'by-sequence';
1|{"name":"Donatello","weapon":"bo","bandana":"purple"}|0|donatello|1-c2f9e6a91b946fb378d53c6a4dd6eaa2
2|{"name":"Leonardo","weapon":"katana","bandana":"blue"}|0|leonardo|1-c95202ca170be0318d085b33528f7995
3|{"name":"Michelangelo","weapon":"nunchaku","bandana":"orange"}|0|michelangelo|1-52ebc5a2f8dbc0dc247cd87213e742d1
4|{"name":"Raphael","weapon":"sai","bandana":"red"}|0|raphael|1-77812e9da146bc18a37e51efb063dbac
```
However, if you're not using PouchDB Server with the `--sqlite` option, then we'll need to use a more generic approach, which
can copy any CouchDB or PouchDB database to a SQLite file.
So let's write a short Node script to do that:
```js
// load PouchDB with the optional node-websql adapter
var PouchDB = require('pouchdb').plugin(require('pouchdb-adapter-node-websql'));
// set up our databases - make sure the URL is correct!
var inputDB = new PouchDB('http://localhost:5984/turtles');
var outputDB = new PouchDB('turtles.db', {adapter: 'websql'});
// replicate
inputDB.replicate.to(outputDB);
```
Let's call this script `dump.js`.
Now, if you haven't already, you'll need to install PouchDB and the [pouchdb-adapter-node-websql](https://www.npmjs.com/package/pouchdb-adapter-node-websql) plugin in the directory where we'll run the script:
```bash
$ npm install pouchdb
$ npm install pouchdb-adapter-node-websql
```
Next, we'll run our script using:
```bash
$ node dump.js
```
If this works, you will have a file called `turtles.db` in the directory where you ran the script. (We're adding the `.db` extension, which is not strictly necessary, but is a common convention for SQLite files.)
You can still inspect the database file using the `sqlite3` command, as described above. Note that the tables
were built by PouchDB, though, and are not meant to be modified by hand.
### Bundling the SQLite file in your app
Now we'll want to bundle the SQLite database file in our app. To do so, we'll use
[SQLite Plugin 2](https://github.com/nolanlawson/cordova-plugin-sqlite-2) and follow
[the instructions for creating prepopulated databases](https://github.com/nolanlawson/cordova-plugin-sqlite-2#how-do-i-create-a-prepopulated-database).
If you don't have it already, install the `cordova` tool:
```bash
$ npm install -g cordova
```
Then create a new Cordova project:
```bash
$ cordova create pouchdb-prebuilt-demo
```
Next, we'll want to copy the `turtles.db` into the `www/` directory. This will ensure that the file
is bundled with the app.
{% include alert/start.html variant="warning" %}
{% markdown %}
**PouchDB 5.x instructions:** Before PouchDB 6, SQLite databases need to be
prefixed with `_pouch_` in order to be located. So you would need to rename
`turtles.db` to `_pouch_turtles.db`. This is not necessary in PouchDB 6+.
{% endmarkdown %}
{% include alert/end.html %}
At this point, we'll also need to install some plugins for our Cordova app. In particular, we'll need the [File Plugin](https://github.com/apache/cordova-plugin-file)
and [SQLite Plugin 2](https://github.com/nolanlawson/cordova-plugin-sqlite-2). Inside of the `pouchdb-prebuilt-demo` directory, run:
```bash
$ cordova plugin add cordova-plugin-file --save
$ cordova plugin add cordova-plugin-sqlite-2 --save
```
(For Ionic, you can also do `ionic plugin add`, and it will have the same effect.)
We'll also need PouchDB itself. For expediency, let's just [download PouchDB](http://pouchdb.com/download.html)
and include `pouchdb.js` in the app, under `www/js`. Then we'll add it to `index.html`:
```html
<script src="js/pouchdb.js"></script>
```
Next, we'll need to write some code to copy the `turtles.db` file at runtime from the read-only `www/` directory
to a read-write directory. This is the only cost we pay at startup when using this approach, but it allows us to
modify the database after it's been loaded.
So let's write a helper function to do this:
```js
// copy a database file from www/ in the app directory to the data directory
function copyDatabaseFile(dbName) {
var sourceFileName = cordova.file.applicationDirectory + 'www/' + dbName;
var targetDirName = cordova.file.dataDirectory;
// resolve the source and target filenames simultaneously
return Promise.all([
new Promise(function (resolve, reject) {
resolveLocalFileSystemURL(sourceFileName, resolve, reject);
}),
new Promise(function (resolve, reject) {
resolveLocalFileSystemURL(targetDirName, resolve, reject);
})
]).then(function (files) {
var sourceFile = files[0];
var targetDir = files[1];
// try to fetch the target file, to check if it exists
return new Promise(function (resolve, reject) {
targetDir.getFile(dbName, {}, resolve, reject);
}).catch(function () {
// target file doesn't exist already, so copy it
return new Promise(function (resolve, reject) {
sourceFile.copyTo(targetDir, dbName, resolve, reject);
});
});
});
}
```
This uses the standard Cordova [File APIs](https://github.com/apache/cordova-plugin-file) – such as `getFile()` and `copyTo()` – while adding Promises to make it a bit more easy to use. (Note that you will need a Promise shim if you are targeting older devices.)
It also checks whether the target file already exists, ensuring that the database is only ever copied once.
Next, we'll need to include a plugin, [pouchdb-adapter-cordova-sqlite(https://github.com/nolanlawson/pouchdb-adapter-cordova-sqlite)]. This plugin will allow us to use native SQLite rather than WebSQL (which doesn't support prebuilt databases).
To include this plugin, either `npm install pouchdb-adapter-cordova-sqlite` or [directly download it](https://unpkg.com/pouchdb-adapter-cordova-sqlite/dist/pouchdb.cordova-sqlite.js), then link to it:
```html
<script src="js/pouchdb.js"></script>
<script src="js/pouchdb.cordova-sqlite.js"></script>
```
{% include alert/start.html variant="info" %}
{% markdown %}
**PouchDB 5.x instructions:** Before PouchDB 6, the WebSQL adapter would automatically fall back to SQLite if it detected it was running in a Cordova environment and the Cordova SQLite plugin was installed. This behavior [was removed](https://github.com/pouchdb/pouchdb/wiki/Breaking-changes) because it was difficult to maintain and configure.
{% endmarkdown %}
{% include alert/end.html %}
Next, let's write the code to call this function and then print the contents of our preloaded PouchDB. Add this code to `www/js/index.js`, ensuring that it runs after the `deviceready` event:
```js
copyDatabaseFile('turtles.db').then(function () {
// using the Cordova SQLite plugin. Make sure this plugin is loaded correctly!
var db = new PouchDB('turtles.db', {adapter: 'cordova-sqlite'});
return db.allDocs({include_docs: true});
}).then(function (results) {
var pre = document.createElement('pre');
pre.innerHTML = JSON.stringify(results, null, ' ');
document.body.appendChild(pre);
}).catch(console.log.bind(console));
```
Next, to run the app on Android, we can do:
```bash
$ cordova platform add android
$ cordova run android
```
Or on iOS:
```bash
$ cordova platform add ios
$ cordova run ios
```
If all goes well, you should see the following screens for Android and iOS:
{% include img.html src="prebuilt.png" alt="Prebuilt database app screenshot on iOS and Android" %}
The text in the background indicates that the database was preloaded and ready to go!
{% include alert/start.html variant="info" %}
{% markdown %}
**The full source code** for this example is available [on Github](https://github.com/nolanlawson/pouchdb-prebuilt-demo).
{% endmarkdown %}
{% include alert/end.html %}
Loading from a dump file
---
On the web, we don't have the luxury of prebuilt SQLite files, because WebSQL
isn't supported in every browser, and even those browsers that have WebSQL don't support prebuilt files.
However, we can dump any PouchDB or CouchDB to a "dump file" using [pouchdb-dump-cli](https://github.com/nolanlawson/pouchdb-dump-cli).
This is less efficient than the prebuilt database (since PouchDB has to process the
file and write the data to IndexedDB/WebSQL), but it gets the job done.
Going back to our "turtles" example, let's use `pouchdb-dump` to dump the contents
of the database to a plaintext dump file. First, install `pouchdb-dump-cli`:
```bash
$ npm install -g pouchdb-dump-cli
```
Then dump the database:
```bash
$ pouchdb-dump http://localhost:5984/turtles > turtles.txt
```
Now that we have a `turtles.txt` file, we can use the [pouchdb-load](https://github.com/nolanlawson/pouchdb-load)
plugin to load it into our database.
{% include alert/start.html variant="info" %}
{% markdown %}
**Note:** if you'd like to make this dump file even smaller, you can use [short-revs](https://github.com/nolanlawson/short-revs)
to shorten all of the revision strings to `1-A`, `1-B`, `2-A`, etc. Be forewarned that this will
make it incompatible with the source database, though, so you won't be able to replicate it back!
{% endmarkdown %}
{% include alert/end.html %}
Assuming the dump file is hosted on our web site, we can then load it using `db.load()`:
```html
<pre id="display"></pre>
<script src="pouchdb.js"></script>
<script src="pouchdb.load.js"></script>
<script>
var db = new PouchDB('turtles');
// check a local document to see if we've already preloaded
db.get('_local/preloaded').then(function (doc) {
}).catch(function (err) {
if (err.name !== 'not_found') {
throw err;
}
// we got a 404, so the local docuent doesn't exist. so let's preload!
return db.load('turtles.txt').then(function () {
// create the local document to note that we've preloaded
return db.put({_id: '_local/preloaded'});
});
}).then(function () {
return db.allDocs({include_docs: true});
}).then(function (res) {
display.innerHTML = JSON.stringify(res, null, ' ');
}).catch(console.log.bind(console));
</script>
```
This will fetch the `turtles.txt` file via AJAX and then load it into PouchDB. A special `_local` document
lets us know if we've already loaded it, which means we can skip this step on subsequent views.
If this works, you should see the four turtle documents printed to the screen, since they
were successfully loaded from the dump file.
{% include alert/start.html variant="info" %}
{% markdown %}
**The full source code** for this example is available in [this Gist](https://gist.github.com/nolanlawson/5100ca90dc1028d811a2b5c73119e323). And here's [a live version](http://bl.ocks.org/nolanlawson/5100ca90dc1028d811a2b5c73119e323).
{% endmarkdown %}
{% include alert/end.html %}
Wrapping up
---
Prebuilding a database can be a nice way to speed up the initial load of your
app or web site. However, it is an optimization, so it takes a little bit of
extra work to get it up and running.
For the average use case, if you can avoid any complicated prebuild steps and simply
replicate between CouchDB and PouchDB via the normal `replicate()` and `sync()` APIs,
you should. In fact, CouchDB's replication is slated to speed up dramatically
with v2.0 and v1.7, thanks to the new [\_bulk\_get endpoint](https://issues.apache.org/jira/browse/COUCHDB-2310).
When `_bulk_get` lands, you may find that `pouchdb-load` isn't significantly
more performant than just replicating directly from CouchDB. (Prebuilt SQLite files, however, are likely
to remain much more performant.) Also note that PouchDB's replicator will automatically switch to the
faster algorithm if it detects that the server supports `_bulk_get`.
Hopefully this tutorial was useful for anybody struggling to get preloaded PouchDB databases into their app.
If not, feel free to [get in touch](https://github.com/pouchdb/pouchdb/blob/master/CONTRIBUTING.md#get-in-touch) anytime!