SQLite trong NodeJS: Hướng dẫn toàn diện từ A đến Z cho lập trình viên

SQLite là một cơ sở dữ liệu quan hệ nhẹ, được nhúng trực tiếp vào ứng dụng của bạn, không cần máy chủ riêng biệt. Điều này khiến nó trở thành lựa chọn lý tưởng cho nhiều dự án NodeJS, đặc biệt là các ứng dụng nhỏ, thử nghiệm hoặc yêu cầu tính di động cao. Bài viết này sẽ cung cấp cho bạn mọi thứ bạn cần biết về việc sử dụng Sqlite Trong Nodejs, từ cài đặt, tạo cơ sở dữ liệu, thực hiện truy vấn, đến các mẹo tối ưu hiệu suất và xử lý lỗi.

Tại sao nên sử dụng SQLite trong NodeJS?

SQLite mang lại nhiều lợi ích khi sử dụng trong NodeJS, đặc biệt trong các trường hợp sau:

  • Đơn giản: Không cần cài đặt hoặc cấu hình máy chủ cơ sở dữ liệu riêng.
  • Nhẹ: Chiếm ít tài nguyên hệ thống, phù hợp cho các thiết bị nhúng hoặc ứng dụng nhỏ.
  • Di động: Cơ sở dữ liệu được lưu trữ trong một tệp duy nhất, dễ dàng sao chép và di chuyển.
  • Nhanh chóng: Cho hiệu suất tốt trong nhiều trường hợp sử dụng, đặc biệt với các truy vấn đơn giản.
  • Miễn phí và mã nguồn mở: Không tốn chi phí bản quyền và có thể tùy chỉnh theo nhu cầu.

Tuy nhiên, SQLite cũng có một số hạn chế cần lưu ý:

  • Không phù hợp cho ứng dụng lớn với lưu lượng truy cập cao: SQLite không được thiết kế để xử lý đồng thời lượng lớn kết nối.
  • Tính năng hạn chế: So với các hệ quản trị cơ sở dữ liệu quan hệ (RDBMS) lớn hơn như PostgreSQL hoặc MySQL, SQLite có ít tính năng hơn.

“SQLite là một lựa chọn tuyệt vời cho các ứng dụng nhỏ và vừa, nơi sự đơn giản và di động là ưu tiên hàng đầu. Nó giúp bạn tiết kiệm thời gian và công sức cài đặt và cấu hình một máy chủ cơ sở dữ liệu phức tạp.” – Ông Nguyễn Văn An, Chuyên gia phát triển NodeJS tại FPT Software

Cài đặt SQLite trong NodeJS

Để sử dụng SQLite trong NodeJS, bạn cần cài đặt một trình điều khiển (driver). Có một vài trình điều khiển phổ biến, nhưng sqlite3 là một trong những lựa chọn được khuyên dùng và phổ biến nhất.

Bạn có thể cài đặt sqlite3 bằng npm (Node Package Manager):

npm install sqlite3

Tạo và kết nối đến cơ sở dữ liệu SQLite

Sau khi cài đặt sqlite3, bạn có thể tạo và kết nối đến cơ sở dữ liệu SQLite.

const sqlite3 = require('sqlite3').verbose();

// Tên tệp cơ sở dữ liệu
const dbFile = 'mydatabase.db';

// Tạo một đối tượng cơ sở dữ liệu
let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Đóng kết nối cơ sở dữ liệu khi hoàn thành
db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Trong ví dụ trên:

  1. Chúng ta require module sqlite3.
  2. Chúng ta chỉ định tên tệp cơ sở dữ liệu (mydatabase.db). Nếu tệp này chưa tồn tại, SQLite sẽ tự động tạo nó.
  3. Chúng ta tạo một đối tượng sqlite3.Database để kết nối đến cơ sở dữ liệu.
  4. Chúng ta sử dụng hàm callback để xử lý lỗi và in thông báo thành công.
  5. Chúng ta đóng kết nối cơ sở dữ liệu sau khi sử dụng.

Tạo bảng trong SQLite

Sau khi kết nối đến cơ sở dữ liệu, bạn có thể tạo bảng bằng câu lệnh SQL CREATE TABLE.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Tạo bảng "users"
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  )
`, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Tạo bảng "users" thành công.');
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Trong ví dụ này, chúng ta tạo một bảng có tên users với các cột id, name, và email. INTEGER PRIMARY KEY AUTOINCREMENT chỉ định rằng cột id là khóa chính, tự động tăng. TEXT NOT NULL chỉ định rằng cột name là kiểu văn bản và không được để trống. TEXT UNIQUE NOT NULL chỉ định rằng cột email là kiểu văn bản, phải là duy nhất và không được để trống. IF NOT EXISTS đảm bảo rằng bảng chỉ được tạo nếu nó chưa tồn tại.

Thêm dữ liệu vào bảng

Bạn có thể thêm dữ liệu vào bảng bằng câu lệnh SQL INSERT.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Chèn dữ liệu vào bảng "users"
db.run(`
  INSERT INTO users (name, email) VALUES
    ('Nguyen Van A', '[email protected]'),
    ('Tran Thi B', '[email protected]')
`, function(err) {
  if (err) {
    return console.error(err.message);
  }
  console.log(`Đã chèn ${this.changes} bản ghi.`);
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Ở đây, chúng ta sử dụng db.run() để thực thi câu lệnh INSERT. this.changes trả về số lượng bản ghi đã được chèn vào.

Truy vấn dữ liệu từ bảng

Bạn có thể truy vấn dữ liệu từ bảng bằng câu lệnh SQL SELECT. Có hai phương pháp chính để truy vấn dữ liệu trong sqlite3:

  • db.each(): Lặp qua từng bản ghi kết quả.
  • db.all(): Trả về tất cả các bản ghi kết quả trong một mảng.

Sử dụng db.each()

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Truy vấn dữ liệu từ bảng "users"
db.each(`SELECT id, name, email FROM users`, (err, row) => {
  if (err) {
    console.error(err.message);
  }
  console.log(`${row.id}t${row.name}t${row.email}`);
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

db.each() lặp qua từng bản ghi kết quả và gọi hàm callback cho mỗi bản ghi. row là một đối tượng chứa dữ liệu của bản ghi hiện tại.

Sử dụng db.all()

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Truy vấn dữ liệu từ bảng "users"
db.all(`SELECT id, name, email FROM users`, (err, rows) => {
  if (err) {
    console.error(err.message);
  }
  rows.forEach((row) => {
    console.log(`${row.id}t${row.name}t${row.email}`);
  });
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

db.all() trả về một mảng chứa tất cả các bản ghi kết quả. Chúng ta có thể sử dụng forEach() để lặp qua mảng và xử lý từng bản ghi.

Cập nhật dữ liệu trong bảng

Bạn có thể cập nhật dữ liệu trong bảng bằng câu lệnh SQL UPDATE.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Cập nhật email của người dùng có id là 1
db.run(`
  UPDATE users
  SET email = '[email protected]'
  WHERE id = 1
`, function(err) {
  if (err) {
    return console.error(err.message);
  }
  console.log(`Đã cập nhật ${this.changes} bản ghi.`);
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Trong ví dụ này, chúng ta cập nhật email của người dùng có id là 1. this.changes trả về số lượng bản ghi đã được cập nhật.

Xóa dữ liệu khỏi bảng

Bạn có thể xóa dữ liệu khỏi bảng bằng câu lệnh SQL DELETE.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Xóa người dùng có id là 2
db.run(`
  DELETE FROM users
  WHERE id = 2
`, function(err) {
  if (err) {
    return console.error(err.message);
  }
  console.log(`Đã xóa ${this.changes} bản ghi.`);
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Ở đây, chúng ta xóa người dùng có id là 2. this.changes trả về số lượng bản ghi đã được xóa.

Sử dụng Prepared Statements để ngăn chặn SQL Injection

SQL injection là một lỗ hổng bảo mật nghiêm trọng cho phép kẻ tấn công thực thi các câu lệnh SQL độc hại. Để ngăn chặn SQL injection, bạn nên sử dụng prepared statements. Prepared statements cho phép bạn tách biệt dữ liệu khỏi câu lệnh SQL, ngăn chặn kẻ tấn công chèn mã SQL độc hại.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

// Sử dụng prepared statement để chèn dữ liệu
const sql = `INSERT INTO users (name, email) VALUES (?, ?)`;
const name = 'Le Van C';
const email = '[email protected]';

db.run(sql, [name, email], function(err) {
  if (err) {
    return console.error(err.message);
  }
  console.log(`Đã chèn ${this.changes} bản ghi.`);
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Trong ví dụ này, chúng ta sử dụng dấu ? làm placeholder cho các giá trị sẽ được chèn vào câu lệnh SQL. Sau đó, chúng ta truyền một mảng chứa các giá trị tương ứng với các placeholder. sqlite3 sẽ tự động escape các giá trị này để ngăn chặn SQL injection.

“Việc sử dụng prepared statements không chỉ giúp bảo vệ ứng dụng của bạn khỏi SQL injection mà còn cải thiện hiệu suất, đặc biệt là khi bạn thực hiện nhiều truy vấn tương tự với các giá trị khác nhau.” – Bà Phạm Thu Hương, Chuyên gia bảo mật ứng dụng web tại CyStack

Xử lý lỗi

Việc xử lý lỗi là rất quan trọng để đảm bảo ứng dụng của bạn hoạt động ổn định. Trong các ví dụ trên, chúng ta đã sử dụng hàm callback để xử lý lỗi. Tuy nhiên, bạn cũng có thể sử dụng try...catch để xử lý lỗi một cách đồng bộ.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

try {
  let db = new sqlite3.Database(dbFile, (err) => {
    if (err) {
      throw err;
    }
    console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
  });

  db.run(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL
    )
  `, (err) => {
    if (err) {
      throw err;
    }
    console.log('Tạo bảng "users" thành công.');
  });

  db.close((err) => {
    if (err) {
      throw err;
    }
    console.log('Đóng kết nối cơ sở dữ liệu.');
  });
} catch (err) {
  console.error(err.message);
}

Transactions

Transactions cho phép bạn thực hiện nhiều thao tác cơ sở dữ liệu như một đơn vị duy nhất. Nếu một trong các thao tác thất bại, tất cả các thao tác sẽ được hoàn tác, đảm bảo tính nhất quán của dữ liệu.

const sqlite3 = require('sqlite3').verbose();
const dbFile = 'mydatabase.db';

let db = new sqlite3.Database(dbFile, (err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Kết nối thành công đến cơ sở dữ liệu SQLite.');
});

db.serialize(() => {
  db.run('BEGIN TRANSACTION');

  db.run(`INSERT INTO users (name, email) VALUES ('Hoang Minh D', '[email protected]')`);
  db.run(`UPDATE users SET name = 'Hoang Minh D Updated' WHERE email = '[email protected]'`);

  db.run('COMMIT', (err) => {
    if (err) {
      db.run('ROLLBACK');
      console.error(err.message);
    } else {
      console.log('Transaction thành công.');
    }
  });
});

db.close((err) => {
  if (err) {
    console.error(err.message);
  }
  console.log('Đóng kết nối cơ sở dữ liệu.');
});

Trong ví dụ này, chúng ta sử dụng db.serialize() để đảm bảo rằng các câu lệnh SQL được thực thi tuần tự. Chúng ta bắt đầu một transaction bằng câu lệnh BEGIN TRANSACTION. Sau đó, chúng ta thực hiện hai thao tác: chèn một bản ghi và cập nhật bản ghi đó. Cuối cùng, chúng ta commit transaction bằng câu lệnh COMMIT. Nếu có lỗi xảy ra, chúng ta rollback transaction bằng câu lệnh ROLLBACK.

Asynchronous Operations với Promises và async/await

Sử dụng Promises và async/await giúp làm cho mã của bạn dễ đọc và dễ quản lý hơn, đặc biệt là khi làm việc với các thao tác bất đồng bộ.

Đầu tiên, bạn cần một thư viện hỗ trợ Promises cho sqlite3. Một lựa chọn phổ biến là sqlite:

npm install sqlite

Sau đó, bạn có thể sử dụng nó như sau:

const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');

async function main() {
  const db = await sqlite.open({
    filename: 'mydatabase.db',
    driver: sqlite3.Database
  });

  try {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL
      )
    `);

    await db.run(`INSERT INTO users (name, email) VALUES (?, ?)`, ['Nguyen Thi E', '[email protected]']);

    const users = await db.all('SELECT * FROM users');
    console.log(users);
  } catch (error) {
    console.error(error);
  } finally {
    await db.close();
  }
}

main();

Trong ví dụ này, chúng ta sử dụng async/await để thực hiện các thao tác cơ sở dữ liệu một cách bất đồng bộ. Chúng ta cũng sử dụng try...catch...finally để xử lý lỗi và đảm bảo rằng kết nối cơ sở dữ liệu được đóng.

Tối ưu hiệu suất

Để tối ưu hiệu suất khi sử dụng SQLite trong NodeJS, bạn có thể áp dụng các mẹo sau:

  • Sử dụng indexes: Indexes giúp tăng tốc độ truy vấn.
  • Sử dụng prepared statements: Như đã đề cập ở trên, prepared statements không chỉ giúp ngăn chặn SQL injection mà còn cải thiện hiệu suất.
  • Sử dụng transactions: Transactions giúp giảm số lượng thao tác ghi đĩa.
  • Tối ưu hóa câu lệnh SQL: Viết câu lệnh SQL hiệu quả.
  • Sử dụng caching: Cache dữ liệu thường xuyên được truy cập.
  • Đóng kết nối cơ sở dữ liệu: Đóng kết nối cơ sở dữ liệu khi không sử dụng nữa.

Các trường hợp sử dụng phổ biến của SQLite trong NodeJS

  • Ứng dụng desktop: Lưu trữ dữ liệu cục bộ cho các ứng dụng desktop.
  • Ứng dụng di động: Lưu trữ dữ liệu cục bộ cho các ứng dụng di động (sử dụng với các framework như React Native hoặc Ionic).
  • Ứng dụng web: Lưu trữ dữ liệu cho các ứng dụng web nhỏ hoặc thử nghiệm.
  • Testing: Sử dụng SQLite làm cơ sở dữ liệu trong quá trình testing để đảm bảo tính độc lập và tốc độ.

So sánh SQLite với các hệ quản trị cơ sở dữ liệu khác (MySQL, PostgreSQL)

Tính năng SQLite MySQL PostgreSQL
Kiến trúc Nhúng (Embedded) Client-Server Client-Server
Độ phức tạp Đơn giản, dễ sử dụng Phức tạp hơn Phức tạp hơn
Khả năng mở rộng Hạn chế Tốt Tốt
Đồng thời Hạn chế Tốt Tốt
Kích thước Nhỏ gọn Lớn hơn Lớn hơn
Chi phí Miễn phí Miễn phí (Community Edition), Trả phí (Enterprise Edition) Miễn phí
Trường hợp sử dụng Ứng dụng nhỏ, cục bộ, testing Ứng dụng web lớn, lưu lượng truy cập cao Ứng dụng web lớn, yêu cầu tính năng nâng cao

Kết luận

Bài viết này đã cung cấp cho bạn một hướng dẫn toàn diện về việc sử dụng SQLite trong NodeJS. Bạn đã học cách cài đặt sqlite3, tạo và kết nối đến cơ sở dữ liệu, tạo bảng, thêm, truy vấn, cập nhật và xóa dữ liệu, sử dụng prepared statements để ngăn chặn SQL injection, xử lý lỗi, sử dụng transactions, tối ưu hiệu suất và các trường hợp sử dụng phổ biến.

Với kiến thức này, bạn có thể bắt đầu sử dụng SQLite trong các dự án NodeJS của mình một cách tự tin và hiệu quả. Hãy thử nghiệm và khám phá thêm các tính năng khác của SQLite để tận dụng tối đa sức mạnh của nó.

FAQ

1. SQLite có phù hợp cho ứng dụng web quy mô lớn không?

Không, SQLite không được thiết kế để xử lý đồng thời lượng lớn kết nối và không có các tính năng mở rộng như MySQL hoặc PostgreSQL. Nó phù hợp hơn cho các ứng dụng nhỏ hoặc trung bình.

2. Làm thế nào để bảo vệ cơ sở dữ liệu SQLite khỏi truy cập trái phép?

Bạn có thể sử dụng các phương pháp sau:

  • Giới hạn quyền truy cập: Đảm bảo rằng chỉ những người dùng cần thiết mới có quyền truy cập vào tệp cơ sở dữ liệu.
  • Mã hóa cơ sở dữ liệu: Mã hóa tệp cơ sở dữ liệu để ngăn chặn truy cập trái phép nếu tệp bị đánh cắp.
  • Sử dụng prepared statements: Ngăn chặn SQL injection.

3. Làm thế nào để sao lưu và phục hồi cơ sở dữ liệu SQLite?

Sao lưu: Sao chép tệp cơ sở dữ liệu.

Phục hồi: Thay thế tệp cơ sở dữ liệu hiện tại bằng tệp sao lưu.

4. Làm thế nào để sử dụng SQLite với ORM (Object-Relational Mapping)?

Bạn có thể sử dụng các ORM như Sequelize hoặc TypeORM để tương tác với SQLite. ORM giúp bạn làm việc với cơ sở dữ liệu bằng các đối tượng JavaScript thay vì câu lệnh SQL.

5. Tôi gặp lỗi “SQLITE_BUSY”. Lỗi này có nghĩa là gì và làm thế nào để khắc phục nó?

Lỗi “SQLITE_BUSY” có nghĩa là cơ sở dữ liệu đang bị khóa bởi một tiến trình khác. Điều này thường xảy ra khi bạn cố gắng truy cập cơ sở dữ liệu từ nhiều tiến trình đồng thời. Để khắc phục, bạn có thể:

  • Sử dụng transactions: Đảm bảo rằng các thao tác cơ sở dữ liệu được thực hiện trong một transaction.
  • Tăng timeout: Tăng thời gian chờ trước khi trả về lỗi.
  • Sử dụng connection pooling: Sử dụng connection pooling để quản lý kết nối cơ sở dữ liệu.

6. SQLite có hỗ trợ các kiểu dữ liệu JSON không?

Có, SQLite hỗ trợ kiểu dữ liệu JSON từ phiên bản 3.9. Bạn có thể lưu trữ và truy vấn dữ liệu JSON trong SQLite.

7. Làm thế nào để xem nội dung của cơ sở dữ liệu SQLite?

Bạn có thể sử dụng các công cụ sau:

  • SQLite command-line tool: Một công cụ dòng lệnh để tương tác với cơ sở dữ liệu SQLite.
  • DB Browser for SQLite: Một công cụ GUI miễn phí và mã nguồn mở để quản lý cơ sở dữ liệu SQLite.