For the past few days I've been working on a proof of concept PhoneGap application that demonstrates an example of database synchronization. This is a fairly complex topic and I'm only going to demonstrate one version of this, but I hope the concepts, code, and example application are useful to people hoping to tackle the same problem.
Before we start digging into code, let me explain what the application will demonstrate and the type of synchronization it will use. Our sample application is going to use a built-in database for documentation. Most mobile applications don't really have a lot of documentation. Anything that complex may not make sense on mobile anyway. But for whatever reason you want to use, the application has a large set of documents stored in the database.
This documentation may or may not ship with the application, but we want the application to sync with a remote server in order to get the latest and greatest documentation. Our synchronization "rules" will be simple:
- If the remote server adds new docs, the app needs to get it.
- If the remote server updates existing docs, the app needs to update its copy.
- If the remote server deletes a doc, the app needs to delete its local copy.
In this scenario, we don't have to worry about user generated content and handling updates bidirectionally. That allows us to simplify things quite a bit.
We have two pieces to this proof of concept, the server and the client. I'm going to quickly talk about the server-side code, but as this is mainly a post about PhoneGap, I won't go into deep detail. I built a very simple ColdFusion application using ORM to allow me to add, edit, and delete objects I called "helpdocuments". These documents contained:
- A title (short string)
- A body (not so short string)
- A field representing when the document was last updated
- A token (built around a UUID, more on why we need this later)
- A deleted flag (to support 'soft deletes')
You can find the code for this simple application attached to this blog entry. But really - it's just simple CRUD and web forms:
Outside of the service component I'll be using to expose this data remotely, you really don't have to worry about the details and forms here. Let's turn our attention to the client side now.
First off - do not forget that PhoneGap provides a wrapper to WebSQL, an embedded database you can use with JavaScript. You can check out the API docs as well as the video demonstration over on Adobe TV. My application will create a mirror version of the remote database to store a copy of the documentation. This involves:
- Creating the initial database
- Creating the empty table
- Asking the server for data, and later on, asking the server for new data
- Rendering the data (in our case, I'm just going to list the titles of the help docs)
Since the actual display is rather trivial, I'll show the HTML for the application first:
<!DOCTYPE html>
<html>
<head>
<title>PhoneGap SyncDB Test</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="cordova-1.5.0.js"></script>
<script type="text/javascript" src="main.js"></script>
</head>
<body onload="init()">
<div id="docs"></div>
</body>
</html>
Yeah, that's it. Obviously a real application would have a bit more meat to it. Let's take a look at main.js, the core logic file for the application. This one is a bit more complex so we're going to take it in chunks.
var db;
//var updateurl = "http://localhost/testingzone/dbsyncexample/serverbackend/service.cfc?method=getupdates&returnformat=json";
var updateurl = "http://www.raymondcamden.com/demos/2012/apr/3/serverbackend/service.cfc?method=getupdates&returnformat=json";
function init() {
document.addEventListener("deviceready",startup);
}
In the first block, we've got a few variable definitions. The db variable will contain a reference to my database. The updateurl is simply the remote server. (And by the way, that URL isn't going to work after I publish this article.) Our init function listens for PhoneGap's deviceready event. This is PhoneGap's way of saying, "It's Business Time."
function startup() {
console.log("Starting up...");
db = window.openDatabase("main","1","Main DB",1000000);
db.transaction(initDB,dbError,dbReady);
}
Here we have the startup function. It opens the database and begins a transaction. Note that I could have put the logic inside an anonymous function within the transaction call. You're going to see an example of that later. But in my opinion, the initialization routine would be complex enough to warrant moving it into its own function to keep things better organized. (It really isn't that complex, but I still feel like it was the right decision to make.)
Here's the error handler. Again - this would typically be more robust in a production environment:
function dbError(e) {
console.log("SQL ERROR");
console.dir(e);
}
Now let's look at initDB:
function initDB(tx) {
tx.executeSql("create table if not exists docs(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, lastupdated DATE, token TEXT)");
}
You can see my table creation scheme uses columns that match up with my server. The only column missing is the "soft delete" one. My thinking here was that while my server wants to keep deleted documents around so that they can (possibly) be restored later, my mobile application should try to stay as thin as possible.
This is a great time now to talk about the token column. Both my server and client side have it. Why? In order to keep documents in sync from one database server to my mobile applications, I need a way to uniquely identify records. I'm not even sure if the WebSQL client (SQLite) supports inserts with assigned primary keys, but in my mind, the easiest way to handle this was to simply let each "side" handle the primary keys their own way and use the UUID as a way to connect them. This could possibly be done better.
After our table creation script runs, we finally get to dbReady:
function dbReady() {
console.log("DB initialization done.");
//begin sync process
if(navigator.network && navigator.network.connection.type != Connection.NONE) syncDB();
else displayDocs();
}
As the comment says, this is where we begin our sync process. However, we only do this if the client is actually online. If it isn't, we immediately display our docs. On first run we won't have any, but after that we'll have something. This ensures that offline or online, we're going to support the user reading and interacting with the database.
Ok - now for the complex portion. Let's look at our synchronization function.
function syncDB() {
$("#docs").html("Refreshing documentation...");
I begin with a simple message letting the user know that important things are going on. Really important things.
var date = localStorage["lastdate"]?localStorage["lastdate"]:'';
console.log("Will get items after "+date);
Local storage? I thought we were using a database? We are! But there's no reason we can't use both. While working on this proof of concept I had an interesting problem. I want to minimize the amount of data going back and forth between the application and the server. To do that, my application needs to tell the server what its most recent update was. One way of doing that would be run SQL against itself and get the date on the last updated document. That works fine... except for deletes. If the most recent update on the server was to delete something, then the client's most recent date wouldn't match. As I said above, we aren't keeping those deleted records. So as a compromise, I simply used a value in localStorage. Later on you will see where this gets updated.
$.get(updateurl, {date:date}, function(resp,code) {
console.log("back from getting updates with "+resp.length + " items to process.");
Here's our server call. It basically pings the remote code and passes in a date. The server asks for all records modified since that date. The result is an array of objects.
resp.forEach(function(ob) {
db.transaction(function(ctx) {
ctx.executeSql("select id from docs where token = ?", [ob.token], function(tx,checkres) {
if(checkres.rows.length) {
console.log("possible update/delete");
if(!ob.deleted) {
console.log("updating "+ob.title+ " "+ob.lastupdated);
tx.executeSql("update docs set title=?,body=?,lastupdated=? where token=?", [ob.title,ob.body,ob.lastupdated,ob.token]);
} else {
console.log("deleting "+ob.title+ " "+ob.lastupdated);
tx.executeSql("delete from docs where token = ?", [ob.token]);
}
} else {
//only insert if not deleted
console.log("possible insert");
if(!ob.deleted) {
console.log("inserting "+ob.title+ " "+ob.lastupdated);
tx.executeSql("insert into docs(title,body,lastupdated,token) values(?,?,?,?)", [ob.title,ob.body,ob.lastupdated,ob.token]);
}
}
});
});
});
Holy smokes that's a lot! Let's take it piece by piece. We begin with a simple forEach iterator over the array. For each item, we need to see if already exists. That's done in the SQL that checks for the token.
If a record exists, then we either need to update it, or delete it. Remember that our server keeps deleted documents and simply flags them. You can see my check for that flag here:
if(!ob.deleted)
If the object wasn't deleted, we perform an update. If it was deleted, we perform a delete.
On the other side of the conditional, we only need to do an insert if the object isn't a delete.
Finally - we update our localStorage flag and call displayDocs too. Here's the display function - and again - this was built to be simple.
function displayDocs() {
db.transaction(function(tx) {
tx.executeSql("select title from docs order by title asc", [], function(tx, results) {
var s = "<h2>Docs</h2>";
for(var i=0; i<results.rows.length; i++) {
s += results.rows.item(i).title + "<br/>";
}
$("#docs").html(s);
});
});
}
The end result is an application that updates its data when online and continues to work fine offline. You can download the entire code base below. Any questions?
Archived Comments
Sorry for the lack of tabbing above folks. Not sure why. I used Sublime Text 2 for all of my work and I'm assuming something pasted wrong.
Oh! Very nice! Thanks for this example!
There's a lot to digest here, and I thank you as always for taking the time to blog. One thing I noticed is that during development, it might be nice to drop the table in initDB before creating it. Just in case you change the schema. It's not as trivial as one would think because (I think) you have to wait for the callback before creating it again.
@Phillip: Dude... get this. I removed this line from my blog post:
//tx.executeSql("drop table docs");
That's _exactly_ what I was doing during testing.
I use the following to test for whether their online or not:
if (navigator.onLine) {
$('#myOnline').text('Your Internet status is: online.');
setTimeout(Sync,3000); // Don't sync until they've paused for a few seconds.
} else {
$('#myOnline').text('Your Internet status is: offline.');
}
window.addEventListener("offline", function(e) {
$('#myOnline').text('Your new Internet status is: offline.');
}, false);
window.addEventListener("online", function(e) {
$('#myOnline').text('Your new Internet status is: online.');
Sync(); // This will start the sync the moment they walk back into Wi-Fi.
}, false);
I tried navigator.onLine, but on my device, it reported me as online while in Airplane Mode.
http://tv.adobe.com/watch/a...
Thanks very much Ray. Amazed not more questions on this great post. On first run the table creates fine but I don't have the last update date in my local storage. I know there hasn't been an update yet so do I have to create it manually to start things off? I am using Chrome on the desktop and can see other local storage with the JavaScript tool. I have replaced the server link providing the JSON
Hmm. So the client-side code should be making the request and passing an empty date string, which should make the server return everything. Can you add some debugging (simple writeLogs) to the CFC to see if it is doing that?
Oh - and this may not be obvious - but did you _write_ some docs using the server-side code first?
All I have done is to change the url of the cfc to http://wildlifehero.com/api.... I used your API example of a while back to create the JSON from an existing table. So I assume the date format is not an issue. I don't see any call to the cfc in the Chrome Network tab - not sure if I should. If you can confirm the JSON is ok I will add some looking as you suggested. Thanks
When I run your URL I get a file not found error.
sorry I missed out the www in the post. It is in my code http://www.wildlifehero.com... Thanks
Terry, that looks right to me. At this point, I'd continue to debug. In my code, I have this:
console.log("back from getting updates with "+resp.length + " items to process.");
Try quickly changing it to an alert, rerunning, and tell me if you see a msg? (console.log is nicer, but if you don't know how to see it - this is quicker for you)
executeSql uses an anonymous callback function, the purpose of the callback being that it may take some time before the result is retrieved... so it's possible that displayDocs() is called before all entries are inserted?
I believe you are right. I would most likely rewrite that logic to make use of jQuery deferreds such that you can say, "When you have done these N asynch ops, then do X", where X is displayDocs.
hai raimond... can i use xui to synch local db and server in phonegap???
I assume you mean as a replacement for jQuery. If so, yes. I prefer Zepto myself.
any good documentation or tutorial related to this topic??? i mean sync localdb with zepto.. btw.. thanks..
I'm not sure I understand you. Zepto, like jQuery, is a library of utilities. Zepto is built to be very similar to jQuery. Take a look at the docs and you will see this. A separate demo would't make much sense.
thank you so much... i'm sorry.. cause i'm newbie.. :)
i am connecting database using phone gap but it giving error can't access the variable
01-08 16:09:59.350: E/Web Console(326): ReferenceError: Can't find variable: AddValueToDB at file:///android_asset/www/b.html:205
I'd have to see your code. Is AddValueToDB something you wrote?
Hello,
This is a very helpful article, but I didn't know if there was any easy way to change the server side coding to php? I currently have some php code and would love to integrate this with it. If not, what is the format of the JSON output and I can rebuild a php version? Thanks!
The JSON can be seen by using the URL I used in the code:
http://www.raymondcamden.co...
Great, thank you. Is there a specific way to generate the token? Or is there a reason it is like this? Thanks again.
I generated the data like that because - it just made sense. :) There isn't a required reason though.
Waw, thanks for this example
Hello Raymond,
Firstly thank you for this excellent tutorial. I was wondering where the CRUD forms were attached? Sorry as i am unable to find it.
Thank you
Hi Raymond,
I have an app already deployed. I now want to upgrade the app with some schema changes to the existing database. How can I achieve it without losing data from the original database? One way I thought of was creating a new database and copying contents from the original database. I haven't tried it - just an idea.
Please let me know how I can achieve this.
What does db.changeVersion(); do in Phonegap?
I wouldn't create a new db, I'd just script the table adds. Remember your SQL can do "create table if not exists", so that can be used to safely add the new ones.
@Akash: Everything should be in the download zip.
Great article! Thanks for sharing, this really got my to a flying start with what I thought would be a tedious task.
I had to add some operations to go "the other way" as in sending data that has been modified on the device back to the server. I'm sure based on your code anybody can get this done in a few minutes, but still if anybody wants to save a bit of coding, here's my solution:
http://pastebin.com/SUsfahYR
comments welcome, as I am no JS expert ;)
Thanks again for a great article, all the best!
This is nice, Szymon - thanks for sharing it!
Hi there, I am getting the error XMLHttpRequest cannot load... localhost ... Origin www.server... is not allowed by Access-Control-Allow-Origin.
Do you know whay I am getting this when tryng to sync from my computer to the server?
Thanks
Alex
Because web browsers can't make XHR (AJAX) requests to other servers. There are workarounds (CORS, JSON/P), but in general, you are blocked. PhoneGap removed this restriction.
So you need to see if your server supports JSON/P or CORS. You can also try Ripple, but Ripple is a bit rough at the moment.
Thanks for this example Raymond.
Nowadays, who is the best option to synchronize offline data with my online server in Phonegap?? I've heard something like couchbase but there arent docs about it. I am using Phonegap 3.0 to my example app. Thanks
Well, I'm not sure I can say what is "best". I spent most of my dev career using ColdFusion, and CF has very good support as a client for XHR requests. You could also use something like Parse.com. (I've done many blog posts on it.)
I got this (just on the client side, the server side works great, I wrote some docs in there), It doesn't show anything. I need assistance, please.
http://minus.com/i/tInX2G7r...
Um, you need to be a bit more clear about what is wrong. A screen shot isn't enough.
The code works exactly the same way as you shared it, with the only difference that uses my localhost, In the server side It works perfectly, with the Doc Editor and everything, I can add content, edit it with no problems, the real issue is on the client side when the app tries to get the documents, it doesn't show neither the table nor the docs.
Best I can suggest is debugging. Have you tried running the code in a desktop browser?
Yes, I run the code in Chrome, but still, I can't figure out what's the problem.
Does it fail the same in Chrome is what I mean? If so - please look into your dev tools to diagnose the issue.
Hello,
I am trying to implement synchronization in my app. I am having doubt that when I connect to internet how will my app will know that internet is connected so it can fire sync event in background without opening my application.
Thanks
Gaurav
You can't, as far as I know. You would have to do the sync next time the user opens the app.
Thanks for instant reply Raymond. Do you know any other way to do that in Cordova Apps. As I think this is a necessary thing (as application like gmail, twitter use background sync.)
Not that I know of. I know there has been some work on 'background plugins' (see the plugins site), but I don't know what state that works is in.
Not sure how your app will know but the user could be alerted of an update with a push notification
Post is nice. however I am curious to know what do you think about my solution.
https://www.digitoffee.com/...
I have also added jsfiddle demo in that post.
Looks nice, Bharat, thanks for sharing it.
Script works fine up until populating the noSQL db. Initially it says its inserting but it doesnt. Wondering why? My table is always empty.
When you debugged, what did you see? Do you see an error in the console?
I get this screen. This is my webSQL db: http://i239.photobucket.com...
I get this in my console. Says date is = null.
http://i239.photobucket.com...
So, it says it is back from getting 28 items. Is resp.forEach running? Maybe on the line after forEach, add console.log("doing an item");
Confirm you see 28 of them.
foreach seems to be working but no values in my db: http://i239.photobucket.com...
weird!
Can you put it online where I can run it?
Can i email or inbox you?
Sure - I'm on vacation this week so may be slow.
Ok... i sent you an email with an example and a jsfiddle to have a look at. I can tell its something simple because it gets the values, it just doesnt insert. I've looked at it 100times.
Thanks Raymond for this article. Im a newbie in this and looking for a MySql backend for the DB on a remote server. How would you advice me to customize your setup. Thanks.
The back end db for this isn't doing anything complete. It's just one table of content. I didn't build a real DB even just used a ColdFusion feature to create a fake query-like object. Maybe if you explain what you tried I can help more.
Thanks for the clarification. I'm actually interested in offline data collection - having the phonegap app work offline, store the data on websql or other html5 storage options and sync the collected data with a MySQL backend on a server. I also want the app to pull some updated tables from the Server and use them as lookups in my data collection form. In a nutshell I'm looking a limited 2-directional syncronization...
Thanks for your help an reply Raymond I was able to get it working with a few tweets.
Oh ok - cool. I'm so taking credit even though I didn't do anything. :)
Great script btw!
Hi,
how you trigger a function only after the loop which populates the table is finished? I understand each iteration is async, right?
e.g.
for(i=0;i<100;1++){
tx.executeSql("INSERT INTO table1 (id) VALUES (?)",[i]);
}
// do something after everything is inserted
Use a transaction. Then you can execute code when the entire thing is complete.
Jan,
I've pasted some code here:
http://jsfiddle.net/Phillip...
This is how I did it a few months ago.
I've since moved to Railo but I hope you can glean some info from looking at how I accomplished the same thing back when.
Thank you for this great article!
I have one problem with the dates (lastupdated):
I rebuild the serverbackend in PHP / MySQL and syncing (insert / update / delete) works fine. But it is *updating all records every time*, though I didn’t make any changes in the serverbackend.
My date format in both db tables and the local storage is 0000-00-00 00:00:00. Is that correct or must it be another format?
I'd begin by doing some console.logging to see if the time thing is a problem. For example, look at your db value on the client side versus the server. For two values that are supposed to be the same, are they not? And if not, *how* are they different?
Just checked with your original date format (April, 03 2012 15:08:50) and it is the same behavior, it is updating all records, not just the changed ones.
I thought, there is a date comparison to just update records with newer dates.
Doesn't matter. In my real app, I will have only one lastupdate field to sync the whole database or something like that. Thx!
Naviagator.connection.type return 0 for cordova 3.5 , Please tell how to solve it??
What platform?
I'd like to put your photo on the wall in my rooom.
Really usefull info for beginers like me! Thanks a lot!
Heh, that's not creepy at all. ;)
Whats the best method to redirect after sync is completely finished inserting?
It depends. If you are using Angular then the $location service makes it simple. jQuery Mobile has a way to load a new view too.
Thank you for this great article!
But in first time i am seen all data .After that i am delete browse data.After that i cant access data. in console log shows.
Starting up...
table created
DB initialization done.
Will get items after April, 03 2012 15:08:50
and nothing it shoes
So it says it will get the items but it does not - what does the Network tab show in Dev Tools?
Here is the code console log shows
"back from getting updates with 0 items to process. "
syncDB();
if(navigator.network && navigator.network.connection.type != Connection.NONE) syncDB();
else displayDocs();
}
function syncDB() {
$("#docs").html("Refreshing documentation...");
console.log("inside");
var date = localStorage["lastdate"]?localStorage["lastdate"]:'';
console.log("Will get items after "+date);
$.get(updateurl, {date:date}, function(resp,code) {
console.log("back from getting updates with "+resp.length + " items to process.");
//Ok, loop through. For each one, we see if it exists, and if so, we update/delete it
//If it doesn't exist, straight insert
resp.forEach(function(ob) {
console.log("we");
db.transaction(function(ctx) {
console.log("back");
ctx.executeSql("select id from docs where token = ?", [ob.token], function(tx,checkres) {
if(checkres.rows.length) {
console.log("possible update/delete");
if(!ob.deleted) {
console.log("updating "+ob.title+ " "+ob.lastupdated);
tx.executeSql("update docs set title=?,body=?,lastupdated=? where token=?", [ob.title,ob.body,ob.lastupdated,ob.token]);
} else {
console.log("deleting "+ob.title+ " "+ob.lastupdated);
tx.executeSql("delete from docs where token = ?", [ob.token]);
}
} else {
//only insert if not deleted
console.log("possible insert");
if(!ob.deleted) {
console.log("inserting "+ob.title+ " "+ob.lastupdated);
tx.executeSql("insert into docs(title,body,lastupdated,token) values(?,?,?,?)", [ob.title,ob.body,ob.lastupdated,ob.token]);
}
}
});
});
});
//if we had anything, last value is most recent
if(resp.length) localStorage["lastdate"] = resp[resp.length-1].lastupdated;
displayDocs();
},"json");
}
You said you deleted data - did you clear the localStorage value used to mark the date?
I have 5 tables to sync, what is the best solution to get the latest date of update?
For every table I can get the latest date with
if(resp.length) var lastdate = resp[resp.length-1].last_update;
But how can I store them to sort and get the latest? I tried with an array, but I can't get this array out of the sync function (because of ajax?). Do I need a callback and how to do it?
Why not use the same process for each? They will be asynch but it will work. If you need to ensure ALL have checked and ALL have updated (if need be), look into using Promises to help manage it.
We need a Windows Mobile app on a Motorola device that will collect data remotely and then sync to an Access db when it is connected to its host PC. Is this a standard feature in the PhoneGap environment or is this always a "do it yourself" solution?
Well syncing via AJAX is easy - connecting to an Access db on a client machine would need a custom plugin.
did you solve this problem??