[Nodejs] - Working With Database(Part 2)

Overview

Ở các phần trước các bạn đã được hướng dẫn xây dựng API cho ứng dụng todolist bao gồm các chứ năng quản lý (CRUD) các object todos, chức năng filter , tìm kiếm. Ở phần tiếp theo mình sẽ hướng dẫn các bạn xây dựng các phương thức để connect DB, thực hiện các câu query để truy vấn dữ liệu trong database thay vì fix cứng dữ liệu như trước. Cuối cùng là chúng ta sẽ thực hiện deploy ứng dụng lên heroku và sử dụng DB engine của postgres để cung chạy dịch vụ mà ta vừa tạo ra.Nào cùng bắt tay vào thực hiện.

Tip: Khi sử dụng postman để không mất thời gian vào việc lặp lại đường dẫn URL nhiều lần hãy save các request cơ bản vào một thư mục để bạn có thể test dễ dàng hơn:

Installing Sequelize and Sqlite

Phần tiếp theo sẽ sử dụng 2 module sau:

npm install sequelize --save

npm install sqlite3 --save

Sequelize giống như một framework cho Database, sequelize còn được định nghĩa là một ORM cho nodejs. Thay vì truy vấn DB với các câu query thuần thì bây giờ mình sẽ sử dụng các hàm do sequelize cung cấp để truy vấn DB. Sequelize còn hỗ trợ các database ví dụ: MySQL, PostgreSQL, SQLite. Để có thể hiểu rõ hơn bạn có thể đọc tài liệu về sequelize tại http://docs.sequelizejs.com/manual/installation/getting-started.html. Trong document đã hưỡng dẫn đầy đủ: cách tạo connection pool với DB, các câu query dùng để truy vấn DB...

  • Setting up a connection
var Sequelize = require('sequelize');
var sequelize = new Sequelize(database, username, password, {
  'dialect': 'sqlite',
  
  // dùng trong trường hợp SQLite 
  'storage': __dirname + '/basic-sqlite-database.sqlite'
});
  • Your first model
var Todo = sequelize.define('todo', {
  description: {
    type: Sequelize.STRING
  },
  completed: {
    type: Sequelize.BOOLEAN
  }
});
  • Test the connection
sequelize.sync().then(function () {
  console.log('Every thing is synced');
});

Sau khi save file và chạy thử thì ta sẽ có một file mới được tạo trong cùng thư mục, để mở file có thể download tool tại đây http://sqlitebrowser.org/. Trong log hệ thống hiển thị như sau:

Executing (default): CREATE TABLE IF NOT EXISTS `todos` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `description` VARCHAR(255), `completed` TINYINT(1),
`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`todos`)
Every thing is synced

Adding Model Validation & Fetching Models

Để định nghĩa một model trong sequelize ta sẽ sử dụng hàm define(http://docs.sequelizejs.com/manual/tutorial/models-definition.html) , khi đó sequelize sẽ thực hiện cơ chế mapping giữa model và table trong DB. Trong document của sequelize còn giới thiệu về: timestamps, primary key, data type ...

Ngoài ra là một phần không thể thiếu được đó là validate các attribute của một model. Ví dụ: trong model todo, thì phần description yêu cầu không dài quá 255 kí tự, không được phép để trống, phần, phần completed có thể tạo mặc định là false. Đó là các yêu cầu đặt ra tiếp theo là việc implement:

var Todo = sequelize.define('todo', {
  description: {
    type: Sequelize.STRING,
    allowNull: false,
    validate: {
      len: [1, 255]
    }
  },
  completed: {
    type: Sequelize.BOOLEAN,
    allowNull: false,
    defaultValue: false
  }
});

Để kiểm tra việc validate trong model Todo vừa thêm vào ta có thể tạo như sau: Case 1: tạo mới todo nhưng không có description -> thông báo lỗi thiếu description

sequelize.sync().then(function () {
  console.log('Every thing is synced');

  Todo.create({
    completed: false,
  }).then(function (todo) {
    console.log(todo);
  }).catch(function(e){
    console.log(e);
  });
});

Case 2: tạo mới todo nhưng không có completed -> tạo thành công với completed = false

sequelize.sync().then(function () {
  console.log('Every thing is synced');

  Todo.create({
    description: 'Take out trash',
  }).then(function (todo) {
    console.log(todo);
  }).catch(function(e){
    console.log(e);
  });
});

POST todos

Trước tiên để bắt đầu implement code ta thêm mới các folder như dưới đây

project_name
├── data
├── models
│   └──  todo.js
├── nodes_modules
├── db.js
├── package.json
├── server.js
└── .gitignore

Đầu tiên trong file db.js ta sẽ khai báo module sequelize tương tự như ở phần trên tuy nhiên để có thể tái sử dụng connect DB ta sẽ export module thành một object như sau:

var Sequelize = require('sequelize');
var sequelize = new Sequelize(undefined, undefined, undefined, {
  'dialect': 'sqlite',
  'storage': __dirname + '/data/todo-api.sqlite'
});

var db = {};

db.todo = sequelize.import(__dirname + '/models/todo.js');
db.sequelize = sequelize;
db.Sequelize = Sequelize

module.exports = db;

Trong đoạn code trên xuất hiên thêm một function khác của sequelize đó là import. Sử dụng import là một cách khác để định nghĩa một model trong ứng dụng node thay vì tạo trực tiếp trong file db dẫn đến hiện tường phình to file => khó bảo trì, việc tách riêng các file định nghĩa model sẽ dễ bảo trì và hiển thị nhìn sẽ logic hơn:

Trong file todo.js ta sẽ copy lại phần tạo model todo được việc ở phần trên:

module.exports = function (sequelize, DataTypes) {
  return sequelize.define('todo', {
    description: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        len: [1, 255]
      }
    },
    completed: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
      defaultValue: false
    }
  });
};

Cuối cùng trong file server.js sẽ được thay đổi để tạo kết nối với DB, trước tiên là import file db.js vừa tạo vào server như sau:

var db = require('./db.js');

Tiếp theo ta sẽ sửa lại thành khi sequelize khởi tạo kết nối thành công thì chạy server API, sequelize sử dụng cơ chế promise mà mình đã giới thiệu ở bài trước :

db.sequelize.sync().then(function () {
  app.listen(PORT, function() {
    console.log('Express listening on port' + PORT + '!');
  });
});

Phần cuối cùng ta sẽ sửa phương thức /POST để có thể tạo todo mới, ý tưởng ban đầu thì không thay đổi tuy nhiên thay vì push object mới vào mảng todo ta sẽ truy vấn để thêm mới như sau:

// POST /todos
app.post('/todos', function(req, res) {
  var body = _.pick(req.body, 'description', 'completed');

  db.todo.create(body).then(function (todo) {
    res.json(todo.toJSON());
  }).catch(function (e) {
    res.status(400).json(e);
  });
});

Lúc này khi chạy lại server ta có thể vào post man và tạo request thêm mới một todo object . Sau đó kiểm tra trong thư mục data vừa tạo kiểm tra trong file sqlite dữ liệu mới đã được thêm vào chưa. Ví dụ:

GET todos and GET by todos id

Trong phần này chúng ta sẽ xử lý 2 phần đó là tìm kiếm dựa trên id và thay đổi phần filter, search của phần get todo object. Trước tiên ta sẽ xử lý phần get todo by id thay vì sử dụng module under score để tìm kiểm dữ liệu trong array todo, trong sequelize cũng hỗ trợ một phương thức để tìm kiếm : findById ta sẽ implement code như sau:

app.get('/todos/:id', function(req, res) {
 var todoId = parseInt(req.params.id, 10);
 db.todo.findById(todoId).then(function (todo) {
   if (todo) {
     res.json(todo.toJSON());
   } else {
     res.status(404).send(); // lỗi không tìm thấy todo
   }
 }).catch(function (e) {
   res.status(500).json(e); // something went wrong
 });
});

Ta sẽ sử dụng postman để kiểm tra lại kết quả vừa được thay đổi . Nếu không có gì sai sót, chúng ta sẽ chuyển sang phần tiếp theo thay đổi filter, search params. Trong sequelize sẽ hỗ trợ phương thức findAll hoặc find trong đó ta có thể truyền where object để filter câu truy vấn, where sẽ có cấu trúc dạng key-value trong key sẽ là thuộc tính của object còn value là giá trị cần tìm kiếm, ngoài ra where còn hỗ trợ cả toán tử AND, OR. Ví dụ

Post.findAll({
 where: {
   authorId: 2
 }
});
// SELECT * FROM post WHERE authorId = 2

Post.findAll({
 where: {
   authorId: 12,
   status: 'active'
 }
});
// SELECT * FROM post WHERE authorId = 12 AND status = 'active';

Tiếp theo là ý tưởng thực hiện: ta sẽ tạo object where để chứa thông tin cần filter và search. Tuy nhiên do phần desccription cần có cơ chế tìm kiếm theo kiểu LIKE trong SQL do đó key description sẽ có nội dung như sau: {$like: '%' + search_content + '%'}. Sau đây là đoạn code của ý tưởng:

app.get('/todos', function(req, res) {
  var query = req.query;
  var where = {};

  if (query.hasOwnProperty('completed') && query.completed == 'true') {
    where.completed = true;
  } else if (query.hasOwnProperty('completed') && query.completed == 'false') {
    where.completed = false;
  }

  if (query.hasOwnProperty('search') && query.search.length > 0) {
    where.description = {
      $like: '%' + query.search + '%'
    };
  }

  db.todo.findAll({where: where}).then(function (todos) {
    res.json(todos);
  }).catch(function (e) {
    res.status(500).json(e);
  });
});

Project DELETE todos id

Tương tự với các phương thức findAll phương thức destroy cũng sẽ nhận object where để xóa object mong muốn. Thông thường với ket object sẽ là id và value sẽ là id của object cần xóa như sau:

Post.destroy({
 where: {
   status: 'inactive'
 }
});

Tương tự với hai phương thức ở trên ta sẽ thay đổi ko tương tác với array bị fix cứng trên server nữa mà tương tác trực tiếp với DB như sau. Có một điều cần lưu ý là sau khi xóa sequelize sẽ trả về số cột bị xóa do đó trong giai đoạn fullfill của promise ta sẽ phải kiểm tra để thông báo kết quả một cách chính xác hơn.

app.delete('/todos/:id', function(req, res) {
 var todoId = parseInt(req.params.id, 10);

 db.todo.destroy({where: {id: todoId}}).then(function(rowsDeleted) {
   if (rowsDeleted == 0) {
     res.status(404).json({
       "error": "no todo found with that id"
     });
   } else {
     res.status(204).send();
   }
 }).catch(function(e){
   res.status(500).json(e);
 });
});

PUT todos id

Cuối cùng là chức năng update nội dung của todo object trong api todololist. Thay vì phải làm việc với một mảng todolist cố định ta sẽ tìm kiếm và update trực tiếp dữ liệu vào SQLite. Như vậy ý tưởng vẫn sẽ là ta sẽ nhận các params truyền lên sau đó tìm kiếm todo object tương ứng nếu không tìm thấy thì sẽ thông báo lỗi 404 (not found), tường hợp tìm kiếm lỗi ta sẽ thực hiện hàm reject trong promise và trả về lỗi 500 (something went wrong). Trong quá trình thực hiện update mà xảy ra lỗi ta sẽ tiếp tục thực hiện phương thức reject và trả về lỗi 400 (bad request).

Để thực hiện ý tưởng nêu ra ở trên trong sequelize module bao gồm các hàm sau: findById, update()

Post.update({
  updatedAt: null,
}, {
  where: {
    deletedAt: {
      [Op.ne]: null
    }
  }
});
// UPDATE post SET updatedAt = null WHERE deletedAt NOT NULL;

Mình sẽ giải thích qua 1 chút ở trên trong đối tượng where với key là deletedAt có giá trị { [Op.ne]: null } (có nghĩa là tìm những giá trị deletedAt not null). Trong câu lệnh có sử dụng toán tử negative của module sequlize ngoài ra còn rất nhiều toàn tử khác(http://docs.sequelizejs.com/manual/tutorial/querying.html) để hỗ trợ xử lý các câu truy vấn phức tạp.

Ở trên mình đã nêu ra ý tưởng thực hiện sau đây là đoạn code để minh họa cho ý tưởng đó:

app.put('/todos/:id', function(req, res) {
  var body = _.pick(req.body, 'description', 'completed');
  var attributes = {}

  var todoId = parseInt(req.params.id, 10);

  if (body.hasOwnProperty('completed')) {
    attributes.completed = body.completed;
  }

  if (body.hasOwnProperty('description')) {
    attributes.description = body.description;
  }

  db.todo.findById(todoId).then(function(todo) {
    if (todo) {
      todo.update(attributes).then(function(todo){
        res.json(todo.toJSON());
      }, function(e) {
        res.status(400).json(e);
      });
    } else {
      res.status(404).send();
    }
  }, function(e) {
    res.status(500).send();
  });
});

Postgres on Heroku and Deploy

Trên heroku không hỗ trợ SQLite do đó ta sẽ sử dụng DB engine khác là postgres DB. Để cài đặt postgres DB cho project sử dụng câu lệnh sau:

npm install pg --save

npm install pg-hstore --save

Trên heroku để thêm addons postgress sql ta sẽ sử dụng câu lệnh sau:

heroku addons:create heroku-postgresql:hobby-dev

heroku pg:wait

Tiếp theo ta sẽ sửa trong file db.js để có thể kết nối postgres engine trên Heroku. Tuy nhiên ta chỉ dùng postgres trên heroku còn với local ta vẫn sử dụng SQLite do đó ta sẽ kiểm tra biến process.env.NODE_ENV trên môi trường production của heroku. Nếu là kết quả trả về là prodution ta sẽ kết nối sequelize với postgres. Ta sẽ thực hiện như sau:

var env = process.env.NODE_ENV || 'development'

if (env == 'production') {
  sequelize = new Sequelize(process.env.DATABASE_URL, {
    'dialect': 'postgres'
  });
} else {
  var sequelize = new Sequelize(undefined, undefined, undefined, {
    'dialect': 'sqlite',
    'storage': __dirname + '/data/todo-api.sqlite'
  });
}

Cuối cùng ta sẽ thực hiện đẩy code mới nhất lên server heroku bạn có thể commit code ngay trên chính nhánh master hoặc checkout ra nhánh mới để có thể tạo một nhánh deploy riêng. Sau khi đã commit code ta sẽ push code mới nhất lên bằng câu lệnh có cấu trúc như sau;

$ git push <remote> <local branch name>:<remote branch to push into>

Với <remote> ở đây là : heroku (kiểm tra xem bạn đã tạo remote chưa bằng câu lệnh git remote -v trước), <local branch name> là nhánh chứa code mới mà bạn muốn deploy, <remote branch to push into> thông thường trên heroku là nhánh master build code . Sau khi push code mới nhất thành công hãy sử dụng câu lệnh heroku open để chạy thử api mà bạn vừa xây dựng được.

Hoặc là thử với api của mình vừa tạo đc tại đây: https://warm-meadow-18486.herokuapp.com/

All Rights Reserved