Статьи

IndexedDB на iOS 8 – сломан плохо


Позвольте мне
вначале сказать, что заслуга в этой находке принадлежит 
@jonnyknowsbest  в Твиттере и его посту SO здесь: 
Проблема первичного ключа при реализации IndexedDb на iOS8 . Я провела исследование по этому вопросу рано утром и надеюсь, что и я, и Джонни оба не правы. Я хотел бы быть неправым об этом. К сожалению, я не думаю, что это так.

Итак, как вы знаете, iOS 8 наконец-то принес IndexedDB в Mobile Safari. Я могу быть предвзятым, но нахожу такие функции гораздо более полезными, чем обновления CSS. Не сказать, что я их не ценю, но для меня глубокое хранение данных на клиенте — это то, что более практично и полезно для большего количества людей. Конечно, я работаю в компании, которая занимается дизайнерами, а не разработчиками, так что я знаю? 😉

К сожалению, кажется, что Apple, возможно, испортила их реализацию IndexedDB — и испортила это плохо. Как очень плохо. Если вы прочитаете пост SO, на который я ссылался выше, вы увидите, что он использовал назначенные идентификаторы и обнаружил, что если вы присвоили один и тот же идентификатор данным в двух хранилищах данных, то данные, вставленные в первое хранилище объектов, будут удалены. Позвольте мне повторить это, чтобы быть очевидным.

Представьте, что у вас есть два магазина объектов, люди и пиво. Вы хотите добавить объект к обоим, и в обоих случаях вы используете жестко закодированный первичный ключ 1. Когда вы это делаете, ошибка не выдается, но объект person удаляется. Осталось только пиво. (Не худший результат …) Вот полный пример, показывающий эту ошибку в действии.

<!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>

В этой демонстрации используется простая форма для запроса ПК. Когда вы нажимаете кнопку, он добавляет статического человека и объект заметки, используя значение, которое вы указали для PK. Когда вы запускаете это, ошибка не выдается. Обработчик успеха для обеих операций запущен. Но данные, которые вы создали для человека, исчезли. Это ужасно

Но ждать! Кто использует определенные первичные ключи? Только ботаны! Мне нравятся автоинкрементные клавиши, так почему бы просто не переключиться на это? Достаточно просто, верно? Я сделал новое демо, с новой базой данных, и изменил свои магазины объектов:

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

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

И та же самая чертова ошибка происходит. Я не шучу. Хорошо, хорошая iOS. Поэтому я попробовал что-то еще. Согласно  спецификации , вы можете создать транзакцию с несколькими объектами. Я подумал, может быть, если бы я сделал это, iOS справился бы со вставками лучше. Итак, давайте попробуем это:

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

Но это вызвало ошибку: DOM IDBDatabase Exception 8: Операция не выполнена, так как не удалось найти запрошенный объект базы данных.

Хорошо, теперь я подумал — а что если бы мы использовали autoIncrement и разные имена ключей? Возможно, одно и то же имя ключа сбивало с толку:

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

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

Нет, та же ошибка. Итак … наконец я сдался. Я указал идентификационный номер и поставил перед ним строку.

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");
	}
	
}

Это сработало. Конечно, у вас все еще есть отстой в создании ваших собственных ключей. Однако вы можете запросить размер хранилища объектов и просто увеличить их. Я написал новую версию, которая делает это. Кажется, это работает хорошо, и сейчас я бы порекомендовал. Он также отлично работает в Chrome, поэтому использование этого обходного пути не является «вредным».

<!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>

Я надеюсь, что это помогает людям. Как я уже сказал, может быть, я глуп и упускаю что-то очевидное. Я надеюсь, что это так. Но, учитывая, что iOS 8 также прервала загрузку файлов (как «обычную», так и через XHR2), неудивительно, что это также может быть прервано. Я собираюсь подать отчет об ошибке сейчас. Если их система отчетов поддерживает совместное использование URL, я сделаю это в комментарии.