A CRUD JavaScript Class
A couple of weeks ago, I started making a little to-do list app' in JavaScript, with history, localStorage and quite a couple of things in order to get better at JavaScript. After a couple of hours, I decided to externalize the storage in an other class to keep things clean.
Once I was done with this little project, I thought I could make the storage class a bit more generic to be used pretty much everywhere we need to store values as key/value pairs. Thus the article, explaining how I did it.
If you're only interested in the code, have a look at the GitHub repo or this pen.
What Does it Do?
The main idea was to provide a simple and tiny CRUD API (<2Kb once gzipped). Basically, you instantiate a database, inserts objects (documents if you're a fan of MongoDB) and store them wherever you want. Then you can retrieve, update and delete documents. Pretty easy.
At first I hard-coded the usage of localStorage, then with the help of Fabrice Weinberg and Valérian Galliat, I managed to externalize the driver (what does the storage) so that you can plug the CRUD class to whatever suits your needs. Thanks a lot mates!
What is the Driver?
The driver is what actually stores your data. It is the interface that deals with persistence. To put it simple, the Database
class manipulate your data while the driver stores them.
You can code your own driver or use the one I made which relies on DOM Storage (either localStorage
or sessionStorage
, depending on how your initialize it). The driver can rely on any storage system that supports key/value pairs (DOM Storage, Redis...). Also, it has to implement the 3 methods: getItem
, setItem
and removeItem
.
How Does the "Quick Search" Work?
The thing is I wanted to be able to retrieve documents not only by ID, but also by searching for a couple of criterias (basically an object with properties/values). To do this, there are not thousand of solutions: you have to loop through all the documents stored in the database, then for each one iterate over all its properties and check whether they match the ones from the object given to the find
function.
While this process does work, it can become painfully slow when you have hundreds of documents and are looking for a match between several properties. I needed something faster. Here comes what I call "quicksearch".
The main idea is to index the properties which are most likely to be used when searching for documents. Let's say you store users, like this one:
var dev = { name: 'Hugo', age: 22, job: 'dev' }
When instanciating the database, you could pass the Database constructor indexedKeys: ['job', 'name']
to inform the class that on every operation, it has to index those properties in order to perform quick searches on those. Here is what happen when you insert the dev into the database:
- It adds a unique key (default is
id
) to the object in order to be able to identify it later - It tells the driver to store the object
- The driver serializes and stores the object like this
"{namespace}:{id}": "{serialized object}"
(where{namespace}
is the name of the database and{id}
is the unique id assigned in step 1) - It loops through all properties of the object to check if some of them have to be indexed. For each of them, it stores an entry like this
"{namespace}:{property}:{value}": "{array of IDs}"
so:"MyDatabase:name:Hugo": "[1]"
"MyDatabase:job:dev": "[1]"
Now whenever you want to look for all documents which have Hugo
as a name
, the find
function can perform a quick search by directly looking into the "MyDatabase:name:Hugo"
entry to retrieve the unique ID of all of them. Fast and efficient.
How Do You Use It?
Instantiating a database
As seen before, the indexedKeys
property aims at speeding up the search. By setting some keys to be indexed, searching for those keys will be way faster. In any case, you can search for any key, even those which are not indexed.
var db = new Database({ name: 'MyDatabase', indexedKeys: ['job', 'age'] })
Inserting a new document
var obj = { name: 'Hugo', age: 22, job: 'dev' } var id = db.insert(obj)
Updating a document
If you want to update a specific document, the easiest way is to pass its ID as the first argument. The ID is being added to the entry when inserted as the id
property. You can change the name of this property by setting the uniqueKey
option when instantiating the database.
obj['mood'] = 'happy' db.update(id, obj)
To update a collection of document based on a search, here is how you would do it:
var dev, devs = this.find({ job: 'dev' }) for(var i = 0, len = devs.length; i < len; i++) { dev = devs[i] dev['mood'] = 'happy' dev.job = 'clown' db.update(dev.id, dev) }
Retrieving documents
The find
method requires an object to parse and search with.
db.find({ mood: 'happy' }) db.find({ job: 'dev', age: 22 })
Retrieving all documents
You can either call the findAll
method which returns all existing documents in the database:
db.findAll()
Or you can call the find
method with no arguments, which basically does the same thing:
db.find()
Deleting a document
If you want to delete a specific document, the easiest way is to pass its ID to the function. The ID is being added to the entry when inserted as the id
property. You can change the name of this property by setting the uniqueKey
option when instantiating the database.
db.delete(id)
If you want to delete a collection of documents based on a search, you can pass an object to the function. The function will first perform a find, then delete all the returned documents.
db.delete({ job: dev })
How Do You Build Your Own Driver?
The thing is you don't have to use the StorageDriver I built if you don't want to use DOM Storage. I kept it out of the core so you build use your own driver as long as it relies on a key/value storage system. To build your own, it is quite easy:
(function ( exports ) { 'use strict'; var NameOfYourDriver = function ( conf ) { this.conf = exports.extend({ name: 'NameOfYourDriver' // whatever you need }, conf || {}); }; NameOfYourDriver.prototype.setItem = function ( key, value ) { // Set an item // If key doesn't exist, create it // If key exists, replace with new value }; NameOfYourDriver.prototype.getItem = function ( key ) { // Return the item matching key // If key doesn't exist, return null }; NameOfYourDriver.prototype.removeItem = function ( key ) { // Remove the item at key if it exists }; if (exports.Database) { exports.Database.drivers.NameOfYourDriver = NameOfYourDriver; } }) ( window );
Then to use it, simply instantiate the Database with an instance of your driver:
var db = new Database({ name: 'MyDatabase', driver: new Database.driver.NameOfYourDriver({ name: 'MyDatabase' // whatever is needed }) })
Done! You don't have to change the Database
code at all. If you've made your driver correctly, everything should work like a charm. Pretty neat, isn't it? :)
What Next?
Well folks, you tell me! I'd like to implement a couple of other tools like limit()
, sort()
as long as operators like OR
and AND
but I'm afraid it adds too much complexity to such a simple API.
In any case if you come across a bug or think of a feature that could make this API better, make sure to open an issue on the GitHub repository.
About Kitty Giraudel
Front-developer from France. Author at Codrops, helper at CSS-Tricks, curator of Browserhacks. CSS Goblin, Sass hacker, margin psycho. You can catch me on HugoGiraudel.com or Twitter.
Pretty neat. I like the modular driver. Thanks for it Hugo !
By the way, whenever you want to compare a length > 0, you can type:
If you could e mail me with a few hints on how you made your blog look this great, I would be grateful.
It could be great if you move the Database Class to TypeScript!
Can we store blobs in the indexed database? :)