DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
View Events Video Library
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Integrating PostgreSQL Databases with ANF: Join this workshop to learn how to create a PostgreSQL server using Instaclustr’s managed service

Mobile Database Essentials: Assess data needs, storage requirements, and more when leveraging databases for cloud and edge applications.

Monitoring and Observability for LLMs: Datadog and Google Cloud discuss how to achieve optimal AI model performance.

Automated Testing: The latest on architecture, TDD, and the benefits of AI and low-code tools.

Trending

  • The Systemic Process of Debugging
  • Implementing Stronger RBAC and Multitenancy in Kubernetes Using Istio
  • Performance Optimization Strategies in Highly Scalable Systems
  • Understanding Europe's Cyber Resilience Act and What It Means for You

IndexedDB on iOS 8 - Broken Bad

Raymond Camden user avatar by
Raymond Camden
·
Sep. 26, 14 · Interview
Like (0)
Save
Tweet
Share
5.56K Views

Join the DZone community and get the full member experience.

Join For Free
Let me begin by saying that credit for this find goes to @jonnyknowsbest on Twitter and his SO post here: Primary Key issue on iOS8 implementation of IndexedDb. I did my research into this issue early this morning and I hope that I, and jonny, are both wrong. I'd love to be wrong about this. Unfortunately, I don't think that is the case.

So, as you know, iOS 8 finally brought IndexedDB to Mobile Safari. I may be biased, but I find features like this far more useful than CSS updates. Not to say that I don't appreciate them, but to me, deep data storage on the client is something that is more practical and useful to more people. Of course, I work for a company that is all about designers and not developers, so what do I know? ;)

Unfortunately, it seems as if Apple may have screwed up their implementation of IndexedDB - and screwed it up bad. Like real bad. If you read the SO post I linked to above, you will see that he was using assigned IDs and discovered that if you assigned the same ID to data in two datastores, then the data inserted in the first objectstore is removed. Let me restate that just to be obvious.

Imagine you have two object stores, people and beer. You want to add an object to both, and in both cases, you use a hard coded primary key of 1. When you do, no error is thrown, but the person object is deleted. Only beer remains. (Not the worst result...) Here is a full example showing this bug in action.

<!doctype html>
<html>
<head>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
</head>
    
<body>

<script>
var db;

function indexedDBOk() {
	return "indexedDB" in window;
}

document.addEventListener("DOMContentLoaded", function() {

	//No support? Go in the corner and pout.
	if(!indexedDBOk) return;

	var openRequest = indexedDB.open("ios8b",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;

		console.log("running onupgradeneeded");

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {keyPath:"id"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {keyPath:"uid"});
		}

	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");

		db = e.target.result;

		console.log("Current Object Stores");
		console.dir(db.objectStoreNames);

		//Listen for add clicks
		document.querySelector("#addButton").addEventListener("click", addPerson, false);
	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}


},false);


function addPerson(e) {
	console.log("About to add person and note");

	var id = Number(document.querySelector("#key").value);
	
	//Get a transaction
	//default for OS list is all, default for type is read
	var transaction = db.transaction(["people"],"readwrite");
	//Ask for the objectStore
	var store = transaction.objectStore("people");

	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
		id:id
	}

	//Perform the add
	var request = store.add(person);

	request.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
		uid:id
	}

	var transaction2 = db.transaction(["notes"],"readwrite");
	//Ask for the objectStore
	var store2 = transaction2.objectStore("notes");

	//Perform the add
	var request2 = store2.add(note);

	request2.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request2.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
}
</script>

enter key: <input id="key"><br/>
<button id="addButton">Add Data</button>

</body>
</html>

This demo uses a simple form to ask you for a PK. When you click the button, it then adds a static person and note object using the value you gave for a PK. When you run this, no error is thrown. The success handler for both operations is run. But the data you created for the person is gone. This is horrible.

But wait! Who uses defined primary keys? Only nerds! I like auto incrementing keys, so why not just switch to that? Simple enough, right? I made a new demo, with a new database, and modified my objectstores:

if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {autoIncrement:true});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {autoIncrement:true});
		}

And the same damn error occurs. I kid you not. Ok, fine iOS. So I then tried something else. According to the spec, you can create a transaction with multiple objectstores. I thought, maybe if I did that, iOS would handle the inserts better. So let's try this:

var transaction = db.transaction(["people","notes"],"readwrite");

But this threw an error: DOM IDBDatabase Exception 8: An operation failed because the requested database object could not be found.

Ok, so next I thought - what if we used autoIncrement and different key names. Maybe the key name being the same was confusing things:

if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {autoIncrement:true,keyPath:"appleisshit"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {autoIncrement:true,keyPath:"id"});
		}

Nope, same error. So... finally I gave up. I specified an ID number and prefixed it with a string.

function addPerson(e) {
	console.log("About to add person and note");

	var id = document.querySelector("#key").value;
	
	//Get a transaction
	//default for OS list is all, default for type is read
	var transaction = db.transaction(["people"],"readwrite");
	//Ask for the objectStore
	var store = transaction.objectStore("people");

	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
		id:"people/"+id
	}

	//Perform the add
	var request = store.add(person);

	request.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
		uid:"notes/"+id
	}

	var transaction2 = db.transaction(["notes"],"readwrite");
	//Ask for the objectStore
	var store2 = transaction2.objectStore("notes");

	//Perform the add
	var request2 = store2.add(note);

	request2.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request2.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
}

This worked. Of course, you still have the suck part of creating your own keys. You can, however, ask the objectstore for the size and simply increment yourself. I wrote up a new version that does this. This seems to work well and for now is what I'd recommend. It works fine in Chrome too so it isn't "harmful" to use this workaround.

<!doctype html>
<html>
<head>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
</head>
    
<body>

<script>
var db;

function indexedDBOk() {
	return "indexedDB" in window;
}

document.addEventListener("DOMContentLoaded", function() {

	//No support? Go in the corner and pout.
	if(!indexedDBOk) return;

	var openRequest = indexedDB.open("ios8_final3",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;

		console.log("running onupgradeneeded");

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {keyPath:"id"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {keyPath:"uid"});
		}

	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");

		db = e.target.result;

		console.log("Current Object Stores");
		console.dir(db.objectStoreNames);

		//Listen for add clicks
		document.querySelector("#addButton").addEventListener("click", addPerson, false);
	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}


},false);


function addPerson(e) {
	console.log("About to add person and note");


	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
	}
	
	//Perform the add
	db.transaction(["people"],"readwrite").objectStore("people").count().onsuccess = function(event) {
		var total = event.target.result;
		console.log(total);
		person.id = "person/" + (total+1);
		
		var request = db.transaction(["people"],"readwrite").objectStore("people").add(person);
		
		request.onerror = function(e) {
			console.log("Error",e.target.error.name);
			//some type of error handler
		}

		request.onsuccess = function(e) {
			console.log("Woot! Did it");
		}

	}

	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
	}

	db.transaction(["notes"],"readwrite").objectStore("notes").count().onsuccess = function(event) {
		var total = event.target.result;
		console.log(total);
		note.uid = "notes/" + (total+1);
		
		var request = db.transaction(["notes"],"readwrite").objectStore("notes").add(note);
		
		request.onerror = function(e) {
			console.log("Error",e.target.error.name);
			//some type of error handler
		}

		request.onsuccess = function(e) {
			console.log("Woot! Did it");
		}

	}
	
}
</script>

<button id="addButton">Add Data</button>

</body>
</html>

I hope this helps folks. As I said, maybe I'm being stupid and missing something obvious. I hope so. But considering that iOS 8 also broke file uploads (both "regular" and via XHR2), it isn't too surprising that this could be broken as well. I'm going to file a bug report now. If their reporting system supports sharing the URL, I'll do so in a comment.


IOS 8

Published at DZone with permission of Raymond Camden, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.


Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: