You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 6-data-storage/03-indexeddb/article.md
+82-20Lines changed: 82 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,18 +35,18 @@ We can have many databases with different names, but all of them exist within th
35
35
36
36
After the call, we need to listen to events on `openRequest` object:
37
37
- `success`: database is ready, there's the "database object" in `openRequest.result`, that we should use it for further calls.
38
-
- `error`: open failed.
39
-
- `upgradeneeded`: database version is outdated (see below).
38
+
- `error`: opening failed.
39
+
- `upgradeneeded`: database is ready, but its version is outdated (see below).
40
40
41
41
**IndexedDB has a built-in mechanism of "schema versioning", absent in server-side databases.**
42
42
43
-
Unlike server-side databases, IndexedDB is client-side, in the browser, so wedon't have the data at hands. But when we publish a new version of our app, we may need to update the database.
43
+
Unlike server-side databases, IndexedDB is client-side, the data is stored in the browser, so we, developers, don't have direct access to it. But when we publish a new version of our app, we may need to update the database.
44
44
45
45
If the local database version is less than specified in `open`, then a special event `upgradeneeded` is triggered, and we can compare versions and upgrade data structures as needed.
46
46
47
47
The event also triggers when the database did not exist yet, so we can perform initialization.
48
48
49
-
For instance, when we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler:
49
+
When we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler:
50
50
51
51
```js
52
52
let openRequest = indexedDB.open("store", *!*1*/!*);
@@ -71,10 +71,10 @@ When we publish the 2nd version:
71
71
```js
72
72
let openRequest = indexedDB.open("store", *!*2*/!*);
73
73
74
-
// check the existing database version, do the updates if needed:
75
74
openRequest.onupgradeneeded = function() {
75
+
// the existing database version is less than 2 (or it doesn't exist)
76
76
let db = openRequest.result;
77
-
switch(db.version) { // existing (old) db version
77
+
switch(db.version) { // existing db version
78
78
case 0:
79
79
// version 0 means that the client had no database
So, in `openRequest.onupgradeneeded` we update the database. Soon we'll see how it's done. And then, only if its handler finishes without errors, `openRequest.onsuccess` triggers.
89
+
88
90
After `openRequest.onsuccess` we have the database object in `openRequest.result`, that we'll use for further operations.
89
91
90
92
To delete a database:
@@ -94,9 +96,70 @@ let deleteRequest = indexedDB.deleteDatabase(name)
94
96
// deleteRequest.onsuccess/onerror tracks the result
95
97
```
96
98
99
+
```warn header="Can we open an old version?"
100
+
Now what if we try to open a database with a lower version than the current one? E.g. the existing DB version is 3, and we try to `open(...2)`.
101
+
102
+
That's an error, `openRequest.onerror` triggers.
103
+
104
+
Such thing may happen if the visitor loaded an outdated code, e.g. from a proxy cache. We should check `db.version`, suggest him to reload the page. And also re-check our caching headers to ensure that the visitor never gets old code.
105
+
```
106
+
107
+
### Parallel update problem
108
+
109
+
As we're talking about versioning, let's tackle a small related problem.
110
+
111
+
Let's say, a visitor opened our site in a browser tab, with database version 1.
112
+
113
+
Then we rolled out an update, and the same visitor opens our site in another tab. So there are two tabs, both with our site, but one has an open connection with DB version 1, while the other one attempts to update it in `upgradeneeded` handler.
114
+
115
+
The problem is that a database is shared between two tabs, as that's the same site, same origin. And it can't be both version 1 and 2. To perform the update to version 2, all connections to version 1 must be closed.
116
+
117
+
In order to organize that, the `versionchange` event triggers an open database object when a parallel upgrade is attempted. We should listen to it, so that we should close the database (and probably suggest the visitor to reload the page, to load the updated code).
118
+
119
+
If we don't close it, then the second, new connection will be blocked with `blocked` event instead of `success`.
120
+
121
+
Here's the code to do that:
122
+
123
+
```js
124
+
let openRequest = indexedDB.open("store", 2);
125
+
126
+
openRequest.onupgradeneeded = ...;
127
+
openRequest.onerror = ...;
128
+
129
+
openRequest.onsuccess = function() {
130
+
let db = openRequest.result;
131
+
132
+
*!*
133
+
db.onversionchange = function() {
134
+
db.close();
135
+
alert("Database is outdated, please reload the page.")
136
+
};
137
+
*/!*
138
+
139
+
// ...the db is ready, use it...
140
+
};
141
+
142
+
*!*
143
+
openRequest.onblocked = function() {
144
+
// there's another open connection to same database
145
+
// and it wasn't closed after db.onversionchange triggered for them
146
+
};
147
+
*/!*
148
+
```
149
+
150
+
Here we do two things:
151
+
152
+
1. Add `db.onversionchange` listener after a successful opening, to be informed about a parallel update attempt.
153
+
2. Add `openRequest.onblocked` listener to handle the case when an old connection wasn't closed. This doesn't happen if we close it in `db.onversionchange`.
154
+
155
+
There are other variants. For example, we can take time to close things gracefully in `db.onversionchange`, prompt the visitor to save the data before the connection is closed. The new updating connection will be blocked immediatelly after `db.onversionchange` finished without closing, and we can ask the visitor in the new tab to close other tabs for the update.
156
+
157
+
Such update collision happens rarely, but we should at least have some handling for it, e.g. `onblocked` handler, so that our script doesn't surprise the user by dying silently.
97
158
98
159
## Object store
99
160
161
+
To store something in IndexedDB, we need an *object store*.
162
+
100
163
An object store is a core concept of IndexedDB. Counterparts in other databases are called "tables" or "collections". It's where the data is stored. A database may have multiple stores: one for users, another one for goods, etc.
101
164
102
165
Despite being named an "object store", primitives can be stored too.
@@ -146,12 +209,12 @@ To perform database version upgrade, there are two main approaches:
146
209
1. We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4 etc. Then, in `upgradeneeded` we can compare versions (e.g. old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4).
147
210
2. Or we can just examine the database: get a list of existing object stores as `db.objectStoreNames`. That object is a [DOMStringList](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#domstringlist) that provides `contains(name)` method to check for existance. And then we can do updates depending on what exists and what doesn't.
148
211
149
-
For small databases the second path may be simpler.
212
+
For small databases the second variant may be simpler.
150
213
151
214
Here's the demo of the second approach:
152
215
153
216
```js
154
-
let openRequest = indexedDB.open("db", 1);
217
+
let openRequest = indexedDB.open("db", 2);
155
218
156
219
// create/upgrade the database without version checks
157
220
openRequest.onupgradeneeded = function() {
@@ -407,9 +470,9 @@ Methods that involve searching support either exact keys or so-called "range que
407
470
408
471
Ranges are created using following calls:
409
472
410
-
- `IDBKeyRange.lowerBound(lower, [open])` means: `>lower` (or `≥lower` if `open` is true)
411
-
- `IDBKeyRange.upperBound(upper, [open])` means: `<upper` (or `≤upper` if `open` is true)
412
-
- `IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen])` means: between `lower` and `upper`, with optional equality if the corresponding `open` is true.
473
+
- `IDBKeyRange.lowerBound(lower, [open])` means: `≥lower` (or `>lower` if `open` is true)
474
+
- `IDBKeyRange.upperBound(upper, [open])` means: `≤upper` (or `<upper` if `open` is true)
475
+
- `IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen])` means: between `lower` and `upper`. If the open flags is true, the corresponding key is not included in the range.
413
476
- `IDBKeyRange.only(key)` -- a range that consists of only one `key`, rarely used.
414
477
415
478
All searching methods accept a `query` argument that can be either an exact key or a key range:
We can also use `IDBKeyRange` to create ranges and looks for cheap/expensive books:
518
581
519
582
```js
520
-
// find books where price < 5
583
+
// find books where price <= 5
521
584
let request = priceIndex.getAll(IDBKeyRange.upperBound(5));
522
585
```
523
586
@@ -615,7 +678,7 @@ In the example above the cursor was made for the object store.
615
678
616
679
But we also can make a cursor over an index. As we remember, indexes allow to search by an object field. Cursors over indexes to precisely the same as over object stores -- they save memory by returning one value at a time.
617
680
618
-
For cursors over indexes, `cursor.key` is the index key (e.g. price), and we should use `cursor.primaryKey` property the object key:
681
+
For cursors over indexes, `cursor.key` is the index key (e.g. price), and we should use `cursor.primaryKey` property for the object key:
619
682
620
683
```js
621
684
let request = priceIdx.openCursor(IDBKeyRange.upperBound(5));
@@ -737,12 +800,11 @@ IndexedDB can be thought of as a "localStorage on steroids". It's a simple key-v
737
800
738
801
The best manual is the specification, [the current one](https://w3c.github.io/IndexedDB) is 2.0, but few methods from [3.0](https://w3c.github.io/IndexedDB/) (it's not much different) are partially supported.
739
802
740
-
The usage can be described with a few phrases:
803
+
The basic usage can be described with a few phrases:
741
804
742
805
1. Get a promise wrapper like [idb](https://github.com/jakearchibald/idb).
743
806
2. Open a database: `idb.openDb(name, version, onupgradeneeded)`
744
-
- Create object storages in indexes in `onupgradeneeded` handlers.
745
-
- Update version if needed - either by comparing numbers or just checking what exists.
807
+
- Create object storages and indexes in `onupgradeneeded` handler or perform version update if needed.
746
808
3. For requests:
747
809
- Create transaction `db.transaction('books')` (readwrite if needed).
748
810
- Get the object store `transaction.objectStore('books')`.
0 commit comments