Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Result Count and Iteration

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Collection Count

We can get the number of results from a collection with the count method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "mary",
    age: 76
  });

  const count = await db.friends
    .where("name")
    .equalsIgnoreCase("jane")
    .count()
  console.log(count)
})()

to get the query results and then call count on it to get the count of the results.

Delete Items

We can call the delete all objects in the query with the delete method.

For instance, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "mary",
    age: 76
  });

  await db.friends
    .where("name")
    .equalsIgnoreCase("jane")
    .delete()
})()

We just call delete on the collection to delete all the items in the collection.

Sort Descending

We can sort items in descending order with the desc method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "mary",
    age: 76
  });

  const someFriends = await db.friends
    .where('age')
    .above(25)
    .desc()
    .toArray()
  console.log(someFriends)
})()

to sort the items in descending order.

Each

We can loop through any items returned in a collection with the each method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "jane",
    age: 76
  });

  await db.friends.each(friend => console.log(friend.name));
})()

to call each to loop through each item in the friends table.

Loop Through Keys in Collections

We can loop through each key returned in the collection with the eachKey method.

For instance, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "jane",
    age: 76
  });

  await db.friends
    .where('age')
    .above(25)
    .eachKey(key => console.log(key));
})()

We make the query that queries the age field.

Then eachKey will have the age value for each result.

Loop Through Each Primary Key

We can loop through each primary key in a collection with the eachPrimaryKey method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "jane",
    age: 76
  });

  await db.friends
    .orderBy('name')
    .eachPrimaryKey(id => console.log(id));
})()

We call eachPrimaryKey to log the value of the id field in our collection.

Conclusion

We can work with collection with various methods provided by Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Exceptions and Collections

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Exception Handling

Exceptions will be handled if they happen in transactions.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, *tags"
});

db.transaction('rw', db.friends, () => {
  window.MissSpelledVar.NonExistingVar = 3;
}).catch((err) => {
  console.error(err);
})

We call catch to catch the error with trying to access a property of a non-existent property.

If we catch an error within an operation, then the transaction won’t be aborted.

This is because the error is considered to be handled already.

So if we have:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, *tags"
});

db.transaction('rw', db.friends, () => {
  db.friends.add({
    id: 1,
    name: "Foo"
  }).catch((e) => {
    console.error("Failed to add friend");
  });
}).catch((err) => {
  console.error(err);
})

Then the inner catch method would handle any errors with the db.friends.add call.

If we want to abort the transaction when an error is thrown, we’ve to rethrow the exception.

To do this, we write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, *tags"
});

db.transaction('rw', db.friends, () => {
  db.friends.add({
    id: 1,
    name: "Foo"
  }).catch((e) => {
    console.error("Failed to add friend");
    throw e;
  });
}).catch((err) => {
  console.error(err);
})

Collection

Collections represent collections of database objects.

They don’t contain any objects by themselves.

Instead, it yields a preparation for how to run a database query.

Collections return a promise with the data.

Add Multiple Criteria

We can add multiple criteria for our collection with the and method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "mary",
    age: 76
  });

  const someFriends = await db.friends
    .where("name")
    .equalsIgnoreCase("jane")
    .and(friends => friends.age > 50)
    .toArray()
  console.log(someFriends)
})()

We call and with a callback that returns the condition that we’re looking for to make the query.

Cloning Collections

We can clone a collection with the clone method.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, age"
});

(async () => {
  await db.friends.put({
    id: 1,
    name: "jane",
    age: 78
  });
  await db.friends.put({
    id: 2,
    name: "mary",
    age: 76
  });

  await db.transaction('r', db.friends, function*() {
    const collection = db.friends.where('age').above(75);
    const clone = collection.clone().reverse().limit(1);
    const allOldFriends = yield collection.toArray();
    const oldestFriend = yield clone.toArray();
    console.log(allOldFriends)
    console.log(oldestFriend)
  }).catch(e => {
    console.error(e.stack);
  });
})()

We added 2 entries to the friends table.

Then we get all the friends with age above 75 with:

const collection = db.friends.where('age').above(75);

Then we call clone to clone the collection with:

const clone = collection.clone().reverse().limit(1);

Then we call toArray on both to get the results as arrays.

Then we should get the entries from the queries in the console.log .

Conclusion

We can handle exceptions and do various things with collections with Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Binary Data and Transactions

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Storing Binary Data

We can store binary with Dexie.

For example, we can write:

(async () => {
  try {
    const db = new Dexie("friends");
    db.version(1).stores({
      friends: "name"
    });
    const res = await fetch("https://images.pexels.com/photos/614810/pexels-photo-614810.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260");
    const blob = await res.blob();
    await db.friends.put({
      name: "David",
      image: blob
    });
  } catch (error) {
    console.log(error);
  }
})()

We get the image with the fetch

Then we convert it to a blob with the res.blob method.

And then we set it as the value of a field.

Indexing Binary Data

IndexedDB 2.0 supports indexing binary data.

For example, we can write:

(async () => {
  try {
    const db = new Dexie("friends");
    db.version(1).stores({
      friends: "id,name"
    });
    await db.friends.put({
      id: new Uint8Array([1, 2, 3, 4, 5]),
      name: "David"
    });

    const friend = await db.friends.get(
      new Uint8Array([1, 2, 3, 4, 5]));

    console.log(friend.name);
  } catch (error) {
    console.log(error);
  }
})()

to add the id column as one of the index fields.

Then we can set the id field to a binary value with the Uint8Array constructor.

And then we call get to search for the given binary value.

Transactions

We can create a transaction to create atomic commits to our IndexedDB database.

This allows us to roll back easily in case of errors.

For example, we can write:

(async () => {
  try {
    const db = new Dexie("friends");
    db.version(1).stores({
      friends: "id,name,age"
    });
    await db.friends.put({
      id: 1,
      name: "David",
      age: 20
    });

    await db.transaction('rw', [db.friends], async () => {
      const friend = await db.friends.get(1);
      ++friend.age;
      await db.friends.put(friend);
    });

    const someFriend = await db.friends.get(1)
    console.log(someFriend)
  } catch (error) {
    console.log(error);
  }
})()

We add an entry to the friends table.

Then we call db.transaction with the 'rw' flag to let us read and write.

The 2nd argument is the tables we’re applying transactions to.

Then we get an entry from the friends table, increment th age, then save the entry to the database.

Now when we get the same entry, we should see the age is 21.

We can use transactionless code in transactions.

For example, we can write:

const db = new Dexie("friends");

db.version(1).stores({
  friends: "id, name, *tags"
});

function goodFriends() {
  return db.friends
    .where('tags')
    .equals('close-friend');
}

async function addComment(friendId, comment) {
  await db.friends
    .where('id')
    .equals(friendId)
    .modify(friend => {
      friend.comments.push(comment);
    });
}

(async () => {
  try {
    await db.friends.put({
      id: 1,
      name: "jane",
      tags: ['close-friend'],
      comments: []
    });

    await db.friends.put({
      id: 2,
      name: "mary",
      tags: [],
      comments: []
    });

    await db.transaction('rw', db.friends, async () => {
      const goodFriendKeys = await goodFriends().primaryKeys();
      await Promise.all(
        goodFriendKeys.map(id => addComment(id, "I like you!"))
      );
    });

    console.log(db.friends.toArray())
  } catch (error) {
    console.log(error);
  }
})()

We have the goodFriends function that returns a query for the friends table with the close-friend' tag.

The addComment function lets us get the friend by the friendId .

Then we call modify to modify the friend entry by adding a string to the comments field.

In the try block, we add some friends entries.

And then we call db.transaction to create the transaction.

We have the same first 2 arguments. Then we get the primary keys of the friends that found with the primaryKeys method.

And then we call Promise.all to loop through the keys and call the addComment function with it to add a comment to the comments array.

In the console.log , we should see the comments field displayed.

Conclusion

We can store binary data and use transactions to create atomic operations with Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Orders and Joins

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Ordering

We can order queries with the between method.

For example, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,[firstName+lastName],age'
    });
    await db.friends.put({
      firstName: "jane",
      lastName: 'smith',
      age: 39
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28
    })
    const someFriends = await db.friends
      .where('[firstName+lastName]')
      .between(["jane", ""], ["jane", "uffff"])
      .toArray()
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

We call between with '' and 'uffff' in the 2nd entry to order the results by lastName .

Also, we can search for items with other methods that act as operators.

For instance, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,firstName,lastName,age,shoeSize,interests'
    });
    await db.friends.put({
      firstName: "jane",
      lastName: 'smith',
      age: 39,
      shoeSize: 7,
      interests: ['sports', 'pets'],
      tags: []
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28,
      shoeSize: 9,
      interests: [],
      tags: []
    })
    const someFriends = await db.friends
      .where('age').above(25)
      .or('shoeSize').below(8)
      .or('interests').anyOf('sports', 'pets', 'cars')
      .modify(friend => friend.tags.push("marketing-target"));
  } catch (error) {
    console.log(error);
  }
})()

We add the items with the age, shoeSize , and interests fields as index fields.

Then we search for the age with the where and above methods.

Then we use the or method to search for the shoeSize below a given size with the below method.

And we use the anyOf method to search if the given values are in the interests array.

Then we call modify to add the tags value to the array.

Retrieve First n Items

We can search for the first n items with the limit method.

For instance, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,firstName,lastName,age'
    });
    await db.friends.put({
      firstName: "jane",
      lastName: 'smith',
      age: 39,
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28,
    })
    const someFriends = await db.friends
      .orderBy("age")
      .reverse()
      .limit(5)
      .toArray();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

We call orderBy to get the entries ordered by age .

We call reverse to sort them by reverse order.

limit limits the number of items returned to the given number.

Joining

We can join multiple tables together.

For example, we can write:

(async () => {
  const db = new Dexie('music');

  try {
    db.version(1).stores({
      albums: '++id,name,*tracks',
      bands: '++id,name,*albumIds'
    });

    const a1 = await db.albums.put({
      name: 'Arrival',
      year: 1974
    })

    const a2 = await db.albums.put({
      name: 'Waterloo',
      year: 1974
    })

    await db.bands.put({
      name: "Abba",
      albumIds: [a1, a2]
    })

    const bands = await db.bands
      .where('name')
      .startsWith('A')
      .toArray();

    const someBands = await Promise.all(bands.map(async band => {
      band.albums = await db.albums.where('id').anyOf(band.albumIds).toArray()
      return band
    }));
    console.log(someBands)
  } catch (error) {
    console.log(error);
  }
})()

We created the albums and bands tables.

Then we populate them with our own data.

a1 and a2 are the IDs for the album entries.

We use then to populate the albumId fields of the band .

Then we query the band with:

const bands = await db.bands
  .where('name')
  .startsWith('A')
  .toArray();

Then we add the albums to the band with:

const someBands = await Promise.all(bands.map(async band => {
  band.albums = await db.albums.where('id').anyOf(band.albumIds).toArray()
  return band
}));

We just attach the child items to a property of the parent.

Conclusion

We can order items and join multiple tables with Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Complex Queries

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Query and Modify Data

We can query for and modify data at the same time.

For example, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 68
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    await db.friends
      .where('age')
      .inAnyRange([
        [0, 18],
        [65, Infinity]
      ])
      .modify({
        discount: 0.5
      });
  } catch (error) {
    console.log(error);
  }
})()

We get all the entries of the friends table with age between 0 to 18 or 65 and up.

Then we call modify to change the discount field to 0.5.

Search for Entries with Any Condition

We can search for entries with our own condition with the filter method.

For example, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 68
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    const someFriends = await db.friends
      .filter(friend => /j/i.test(friend.name))
      .toArray();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

We call filter with a callback that checks the friend.name value to see if it has the letter j in it.

Also, we can search for data with compound indexes.

For instance, we can write:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,[firstName+lastName],age'
    });
    await db.friends.put({
      firstName: "mary",
      lastName: 'smith',
      age: 39
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28
    })
    const someFriends = await db.friends
      .where('[firstName+lastName]')
      .equals(["jane", "wong"])
      .first();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

We have the compound index [firstName+lastName] .

Then we can search for both fields together with the where method with the compound fields.

And equals with the value of each.

first returns the first result.

We search for the value of firstName and lastName together with equals .

We can make this query simpler with the where method:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,[firstName+lastName],age'
    });
    await db.friends.put({
      firstName: "mary",
      lastName: 'smith',
      age: 39
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28
    })
    const someFriends = await db.friends
      .where({
        firstName: "jane",
        lastName: "wong"
      })
      .first();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

We can also use the get method to call where and first together:

(async () => {
  const db = new Dexie("friend_database");
  try {
    await db.version(1).stores({
      friends: '++id,[firstName+lastName],age'
    });
    await db.friends.put({
      firstName: "mary",
      lastName: 'smith',
      age: 39
    })
    await db.friends.put({
      firstName: "jane",
      lastName: 'wong',
      age: 28
    })
    const someFriends = await db.friends
      .get({
        firstName: "jane",
        lastName: "wong"
      })
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

Conclusion

We can make queries that modify data, queries that have more complex conditions, and queries with compound indexes with Dexie.