Làm việc với IndexedDB

Hướng dẫn này trình bày các khái niệm cơ bản về API IndexedDB. Chúng tôi đang dùng thư viện IndexedDB Promified của Jake Archibald, rất giống với API IndexedDB nhưng sử dụng các hứa hẹn, bạn có thể await để có cú pháp ngắn gọn hơn. Việc này giúp đơn giản hoá API trong khi vẫn duy trì cấu trúc của API.

IndexedDB là gì?

IndexedDB là một hệ thống lưu trữ NoSQL có quy mô lớn cho phép lưu trữ chỉ về mọi nội dung trong trình duyệt của người dùng. Ngoài các thao tác tìm kiếm, lấy và đặt thông thường, IndexedDB cũng hỗ trợ các giao dịch và rất phù hợp để lưu trữ một lượng lớn dữ liệu có cấu trúc.

Mỗi cơ sở dữ liệu IndexedDB là duy nhất cho một nguồn gốc (thường là miền trang web hoặc miền con), nghĩa là cơ sở dữ liệu đó không thể truy cập hoặc được bất kỳ nguồn gốc nào khác truy cập. Hạn mức bộ nhớ dữ liệu thường lớn (nếu có), nhưng mỗi trình duyệt lại xử lý các giới hạn và việc loại bỏ dữ liệu theo cách khác nhau. Hãy xem phần Đọc thêm để biết thêm thông tin.

Điều khoản IndexedDB

Cơ sở dữ liệu
Cấp cao nhất của IndexedDB. Tệp này chứa các đối tượng lưu trữ, từ đó chứa dữ liệu bạn muốn duy trì. Bạn có thể tạo nhiều cơ sở dữ liệu với bất kỳ tên nào bạn chọn.
Kho lưu trữ đối tượng
Một bộ chứa riêng lẻ để lưu trữ dữ liệu, tương tự như bảng trong cơ sở dữ liệu quan hệ. Thông thường, có một kho lưu trữ đối tượng cho mỗi loại (không phải loại dữ liệu JavaScript) mà bạn đang lưu trữ. Không giống như trong các bảng cơ sở dữ liệu, các kiểu dữ liệu JavaScript trong cửa hàng không nhất thiết phải nhất quán. Ví dụ: nếu một ứng dụng có kho lưu trữ đối tượng people chứa thông tin về 3 người, thì các thuộc tính tuổi của những người đó có thể là 53, 'twenty-five'unknown.
Chỉ mục
Một loại kho lưu trữ đối tượng dùng để sắp xếp dữ liệu trong một kho lưu trữ đối tượng khác (được gọi là kho lưu trữ đối tượng tham chiếu) theo từng thuộc tính riêng lẻ của dữ liệu. Chỉ mục được thuộc tính này dùng để truy xuất các bản ghi trong kho đối tượng. Ví dụ: nếu đang lưu trữ người, bạn có thể muốn tìm nạp sau theo tên, tuổi hoặc loài động vật yêu thích.
Hoạt động
Hoạt động tương tác với cơ sở dữ liệu.
Giao dịch
Một trình bao bọc xung quanh một toán tử hoặc nhóm thao tác giúp đảm bảo tính toàn vẹn của cơ sở dữ liệu. Nếu một trong các hành động trong giao dịch không thành công, thì sẽ không có hành động nào được áp dụng và cơ sở dữ liệu sẽ trở về trạng thái trước khi giao dịch bắt đầu. Tất cả thao tác đọc hoặc ghi trong IndexedDB phải là một phần của một giao dịch. Điều này cho phép các thao tác đọc-sửa đổi-ghi nguyên tử mà không có nguy cơ xung đột với các luồng khác hoạt động trên cơ sở dữ liệu cùng lúc.
Con trỏ
Cơ chế lặp lại nhiều bản ghi trong cơ sở dữ liệu.

Cách kiểm tra khả năng hỗ trợ IndexedDB

IndexedDB gần như được hỗ trợ trên toàn cầu. Tuy nhiên, nếu đang làm việc với các trình duyệt cũ hơn, bạn không nên phát hiện tính năng hỗ trợ trong trường hợp cần đến. Cách dễ nhất là kiểm tra đối tượng window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

Cách mở cơ sở dữ liệu

Với IndexedDB, bạn có thể tạo nhiều cơ sở dữ liệu với bất kỳ tên nào bạn chọn. Nếu một cơ sở dữ liệu không tồn tại khi bạn cố mở, cơ sở dữ liệu đó sẽ được tạo tự động. Để mở một cơ sở dữ liệu, hãy dùng phương thức openDB() trong thư viện idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

Phương thức này trả về một lời hứa sẽ phân giải tới một đối tượng cơ sở dữ liệu. Khi sử dụng phương thức openDB(), hãy cung cấp tên, số phiên bản và đối tượng sự kiện để thiết lập cơ sở dữ liệu.

Dưới đây là ví dụ về phương thức openDB() trong ngữ cảnh:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

Đặt mục kiểm tra khả năng hỗ trợ IndexedDB ở đầu hàm ẩn danh. Thao tác này sẽ thoát hàm nếu trình duyệt không hỗ trợ IndexedDB. Nếu có thể tiếp tục, hàm này sẽ gọi phương thức openDB() để mở một cơ sở dữ liệu có tên là 'test-db1'. Trong ví dụ này, đối tượng sự kiện không bắt buộc đã được bỏ qua để giữ cho mọi thứ đơn giản, nhưng bạn cần chỉ định đối tượng đó để thực hiện mọi công việc có ý nghĩa với IndexedDB.

Cách làm việc với kho lưu trữ đối tượng

Cơ sở dữ liệu IndexedDB chứa một hoặc nhiều kho lưu trữ đối tượng, mỗi kho lưu trữ có một cột cho một khoá và một cột khác cho dữ liệu liên kết với khoá đó.

Tạo kho lưu trữ đối tượng

Cơ sở dữ liệu IndexedDB có cấu trúc hợp lý phải có một kho lưu trữ đối tượng cho mỗi loại dữ liệu cần được duy trì. Ví dụ: một trang web duy trì hồ sơ và ghi chú của người dùng có thể có một kho lưu trữ đối tượng people chứa các đối tượng person và một kho lưu trữ đối tượng notes chứa các đối tượng note.

Để đảm bảo tính toàn vẹn của cơ sở dữ liệu, bạn chỉ có thể tạo hoặc xoá các kho lưu trữ đối tượng trong đối tượng sự kiện trong lệnh gọi openDB(). Đối tượng sự kiện sẽ hiển thị một phương thức upgrade() cho phép bạn tạo các kho lưu trữ đối tượng. Gọi phương thức createObjectStore() bên trong phương thức upgrade() để tạo kho lưu trữ đối tượng:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

Phương thức này lấy tên của kho lưu trữ đối tượng và một đối tượng cấu hình không bắt buộc cho phép bạn xác định nhiều thuộc tính cho kho lưu trữ đối tượng.

Sau đây là ví dụ về cách sử dụng createObjectStore():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

Trong ví dụ này, một đối tượng sự kiện được truyền đến phương thức openDB() để tạo kho lưu trữ đối tượng và như trước đây, công việc tạo kho lưu trữ đối tượng được thực hiện trong phương thức upgrade() của đối tượng sự kiện. Tuy nhiên, vì trình duyệt sẽ gửi lỗi nếu bạn cố gắng tạo một kho lưu trữ đối tượng đã tồn tại, vì vậy, bạn nên gói phương thức createObjectStore() trong một câu lệnh if để kiểm tra xem kho lưu trữ đối tượng đó có tồn tại hay không. Bên trong khối if, hãy gọi createObjectStore() để tạo một kho lưu trữ đối tượng có tên là 'firstOS'.

Cách xác định khoá chính

Khi xác định kho lưu trữ đối tượng, bạn có thể xác định cách xác định duy nhất dữ liệu trong cửa hàng bằng cách sử dụng khoá chính. Bạn có thể xác định một khoá chính bằng cách xác định đường dẫn khoá hoặc sử dụng trình tạo khoá.

Đường dẫn khoá là một thuộc tính luôn tồn tại và chứa một giá trị duy nhất. Ví dụ: trong trường hợp kho lưu trữ đối tượng people, bạn có thể chọn địa chỉ email làm đường dẫn khoá:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

Ví dụ này sẽ tạo một kho lưu trữ đối tượng có tên là 'people' và chỉ định thuộc tính email làm khoá chính trong tuỳ chọn keyPath.

Bạn cũng có thể sử dụng một trình tạo khoá như autoIncrement. Trình tạo khoá tạo một giá trị duy nhất cho mỗi đối tượng được thêm vào kho lưu trữ đối tượng. Theo mặc định, nếu bạn không chỉ định một khoá, IndexedDB tạo và lưu trữ khoá đó riêng biệt với dữ liệu.

Ví dụ sau đây sẽ tạo một kho lưu trữ đối tượng có tên là 'notes' và đặt khoá chính được tự động gán ở dạng số tự động tăng dần:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Ví dụ sau tương tự như ví dụ trước, nhưng lần này giá trị tự động tăng được chỉ định rõ ràng cho một thuộc tính có tên là 'id'.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Việc chọn phương thức dùng để xác định khoá này sẽ tuỳ thuộc vào dữ liệu của bạn. Nếu dữ liệu của bạn có một thuộc tính luôn là duy nhất, bạn có thể đặt thuộc tính đó làm keyPath để thực thi tính duy nhất này. Nếu không, hãy sử dụng giá trị tự động tăng.

Đoạn mã sau đây tạo ra 3 kho lưu trữ đối tượng minh hoạ nhiều cách xác định khoá chính trong kho lưu trữ đối tượng:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

Cách xác định chỉ mục

Chỉ mục là một loại kho lưu trữ đối tượng được dùng để truy xuất dữ liệu từ kho lưu trữ đối tượng tham chiếu theo một thuộc tính được chỉ định. Chỉ mục nằm trong kho lưu trữ đối tượng tham chiếu và chứa cùng một dữ liệu, nhưng sử dụng thuộc tính được chỉ định làm đường dẫn khoá thay vì khoá chính của kho lưu trữ tham chiếu. Bạn phải tạo chỉ mục khi tạo kho lưu trữ đối tượng và có thể dùng chỉ mục để xác định một quy tắc ràng buộc riêng biệt đối với dữ liệu của mình.

Để tạo chỉ mục, hãy gọi phương thức createIndex() trên một thực thể kho lưu trữ đối tượng:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

Phương thức này tạo và trả về một đối tượng chỉ mục. Phương thức createIndex() trên thực thể của kho đối tượng lấy tên của chỉ mục mới làm đối số đầu tiên và đối số thứ hai tham chiếu đến thuộc tính trên dữ liệu mà bạn muốn lập chỉ mục. Đối số cuối cùng cho phép bạn xác định 2 tuỳ chọn xác định cách chỉ mục hoạt động: uniquemultiEntry. Nếu bạn đặt unique thành true, chỉ mục sẽ không cho phép các giá trị trùng lặp đối với một khoá duy nhất. Tiếp theo, multiEntry sẽ xác định cách createIndex() hoạt động khi thuộc tính được lập chỉ mục là một mảng. Nếu bạn đặt giá trị này thành true, thì createIndex() sẽ thêm một mục vào chỉ mục cho từng phần tử mảng. Nếu không, hàm này sẽ thêm một mục nhập chứa mảng.

Ví dụ:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

Trong ví dụ này, các kho lưu trữ đối tượng 'people''notes' có chỉ mục. Để tạo chỉ mục, trước tiên, hãy chỉ định kết quả của createObjectStore() (đối tượng lưu trữ đối tượng) cho một biến để bạn có thể gọi createIndex() trên đó.

Cách làm việc với dữ liệu

Phần này mô tả cách tạo, đọc, cập nhật và xoá dữ liệu. Tất cả các thao tác này đều không đồng bộ, sử dụng các lời hứa khi API IndexedDB sử dụng các yêu cầu. Việc này giúp đơn giản hoá API. Thay vì theo dõi các sự kiện do yêu cầu kích hoạt, bạn có thể gọi .then() trên đối tượng cơ sở dữ liệu được phương thức openDB() trả về để bắt đầu tương tác với cơ sở dữ liệu hoặc await tạo cơ sở dữ liệu đó.

Tất cả các thao tác dữ liệu trong IndexedDB đều được thực hiện bên trong một giao dịch. Mỗi toán tử có dạng sau:

  1. Lấy đối tượng cơ sở dữ liệu.
  2. Mở giao dịch trên cơ sở dữ liệu.
  3. Mở cửa hàng đối tượng trên giao dịch.
  4. Thực hiện thao tác trên kho lưu trữ đối tượng.

Giao dịch có thể được coi là trình bao bọc an toàn xung quanh một thao tác hoặc nhóm thao tác. Nếu một trong các hành động trong một giao dịch không thành công, thì tất cả các hành động đó sẽ được hoàn nguyên. Giao dịch là dành riêng cho một hoặc nhiều cửa hàng đối tượng mà bạn xác định khi mở giao dịch. Chúng có thể ở chế độ chỉ đọc hoặc đọc và ghi. Điều này cho biết liệu các thao tác bên trong giao dịch có đọc dữ liệu hoặc thực hiện thay đổi đối với cơ sở dữ liệu hay không.

Tạo dữ liệu

Để tạo dữ liệu, hãy gọi phương thức add() trên thực thể cơ sở dữ liệu và truyền dữ liệu mà bạn muốn thêm. Đối số đầu tiên của phương thức add() là kho lưu trữ đối tượng bạn muốn thêm dữ liệu vào và đối số thứ hai là một đối tượng chứa các trường và dữ liệu liên quan mà bạn muốn thêm. Dưới đây là ví dụ đơn giản nhất khi thêm một hàng dữ liệu:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

Mỗi lệnh gọi add() xảy ra trong một giao dịch, vì vậy, ngay cả khi lời hứa giải quyết thành công, điều đó không nhất thiết có nghĩa là thao tác đó đã hoạt động. Để đảm bảo thao tác thêm đã được thực hiện, bạn cần kiểm tra xem toàn bộ giao dịch đã hoàn tất hay chưa bằng phương thức transaction.done(). Đây là một lời hứa sẽ được giải quyết khi giao dịch tự hoàn tất và sẽ từ chối nếu xảy ra lỗi giao dịch. Bạn phải thực hiện quy trình kiểm tra này cho tất cả các thao tác "ghi", vì đó là cách duy nhất để bạn biết các thay đổi đối với cơ sở dữ liệu đã thực sự xảy ra.

Đoạn mã sau đây cho thấy việc sử dụng phương thức add() bên trong một giao dịch:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

Sau khi mở cơ sở dữ liệu (và tạo một kho lưu trữ đối tượng nếu cần), bạn cần mở một giao dịch bằng cách gọi phương thức transaction() trên cơ sở dữ liệu đó. Phương thức này lấy một đối số cho cửa hàng bạn muốn giao dịch, cũng như cho chế độ. Trong trường hợp này, chúng ta muốn ghi chú cho cửa hàng, vì vậy, ví dụ này sẽ chỉ định 'readwrite'.

Bước tiếp theo là bắt đầu thêm các mặt hàng vào cửa hàng trong quá trình giao dịch. Trong ví dụ trước, chúng ta sẽ xử lý 3 thao tác trên cửa hàng 'foods', mỗi thao tác đều trả về một lời hứa:

  1. Đang thêm một bản ghi để có một chiếc bánh sandwich ngon.
  2. Đang thêm bản ghi cho một số trứng.
  3. Tín hiệu rằng giao dịch đã hoàn tất (tx.done).

Vì tất cả các hành động này đều dựa trên lời hứa, nên chúng ta cần phải đợi tất cả các hành động này kết thúc. Việc truyền những lời hứa này cho Promise.all là một cách hay, hiệu quả để thực hiện việc này. Promise.all chấp nhận một loạt lời hứa và kết thúc khi tất cả lời hứa được truyền đến đã được giải quyết.

Đối với hai bản ghi đang được thêm vào, giao diện store của thực thể giao dịch sẽ gọi add() và truyền dữ liệu vào bản ghi đó. Bạn có thể await lệnh gọi Promise.all để nó kết thúc khi giao dịch hoàn tất.

Đọc dữ liệu

Để đọc dữ liệu, hãy gọi phương thức get() trên thực thể cơ sở dữ liệu mà bạn truy xuất bằng phương thức openDB(). get() lấy tên cửa hàng và giá trị khoá chính của đối tượng bạn muốn truy xuất. Dưới đây là ví dụ cơ bản:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

Giống như add(), phương thức get() sẽ trả về một lời hứa, vì vậy, bạn có thể await nếu muốn, hoặc sử dụng lệnh gọi lại .then() của lời hứa.

Ví dụ sau đây sử dụng phương thức get() trên kho lưu trữ đối tượng 'foods' của cơ sở dữ liệu 'test-db4' để lấy một hàng duy nhất theo khoá chính 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

Việc truy xuất một hàng trong cơ sở dữ liệu khá đơn giản: mở cơ sở dữ liệu và chỉ định kho lưu trữ đối tượng và giá trị khoá chính của hàng bạn muốn lấy dữ liệu. Vì phương thức get() trả về một lời hứa nên bạn có thể await nó.

Cập nhật dữ liệu

Để cập nhật dữ liệu, hãy gọi phương thức put() trên kho lưu trữ đối tượng. Phương thức put() tương tự như phương thức add() và cũng có thể được sử dụng thay cho add() để tạo dữ liệu. Dưới đây là ví dụ cơ bản về cách sử dụng put() để cập nhật một hàng trong kho lưu trữ đối tượng theo giá trị khoá chính:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

Giống như các phương thức khác, phương thức này trả về một lời hứa. Bạn cũng có thể sử dụng put() trong giao dịch. Dưới đây là ví dụ về cách sử dụng cửa hàng 'foods' ở phần trước để cập nhật giá của bánh sandwich và trứng:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

Cách các mục được cập nhật tuỳ thuộc vào cách bạn đặt khoá. Nếu bạn thiết lập keyPath, mỗi hàng trong kho đối tượng sẽ được liên kết với một khoá nội tuyến. Ví dụ trên cập nhật các hàng dựa trên khoá này. Khi cập nhật các hàng trong tình huống này, bạn cần chỉ định khoá đó để cập nhật mục thích hợp trong kho lưu trữ đối tượng. Bạn cũng có thể tạo một khoá ngoại tuyến bằng cách đặt autoIncrement làm khoá chính.

Xoá dữ liệu

Để xoá dữ liệu, hãy gọi phương thức delete() trên kho lưu trữ đối tượng:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

Giống như add()put(), bạn có thể sử dụng giá trị này trong giao dịch:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

Cấu trúc của hoạt động tương tác với cơ sở dữ liệu cũng giống như đối với các thao tác khác. Hãy nhớ kiểm tra để đảm bảo rằng toàn bộ giao dịch đã hoàn tất bằng cách đưa phương thức tx.done vào mảng mà bạn truyền đến Promise.all.

Nhận tất cả dữ liệu

Cho đến nay, bạn chỉ truy xuất lần lượt từng đối tượng từ cửa hàng. Bạn cũng có thể truy xuất tất cả dữ liệu hoặc một tập hợp con từ kho lưu trữ đối tượng hoặc chỉ mục bằng phương thức getAll() hoặc con trỏ.

Phương thức getAll()

Cách đơn giản nhất để truy xuất tất cả dữ liệu của kho đối tượng là gọi getAll() trên kho lưu trữ đối tượng hoặc chỉ mục, như sau:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

Phương thức này trả về tất cả các đối tượng trong kho lưu trữ đối tượng mà không có bất kỳ quy tắc ràng buộc nào. Đây là cách trực tiếp nhất để nhận tất cả giá trị từ một kho lưu trữ đối tượng, nhưng cũng là cách ít linh hoạt nhất.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

Ví dụ này gọi getAll() trên kho lưu trữ đối tượng 'foods'. Thao tác này sẽ trả về tất cả đối tượng từ 'foods', được sắp xếp theo khoá chính.

Cách sử dụng con trỏ

Con trỏ là một cách linh hoạt hơn để truy xuất nhiều đối tượng. Con trỏ chọn từng đối tượng trong một kho lưu trữ đối tượng hoặc lập chỉ mục từng đối tượng, cho phép bạn thực hiện thao tác nào đó với dữ liệu khi dữ liệu được chọn. Con trỏ, giống như các thao tác khác với cơ sở dữ liệu, hoạt động trong các giao dịch.

Để tạo con trỏ, hãy gọi openCursor() trên kho lưu trữ đối tượng trong giao dịch. Sử dụng kho lưu trữ 'foods' từ các ví dụ trước, sau đây là cách di chuyển con trỏ qua tất cả các hàng dữ liệu trong kho lưu trữ đối tượng:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

Trong trường hợp này, giao dịch sẽ được mở ở chế độ 'readonly' và phương thức openCursor của giao dịch sẽ được gọi. Trong vòng lặp while tiếp theo, hàng ở vị trí hiện tại của con trỏ có thể đọc thuộc tính keyvalue. Bạn có thể thao tác trên các giá trị đó theo bất kỳ cách nào phù hợp nhất với ứng dụng của mình. Khi đã sẵn sàng, bạn có thể gọi phương thức continue() của đối tượng cursor để chuyển đến hàng tiếp theo và vòng lặp while sẽ chấm dứt khi con trỏ đến cuối tập dữ liệu.

Sử dụng con trỏ cùng với dải ô và chỉ mục

Chỉ mục cho phép bạn tìm nạp dữ liệu trong kho lưu trữ đối tượng theo một thuộc tính không phải là khoá chính. Bạn có thể tạo chỉ mục trên bất kỳ thuộc tính nào, chỉ mục này sẽ trở thành keyPath cho chỉ mục, chỉ định một dải ô trên thuộc tính đó và lấy dữ liệu trong dải ô đó bằng cách sử dụng getAll() hoặc con trỏ.

Hãy xác định dải ô bằng cách sử dụng đối tượng IDBKeyRange và bất kỳ phương thức nào sau đây:

Phương thức upperBound()lowerBound() chỉ định giới hạn trên và giới hạn dưới của phạm vi.

IDBKeyRange.lowerBound(indexKey);

hoặc:

IDBKeyRange.upperBound(indexKey);

Mỗi đối số sẽ lấy một đối số: giá trị keyPath của chỉ mục cho mục bạn muốn chỉ định làm giới hạn trên hoặc giới hạn dưới.

Phương thức bound() chỉ định cả giới hạn trên và giới hạn dưới:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

Phạm vi cho các hàm này được bao gồm theo mặc định, nghĩa là phạm vi này bao gồm dữ liệu được chỉ định làm giới hạn của phạm vi. Để loại bỏ các giá trị đó, hãy chỉ định dải ô là loại trừ bằng cách truyền true làm đối số thứ hai cho lowerBound() hoặc upperBound() hoặc làm đối số thứ ba và thứ tư của bound() cho giới hạn dưới và giới hạn trên tương ứng.

Ví dụ tiếp theo sử dụng một chỉ mục trên thuộc tính 'price' trong kho lưu trữ đối tượng 'foods'. Cửa hàng hiện cũng đính kèm một biểu mẫu với hai dữ liệu đầu vào cho giới hạn trên và giới hạn dưới của phạm vi. Hãy sử dụng mã sau để tìm thực phẩm có giá nằm giữa các giới hạn đó:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

Trước tiên, mã ví dụ sẽ lấy giá trị cho các giới hạn và kiểm tra xem các giới hạn đó có tồn tại hay không. Khối mã tiếp theo sẽ quyết định phương thức cần sử dụng để giới hạn phạm vi dựa trên các giá trị. Trong hoạt động tương tác với cơ sở dữ liệu, hãy mở kho lưu trữ đối tượng trên giao dịch như bình thường, sau đó mở chỉ mục 'price' trên kho lưu trữ đối tượng. Chỉ mục 'price' cho phép bạn tìm kiếm các mặt hàng theo giá.

Sau đó, mã sẽ mở một con trỏ trên chỉ mục và chuyển vào dải ô. Con trỏ trả về một lời hứa đại diện cho đối tượng đầu tiên trong dải ô hoặc undefined nếu không có dữ liệu trong dải ô. Phương thức cursor.continue() trả về một con trỏ đại diện cho đối tượng tiếp theo và tiếp tục thông qua vòng lặp cho đến khi bạn đến cuối phạm vi.

Tạo phiên bản cơ sở dữ liệu

Khi gọi phương thức openDB(), bạn có thể chỉ định số phiên bản cơ sở dữ liệu trong tham số thứ hai. Trong tất cả các ví dụ trong hướng dẫn này, phiên bản đã được đặt thành 1, nhưng bạn có thể nâng cấp cơ sở dữ liệu lên phiên bản mới nếu cần sửa đổi cơ sở dữ liệu theo một cách nào đó. Nếu phiên bản được chỉ định lớn hơn phiên bản của cơ sở dữ liệu hiện có, thì lệnh gọi lại upgrade trong đối tượng sự kiện sẽ được thực thi, cho phép bạn thêm kho lưu trữ và lập chỉ mục đối tượng mới vào cơ sở dữ liệu.

Đối tượng db trong lệnh gọi lại upgrade có một thuộc tính oldVersion đặc biệt, cho biết số phiên bản của cơ sở dữ liệu mà trình duyệt có quyền truy cập. Bạn có thể truyền số phiên bản này vào câu lệnh switch để thực thi các khối mã bên trong lệnh gọi lại upgrade dựa trên số phiên bản cơ sở dữ liệu hiện có. Ví dụ:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

Ví dụ này đặt phiên bản mới nhất của cơ sở dữ liệu thành 2. Khi mã này thực thi lần đầu tiên, cơ sở dữ liệu chưa tồn tại trong trình duyệt, vì vậy, oldVersion sẽ là 0 và câu lệnh switch sẽ bắt đầu tại case 0. Trong ví dụ, thao tác này sẽ thêm kho lưu trữ đối tượng 'store' vào cơ sở dữ liệu.

Điểm chính: Trong các câu lệnh switch, thường có một break sau mỗi khối case, nhưng điều này không được cố ý sử dụng ở đây. Bằng cách này, nếu cơ sở dữ liệu hiện có bị chậm hơn một vài phiên bản hoặc nếu cơ sở dữ liệu không tồn tại, mã sẽ tiếp tục qua phần còn lại của khối case cho đến khi được cập nhật. Vì vậy, trong ví dụ này, trình duyệt tiếp tục thực thi thông qua case 1, tạo một chỉ mục name trên kho lưu trữ đối tượng store.

Để tạo chỉ mục 'description' trên kho lưu trữ đối tượng 'store', hãy cập nhật số phiên bản và thêm một khối case mới như sau:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

Nếu cơ sở dữ liệu bạn đã tạo trong ví dụ trước vẫn tồn tại trong trình duyệt, thì khi cơ sở dữ liệu này được thực thi, oldVersion sẽ là 2. Trình duyệt bỏ qua case 0case 1, đồng thời thực thi mã trong case 2 để tạo chỉ mục description. Sau đó, trình duyệt có một cơ sở dữ liệu ở phiên bản 3 chứa kho lưu trữ đối tượng store với các chỉ mục namedescription.

Tài liệu đọc thêm

Các tài nguyên sau đây cung cấp thêm thông tin và bối cảnh về cách sử dụng IndexedDB.

Tài liệu về IndexedDB

Hạn mức bộ nhớ dữ liệu