Brian Lorraine
Brian Lorraine

Reputation: 183

IndexedDB w/ two transactions: 1 read then 1 update

With IndexedDB, I can't seem to create a second transaction to my objectstore (just a list of ToDo's) once I've already created a transaction to read and display the data. I don't get an error on the line that tries to create the second transaction, but nothing happens and code stops execution.

This part that retrieves and displays my data runs fine:

var trans;
var ostore;
var db;
var reqdb = window.indexedDB.open("ToDoApp", 2);


reqdb.onupgradeneeded = function (event) {
    console.log("running onupgradeneeded");
    var myDB = event.target.result;
    if (!myDB.objectStoreNames.contains("todos")) {
        myDB.createObjectStore("todos", { keyPath: "ToDoTitle" });
    }
};


reqdb.onsuccess = function (event) {
    db = event.target.result;
    trans = db.transaction(["todos"], "readonly");
    ostore = trans.objectStore("todos");
    var req = ostore.get("TEST IDB 2");   
    $("#btnSave").click(function () {
        UpdateToDo();
    });
    req.onerror = function (event) {
        alert("error");
    };
    req.onsuccess = function (event) {
        $("#ToDoTitle").val(req.result.ToDoTitle);
    };
};

This gets and displays things just fine. But notice the UpdateToDo() function that gets set with the onclick event so I can actually UPDATE my data.

function UpdateToDo(event) {
    alert("1"); 
    var newtransaction = db.transaction(["todos"], "readwrite");
    alert("2");
    var newstore = newtransaction.objectStore("todos");

        newstore.openCursor().onsuccess = function (event) {
            const cursor = event.target.result;
            if (cursor) {
                if (cursor.value.ToDoTitle == 'TEST IDB 2') {       
                    const updateData = cursor.value;
                    updateData.ToDoCategory = 1; // hard coding for now
                    var requpdate = cursor.update(updateData);
                    requpdate.onsuccess = function () {
                        console.log('Updated');
                    };
                    requpdate.onerror = function () {
                        console.log('Error');
                    }
                };
                cursor.continue();
            } else {
                console.log('Cursor error.');
            }
        };
}

This first alert fires, but the second one doesn't. I assumed that because the first callback that create the first transaction was returned, that transaction was closed, but it still appears to be blocking me from creating this transaction. If I take out the first transaction COMPLETELY, the second transaction gets created and the second alert runs... then from there I can create a cursor and update the data.

I tried making the first transaction and object stores as global variables, but that doesn't work either.

It seems kind of ridiculous to only let you perform one transaction per page. How else am I suppose to initially load data and THEN allow the user to update it? Am I missing something?

Upvotes: 1

Views: 2021

Answers (2)

Josh
Josh

Reputation: 18690

Just a few points:

  • Not using alert together with indexedDb calls. indexedDB is asynchronous. alert is not asynchronous, it halts execution while displayed, which can lead to some strange behavior. It is better to use console.log because that does not halt execution.
  • Not mixing together code that does DOM modification and code that does database queries in the same function (or nested). I would organize your project code into smaller functions that just do one thing.
  • Not using a global database variable. Instead, open the database each time. Opening the database once, and then creating one or more transaction on click can work, but I would suggest instead you create the database connection each time as needed.
  • Consider using one transaction. You can issue both read and write requests on the same transaction.
  • Consider using async/await along with promises if your deployment environment supports it. If done correctly your code will become very readable.
  • If you are just updating one value, use IDBObjectStore.prototype.get instead of openCursor
  • When doing a write request in a readwrite txn, most of the time listening to the request success indicates success, but not always, so it is better to listen for the txn complete event.

For example, here is some pseudocode following some of the above suggestions:

function connect(name, version, upgrade) {
  return new Promise((resolve, reject) => {
    var req = indexedDB.open(name, version);
    req.onupgradeneeded = upgrade;
    req.onsuccess = event => resolve(req.result);
    req.onerror = event => reject(req.error);
  });
}

function onupgradeneeded(event) {
  var db = event.target.result;
  db.createObjectStore(...);
}

function updateTodo(db, todo) {
  return new Promise((resolve, reject) => {
    var tx = db.transaction('todos', 'readwrite');
    tx.onerror = event => reject(event.target.error);
    tx.oncomplete = resolve;
    var store = tx.objectStore('todos');
    var req = store.get(todo.title);
    req.onsuccess = event => {
      // When using get instead of openCursor, we just grab the result
      var old = event.target.result;

      // If we are updating we expect the current db value to exist
      // Do not confuse a get request succeeding with it actually matching 
      // something. onsuccess just means completed without error. 
      if(!old) {
        const error = new Error('Cannot find todo to update with title ' +  todo.title);
        reject(error);
        return;
      }

      // Change old todo object props
      old.category = todo.category;
      old.foo = todo.bar;

      // Replace old with a newer version of itself
      store.put(old);
   };
 });
}

function onpageloadInitStuff() {
  mybutton.onclick = handleClick;
}

async function handleClick(event) {
  var db;
  try {
    db = await connect('tododb', 1, upgradeneededfunc);
    var newtodo = todolist.querySelector('todolist[checked]');
    await updateTodo(db, newTodo);
  } catch(error) {
    alert(error); // it is ok to alert here, still better to console.log though
  } finally {
    if(db) {
      db.close();
    }
  }
}

Upvotes: 2

Brian Lorraine
Brian Lorraine

Reputation: 183

Iskandar and I had the same idea at the same time. In the UpdateToDo function, I opened a completely seperate connection to the IndexedDB instance and made my new transaction/store connect to that instance.. Working with that request worked

    function UpdateToDo() {
    var udb = window.indexedDB.open("ToDoApp", 2);
    alert("1"); 
    udb.onsuccess = function (event) {
        alert("2");
        db2 = event.target.result;
        var trans2 = db2.transaction(["todos"], "readwrite");
        var ostore2 = trans2.objectStore("todos");
        alert("3");
        var blabla = ostore2.openCursor();
    blabla.onsuccess = function (event2) {    

Coming from a MS Sql Server/ASP.NET background, opening multuiple connections to the same DB feels counter intuitive.

It seems IndexedDB and Cache API's weren't really stable on Safari until last year and service workers weren't even included on Safari at all until last year (2018). While Android has the global market, in North America, if Apple doesn't support it, there isn't much development... it sort of explains the last of documentation and tutorials for these new features. Good luck, everyone

Upvotes: 1

Related Questions