Daily Archives: 07/10/2016

NodeJS – Lưu trữ dữ liệu với SQLite3

Trong bài này chúng ta sẽ tìm hiểu cách lưu trữ dữ liệu cho ứng dụng ghi chú trong cơ sở dữ liệu SQLite3.

SQLite3

SQLite3 là một cơ sở dữ liệu SQL, đặc điểm của SQLite3 là rất nhỏ, nhẹ, dễ cài đặt, phù hợp với nhiều loại ứng dụng. Điểm đặc biệt của SQLite3 là chúng ta không cần một server, không cần các bước cài đặt phức tạp, rườm rà.

Nếu bạn đã có trình sqlite3.exe trên máy thì bạn có thể bỏ qua phần này và kéo xuống phần tạo cơ sở dữ liệu để đọc tiếp. Nếu chưa thì đầu tiên bạn tải công cụ của sqlite3 tại địa chỉ https://sqlite.org/download.html

Bạn tải các tool về và giải nén theo hệ điều hành mà bạn đang dùng, chẳng hạn như bạn dùng Windows thì tải file sqlite-tools-win32-x86-3140200.zip (1.52 MiB) về:

capture:

Tiếp theo bạn nên (hoặc phải) đưa đường dẫn đến thư mục chứa các file vừa được giải nén này vào biến môi trường PATH để tiện sử dụng sau này:

Ví dụ với Windows 10:

Bạn bấm chuột phải vào My Computer → Properties → Advanced system settings → Environment Variables, sau đó click chọn biến Path trong phần System variables rồi chèn thêm đường dẫn đến thư mục sqlite3 vừa giải nén vào. Chẳng hạn ở đây mình giải nén vào thư mục F:\DevSoft\sqlite3 thì mình có hình như sau:

capture

Bây giờ chúng ta kiểm tra xem sqlite3 đã được đưa vào biến môi trường Path chưa thì bạn mở terminal lên (Cmd trong Windows) rồi gõ lệnh sqlite3 -version để xem phiên bản sqlite3.

capture

Nếu bạn ra được giống như hình trên thì đường dẫn đến thư mục chứa sqlite3 của bạn đã hoàn toàn nằm trong biến môi trường Path rồi. Ở đây mình dùng SQLite3 phiên bản 3.14.2 như trong hình.

Tạo cơ sở dữ liệu

Bây giờ chúng ta sẽ tạo cơ sở dữ liệu để lưu các ghi chú cho ứng dụng Notes.

Đầu tiên bạn mở terminal lên rồi trỏ đến đường dẫn thư mục của project, sau đó gõ đoạn lệnh sau:

C:\NodeJS\notes>sqlite3 db.sqlite3
SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> CREATE TABLE notes (
   ...> notekey VARCHAR(255),
   ...> title VARCHAR(255),
   ...> author VARCHAR(255),
   ...> body TEXT
   ...> );
sqlite>

Lệnh sqlite3 db.sqlite3 sẽ tạo một file có tên db.sqlite3 ở thư mục hiện tại, nếu file đã tồn tại thì mở file đó ra để đọc/ghi, file này sẽ được dùng để lưu dữ liệu. Sau đó bạn có thể gõ các lệnh truy vấn trong ngôn ngữ SQL như bình thường.

Vậy là xong, đoạn lệnh trên sẽ tạo cơ sở dữ liệu lưu trong file db.sqlite3, trong cơ sở dữ liệu này có một bảng tên là notes với các trường như trên.

Tạo model

Giống như các phần trước, bây giờ chúng ta sẽ tạo một thư mục có tên models-sqlite3 nằm trong thư mục gốc của project (thư mục chứa các thư mục như bin, public, models, models-fs…). Trong thư mục này chúng ta tạo một file có tên notes.js với nội dung như sau:

var sqlite3 = require('sqlite3');
sqlite3.verbose();
var db = undefined;
exports.connect = function(dbname, callback) {
    db = new sqlite3.Database(dbname, 
        sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
        function(err) {
            if(err)
                callback(err);
            else
                callback(err);
    });
}
exports.disconnect = function(callback) {
     callback();
}

exports.create = function(key, title, body, callback) { 
     db.run("INSERT INTO notes(notekey, title, body) " +
         "VALUES (?, ?, ?);",
         [key, title, body],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback();
     });
}

exports.update = function(key, title, body, callback) { 
     db.run("UPDATE notes " + 
         "SET title = ?, body = ? " +
         "WHERE notekey = ?;",
         [ title, body, key ],
         function(err) { 
             if(err)
                 callback(err);
             else
                 callback();
     });
}

exports.read = function(key, callback) { 
     db.get("SELECT * FROM notes WHERE notekey = ?;",
     [key],
     function(err, row) { 
         if(row == undefined)
             callback("Something wrong");
         else if(err) 
             callback(err);
         else
             callback(null, row);
     });
}

exports.destroy = function(key, callback) {
     db.run("DELETE FROM notes WHERE notekey = ?;",
         [ key ],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback(err);
     });
}

exports.titles = function(callback) {
    var titles = [];
    db.each("SELECT notekey, title FROM notes",
        function(err, row) {
            if(err)
                callback(err);
            else
                titles.push({
                key: row.notekey,
                title: row.title
            });
        },
        function(err, num) {
            if(err)
                callback(err);
            else
                callback(null, titles);
    });
}

File này sẽ chứa các hàm hỗ trợ đọc/ghi dữ liệu với cơ sở dữ liệu SQLite3.

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

Ở đây chúng ta dùng module sqlite3. Hàm verbose() có chức năng bật chế độ stack trace, tức là khi có lỗi xảy ra, bạn sẽ biết câu lệnh nào đã gây ra lỗi đó để sửa cho dễ.

var db = undefined;
exports.connect = function(dbname, callback) {
    db = new sqlite3.Database(dbname, 
        sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
        function(err) {
            if(err)
                callback(err);
            else
                callback(err);
    });
}
exports.disconnect = function(callback) {
     callback();
}

Giống với bài trước, ở đây chúng ta cũng tạo một biến để lưu đối tượng cơ sở dữ liệu và định nghĩa 2 hàm để kết nối và ngắt kết nối với cơ sở dữ liệu. Chúng ta viết hàm connect() có chức năng là kết nối CSDL, hàm này nhận vào tên file CSDL và một hàm callback. Hàm sqlite3.Database() sẽ thực hiện việc kết nối, hàm này nhận vào tên CSDL, chế độ mở và một hàm callback. Chế độ mở ở trên là OPEN_READWRITEOPEN_CREATE, tức là mở để đọc/ghi và tạo CSDL.

exports.create = function(key, title, body, callback) { 
     db.run("INSERT INTO notes(notekey, title, body) " + 
         "VALUES (?, ?, ?);",
         [key, title, body],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback();
     });
}

Hàm create() sẽ được dùng để tạo các ghi chú và lưu vào cơ sở dữ liệu. Ở đây chúng ta dùng hàm run() để thực thi các câu truy vấn SQL. Trong các câu truy vấn chúng ta có thể truyền tham số vào dưới dạng dấu chấm hỏi. Các tham số truyền vào sẽ nằm trong mảng phía sau câu truy vấn. Mục đích của việc truyền tham số là để chống lại tấn công SQL Injection.

exports.update = function(key, title, body, callback) { 
     db.run("UPDATE notes " + 
         "SET title = ?, body = ? " +
         "WHERE notekey = ?;",
         [ title, body, key ],
         function(err) { 
             if(err)
                 callback(err);
             else
                 callback();
     });
}

Không như các phần trước, hàm update()create() là một thì ở đây 2 hàm này là 2 hàm khác nhau, vì chúng ta phải chạy các câu truy vấn khác nhau. Để chạy câu truy vấn UPDATE thì chúng ta cũng làm giống như trên là dùng hàm run().

exports.read = function(key, callback) { 
     db.get("SELECT * FROM notes WHERE notekey = ?;",
     [key],
     function(err, row) { 
         if(row == undefined)
             callback("Something wrong");
         else if(err) 
             callback(err);
         else
             callback(null, row);
     });
}

Hàm read() được dùng để truy vấn thông tin của một ghi chú cụ thể, khác với UPDATEINSERT, câu truy vấn SELECT phải dùng hàm get() chứ không dùng hàm run(). Tức là các câu truy vấn nào mà có trả về dữ liệu thì chúng ta dùng get(), còn không trả về dữ liệu thì dùng run(), cú pháp của hàm get() cũng giống với cú pháp của hàm run(), chỉ khác ở chỗ là hàm callback sẽ nhận thêm một tham số nữa là dữ liệu trả về từ CSDL (tham số row), ngoài ra tham số row có thể sẽ là undefined nếu câu truy vấn trả về là rỗng, nên chúng ta phải kiểm tra trước.

exports.destroy = function(key, callback) {
     db.run("DELETE FROM notes WHERE notekey = ?;",
         [ key ],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback(err);
     });
}

Hàm destroy() được dùng để xóa một ghi chú, tương tự như 2 hàm create()update(), ở đây chúng ta cũng dùng hàm run().

exports.titles = function(callback) {
    var titles = [];
    db.each("SELECT notekey, title FROM notes",
        function(err, row) {
            if(err)
                callback(err);
            else
                titles.push({
                key: row.notekey,
                title: row.title
            });
        },
        function(err, num) {
            if(err)
                callback(err);
            else
                callback(null, titles);
    });
}

Hàm titles() được dùng để truy vấn danh sách các ghi chú có trong CSDL, chúng ta dùng hàm each() để truy vấn, tham số đầu tiên của hàm này là câu truy vấn, tham số thứ 2 và thứ 3 là một hàm callback. Khi kết quả truy vấn từ CSDL trả về lại hàm này thì hàm Node sẽ thực hiện lặp qua từng dòng dữ liệu và mỗi lần lặp thì gọi hàm callback thứ 2 và truyền dữ liệu của dòng đó vào hàm này, ở đây mỗi lần lặp chúng ta chèn dữ liệu của hàm đó vào mảng titles[]. Đến khi lặp hết thì hàm callback thứ 3 sẽ được gọi, hàm này ngoài tham số lỗi err thì còn có tham số num là số dòng đã trả về từ CSDL, tại đây chúng ta gọi lại hàm đã gọi titles() và truyền vào mảng titles[] nếu không có lỗi.

Cấu hình project

Chúng ta sửa lại file app.js như sau:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
var models = require('./models-sqlite3/notes');
models.connect("./db.sqlite3", function(err) {
    if(err)
    throw err;
});
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
       message: err.message,
       error: {}
    });
});


module.exports = app;

Ở đây chúng ta tạo lại đối tượng model mới từ module notes trong thư mục models-sqlite3.

Như đã nói, ở đây chúng ta dùng module sqlite3 nên chúng ta phải khai báo trong file package.json để npm cài thêm module này cho chúng ta như sau:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*"
    }
}

Sau khi đã khai báo xong thì chúng ta chạy lệnh npm install để cài, tuy nhiên module này có hơi không được bình thường cho lắm, nếu bạn chạy lệnh này mà thấy một đống dòng thông báo lỗi dạng như hình dưới đây:

 

thì tức là trên npm không có sẵn thư viện này cho hệ điều hành cùng với phiên bản Node.js của bạn, và npm đã chuyển sang cách cài khác là dịch thư viện sqlite3 từ mã nguồn để cài, tuy nhiên việc dịch vẫn không thành công. Lý do là vì máy của bạn thiếu một trong các trình biên dịch Visual C++ hoặc Python 2, hoặc có thể thiếu cả 2. Việc bạn cần làm là lên mạng tải và cài đặt cho đủ 2 trình biên dịch này, sau đó chạy lại lệnh npm install là được, Visual C++ thì bạn có thể cài từ Visual Studio Community cũng được, Python phải là dòng Python 2 chứ không phải dòng Python 3.

Sau đó bạn có thể chạy lệnh npm start để chạy ứng dụng được rồi, và lần này dữ liệu đã được lưu trong file cơ sở dữ liệu db.sqlite3 ở thư mục gốc của project.

Bạn có thể mở terminal lên và chạy sqlite3 bằng dòng lệnh để kiểm tra:

capture

capture