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.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Delete Items and 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.

Delete Items

We can an item by its primary key with the delete 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: 28
    })
    const friend = await db.friends.get('mary');
    await db.friends.delete('mary');
  } catch (error) {
    console.log(error);
  }
})()

We create an object with primary key 'mary' , which is the value of the name column.

Then we call delete to delete the item by its primary key.

We can call bulkDelete to delete multiple items with their primary keys.

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: 28
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    const friend = await db.friends.get('mary');
    await db.friends.bulkDelete(['mary', 'jane']);
  } catch (error) {
    console.log(error);
  }
})()

We create friends entries with primary key 'mary' and 'jane' .

Then we can delete them both with the bulkDelete method.

Query Items

We can query items with various methods.

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: 28
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    const someFriends = await db.friends
      .where("age")
      .between(20, 30)
      .offset(0)
      .limit(25)
      .toArray();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

The where method takes the field we’re searching in.

between takes the arguments we want to search between.

offset lets us skip the first number of items given the arguments.

limit limits the number of results returned.

toArray converts the results to an array.

We can also loop through the results with the each method.

For instance, 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: 28
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    const someFriends = await db.friends
      .where("name")
      .equalsIgnoreCase("mary")
      .each(friend => {
        console.log(friend);
      });
  } catch (error) {
    console.log(error);
  }
})()

We call equalsIgnoreCase to find the name with the given value ignoring the case.

Then we use each to loop through each result.

We can do a case-insensitive search for values of a field that starts with any one of the given substrings with the startsWithAnyOfIgnoreCase 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: 28
    })
    await db.friends.put({
      name: "jane",
      age: 28
    })
    const someFriends = await db.friends
      .where("name")
      .startsWithAnyOfIgnoreCase(['m', 'j'])
      .toArray();
    console.log(someFriends)
  } catch (error) {
    console.log(error);
  }
})()

to search for values of name that starts with 'm' or 'j' .

Conclusion

We can delete items from our tables and query for data in our IndexedDB database with Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — CRUD Operations

%2F%2Funsplash.com%3Futm_source%3Dmedium%26utm_medium%3Dreferral)

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.

Declare Database

We declare our IndexedDB database with the Dexie class.

For example, we write:

(async () => {
  var db = new Dexie("MyDatabase");
  db.version(1).stores({
    friends: "++id, name, age, *tags",
    records: "id, score"
  });
})()

friends and records are the tables.

The property strings are the columns that we want to index.

We shouldn’t declare all columns like in SQL.

The string has some special syntax. They include:

  • + — Auto-incremented primary key
  • & — Unique
  • * — Multi-entry index
  • [A+B] — Compound index

Class Binding

We can map our entries to an instance of a class by writing:

class Friend {
  save() {
    return db.friends.put(this);
  }

get age() {
    return moment(Date.now()).diff(this.birthDate, 'years');
  }
}

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  db.friends.mapToClass(Friend);
})()

We call mapToClass to map our friends entries to Friend class instances.

Add Items

To add items into our database, we call the add method.

For example, we can write:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  await db.friends.add({
    name: "joe",
    age: 21
  });
})()

to add an entry to the friends table.

We can add multiple entries at once with the bulkAdd method:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  await db.friends.bulkAdd([{
      name: "alex",
      age: 31
    },
    {
      name: "mary",
      age: 32
    }
  ]);
})()

Update Items

We update items in our database with the put method.

If it doesn’t exist, put will also add new objects to the store.

For example, we can write:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  await db.friends.put({
    id: 4,
    name: "jane",
    age: 33
  });

})()

We can use bulkPut to call put with multiple objects.

For example, we can write:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  await db.friends.bulkPut([{
      id: 4,
      name: "bob",
      age: 34
    },
    {
      id: 5,
      name: "may",
      age: 44
    }
  ]);

})()

to add or update both entries.

If we just want to update an entry, we can use the update method.

For example, we can write:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, *tags",
  });
  await db.friends.update(4, {
    name: "peter"
  });
})()

to update entries with id 4 to with the new field values.

We can also call modify to modify any entry that is returned from queries:

(async () => {
  const db = new Dexie("MyDatabase");
  await db.version(1).stores({
    friends: "++id, name, age, discount",
  });
  await db.friends.bulkAdd([{
      name: "alex",
      age: 17
    },
    {
      name: "mary",
      age: 32
    },
    {
      name: "don",
      age: 70
    }
  ]);
  const results = await db.friends
    .where("age")
    .inAnyRange([
      [0, 18],
      [65, Infinity]
    ])
  await results.modify(friend => {
    friend.discount = 0.3;
  });
})()

We get the results from the query. where takes a field name.

inAnyRange lets us search for values in the given ranges for the given field name.

The modify modifies the returned results.

Conclusion