Ứng dụng web sử dụng DynamoDB, Typescript, Express, Generic repository pattern…(Phần 02) – DynamoDB local

Đây là bài viết thuộc phần 2 của series. Trong bài viết này, chúng ta sẽ đi từng bước để cài đặt DynamoDB trên môi trường local, và thực hiện một vài thao tác đơn giản với DynamoDB.

DynamoDB locally trên máy tính của bạn

Về cơ bản bài viết sẽ giới thiệu 2 cách để có thể cài đặt được DynamoDB local: Docker, .jar file.

Cách 1: Docker

Cá nhân mình khuyến khích sử dụng cách dùng Docker. Cách này cực kỳ tiện lợi, chỉ chạy một lệnh là xong. Nếu không dùng nữa các bạn chỉ cần xóa bỏ đi là được.

Đảm bảo máy tính các bạn đã cài đặt sẵn Docker.

AWS có sẵn một docker image cho DynamoDB , về cơ bản thì image này đủ dùng cho chúng ta.

Nhưng mình khuyến khích các bạn mới bắt đầu nên sử dụng dynamo-local-admin image. Image này có sử dụng package dynamodb-admin là công cụ giúp bạn làm việc với DynamoDB thông qua 1 giao diện web khá trực quan.

Để chạy DynamoDB container, hãy sử dụng lệnh (bạn có thể chạy lệnh này ở bất kỳ đâu):

docker run -p 8000:8000 -it --rm instructure/dynamo-local-admin

Các bạn chú ý, mình có thêm tùy chọn --rm , có nghĩa là khi các bạn ngừng tiến trình trên terminal (Ctrl + C, hay Control + C) thì dữ liệu sẽ bị xóa hết.

Lần đầu chạy có thể sẽ hơi lâu, máy tính của bạn sẽ tải image instructure/dynamo-local-admin từ Internet xuống, từ lần sau thì không cần nữa.

Khi các bạn thấy kết quả tương tự như thế này, nghĩa là đã thành công:

Các bạn truy cập vào link: http://localhost:8000/ bạn sẽ thấy giao diện quản lý các bảng của DynamoDB.

Cách 2: Cài đặt bằng file .jar từ AWS

Cách này yêu cầu bạn cài đặt DynamoDB “thủ công”. Tài liệu hướng dẫn có tại đây.

Đảm bảo máy tính của bạn đã cài đặt Java Runtime Environment (JRE) với version lớn hơn 6. Kiểm tra bằng lệnh java --version , kết quả trả về tương tự như sau:

java 10 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10+46)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)

Tiếp theo, tải file dynamodb_local_latest.zip (các bạn có thể chọn tải file khác). Sau khi tải được file, các bạn giải nén ra một thư mục, ví dụ: dynamodb_local.

Tại thư mục bạn mới giải nén, chạy lệnh:

java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb

Kết quả mong muốn:

~/Downloads/dynamodb_local 
➜ java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb
Initializing DynamoDB Local with the following configuration:
Port:	8000
InMemory:	false
DbPath:	null
SharedDb:	true
shouldDelayTransientStatuses:	false
CorsParams:	*

Như vậy là bạn đã thành công. Hãy thử truy cập vào đường dẫn http://localhost:8000/shell/ (DynamoDB shell) ,các bạn sẽ thấy giao diện để làm việc với DynamoDB. Hãy thử liệt kê các bảng đang có bằng đoạn mã sau:

var params = {
  ExclusiveStartTableName: 'table_name',
  Limit: 10,
};
dynamodb.listTables(params, function (err, data) {
  if (err) ppJson(err);
  else ppJson(data);
});

Đây là mã Javascript, và kết quả nhận được sẽ tương tự như sau (nếu bạn chưa tạo bảng nào)

Tới đây các bạn có thể cài đặt https://www.npmjs.com/package/dynamodb-admin để thao tác với DynamoDB local. Từ bước này trở đi, mình sẽ sử dụng công cụ này thay vì DynamoDB shell.

Tạo bảng và thêm dữ liệu mẫu

Trong bài viết, chúng ta sẽ sử dụng bảng và dữ liệu như trong hướng dẫn này – Node.js and DynamoDB

Chuẩn bị

Quay lại project, chúng ta cần thêm một số thứ để bắt đầu có thể làm việc với DynamoDB, những thứ này sẽ giúp ích cho cả sau này nữa.

Các package bắt đầu được sử dụng ở bài viết này sẽ là: dotenv, aws-sdk.

npm install aws-sdk dotenv -S
  • dotenv: Giúp bạn làm việc với file định nghĩa biến môi trường (thường là .env). Package giúp load toàn bộ biến môi trường được định nghĩa ở file .env vào chương trình.
  • aws-sdk: Package giúp bạn làm việc với các dịch vụ của AWS, trong trường hợp này là AWS DynamoDB.

Một vài package định nghĩa kiểu cho các package ở trên:

npm install @types/dotenv @types/node

(aws-sdk đã hỗ trợ sẵn Typescript)

Tạo bảng Movies

Theo hướng dẫn, chúng ta sẽ tạo bảng Movies, bảng này có key bao gồm:

  • year: Kiểu dữ liệu là number. Đóng vai trò là HASH KEY hay Partition key
  • title: Kiểu dữ liệu là string. Đóng vai trò là RANGE KET hay Sort key

Về việc thiết kế cơ sở dữ liệu với DynamoDB thì sẽ có phần hơi “dị” so với các hệ cơ sở dữ liệu khác. Mình sẽ nói tới trong một bài viết khác, ở bài viết này tạm thời chúng ta chỉ làm theo.

Bắt đầu, để kết nối được tới DynamoDB chúng ta cần các thông tin: endpoint, region.

Ở thư mục gốc của dự án (không phải src), chúng ta tạo ra một file .env (tên như vậy luôn :D), file này định nghĩa các biến môi trường sẽ dùng trong chương trình. Biến môi trường giúp bạn linh hoạt và bảo mật các giá trị nhạy cảm. Vì vậy các giá trị trong file này là “bí mật” chỉ những người nên biết mới được biết, và nếu bạn dùng git thì .env phải có trong file .gitignore .

Nội dung .env trong dự án của chúng ta sẽ như sau:

DYNAMO_ENDPOINT=http://localhost:8000
REGION=local

Là các thông tin để kết nối tới DynamoDB, tùy thuộc ở bước trên bạn để DynamoDb lắng nghe ở cổng số bao nhiêu mà định nghĩa giá trị DYNAMO_ENDPOINT phù hợp (mặc định là http://localhost:8000).

Tạo file src/config/index.ts , file này sẽ chứa các giá trị dùng chung cho toàn bộ dự án, các giá trị có thể là hằng số, hoặc được lấy từ biến môi trường:

import dotenv from 'dotenv';

dotenv.config(); // load giá trị từ file .env

export const ENVIRONMENTS = {
  DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT || 'http://localhost:8000',
  REGION: process.env.REGION || 'local',
};

Tiếp theo các bạn tạo file src/migrate/create-tables.ts, file này khi được chạy sẽ tạo ta bảng Movies

/* tslint:disable:no-console */

import { DynamoDB } from 'aws-sdk';
import { CreateTableInput } from 'aws-sdk/clients/dynamodb';
import { ENVIRONMENTS } from '../config';

(async () => {
  const dynamodb = new DynamoDB({
    endpoint: ENVIRONMENTS.DYNAMO_ENDPOINT,
    region: ENVIRONMENTS.REGION,
  });

  const params: CreateTableInput = {
    TableName: 'Movies',
    KeySchema: [
      { AttributeName: 'year', KeyType: 'HASH' },  // Partition key
      { AttributeName: 'title', KeyType: 'RANGE' },  // Sort key
    ],
    AttributeDefinitions: [
      { AttributeName: 'year', AttributeType: 'N' },
      { AttributeName: 'title', AttributeType: 'S' },
    ],
    ProvisionedThroughput: {
      ReadCapacityUnits: 10,
      WriteCapacityUnits: 10,
    },
  };

  try {
    const result = dynamodb.createTable(params).promise();
    console.log('Created table. Table description JSON:', JSON.stringify(result, null, 2));
  } catch (error) {
    console.error('Unable to create table. Error JSON:', JSON.stringify(error, null, 2));
  }
})();

Tạo ra một DynamoDB instance sử dụng các giá trị được định nghĩa trong file config.ts, tạo ra một bảng với cấu hình định trước.

Để sử dụng cú phát async/await,  mình sử dụng cú pháp IIFE (immediately invoked function expression) để tạo ra một async function gói toàn bộ nội dung chương trình.

Hàm createTable, thông thường cần truyền vào 2 tham số, tham số thứ 2 là callback function, nhưng mình đã sử dụng hàm .promise() để hàm return một Promise (chi tiết).

Để thực thi file này thì như đã nói ở Phần 01, chúng ta sẽ có 2 cách: Build rồi chạy file .js, chạy trực tiếp file .ts. Ở đây mình dùng cách chạy trực tiếp file .ts:

ts-node ./src/migrate/create-tables.ts

Kết quả mong muốn:

Created table. Table description JSON:

Và khi truy cập vào đường dẫn: http://localhost:8000/ , chúng ta sẽ thấy bảng Movies

Thêm dữ liệu mẫu

Chúng ta sẽ thêm một số dữ liệu mẫu vào bảng Movies vừa tạo ở trên.

Các bạn tải file zip chứa dữ liệu mẫu từ link này: moviedata.zip (Nếu link bị hỏng, các bạn có thể tìm thấy file dữ liệu mẫu ở đây).

Sau khi tải được file dữ liệu mẫu, các bạn giải nén file moviedata.json vào thư mục ./src/migrate/data

Tạo file src/migrate/load-simple-movies.ts

/* tslint:disable:no-console */

import { DynamoDB } from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import fs from 'fs';
import { ENVIRONMENTS } from '../config';

(async () => {
  const docClient = new DynamoDB.DocumentClient({
    endpoint: ENVIRONMENTS.DYNAMO_ENDPOINT,
    region: ENVIRONMENTS.REGION,
  });

  console.log('Importing movies into DynamoDB. Please wait.');

  const allMovies: {
    year: number;
    title: string;
    info: string;
  }[] = JSON.parse(fs.readFileSync(`${__dirname}/data/moviedata.json`, 'utf8'));

  let promisesBatch: Promise<any>[] = [];
  for (const movie of allMovies) {
    const params: DocumentClient.PutItemInput = {
      TableName: 'Movies',
      Item: {
        'year':  movie.year,
        'title': movie.title,
        'info':  movie.info,
      },
    };

    const promise = docClient.put(params).promise()
      .then(_ => {
        console.error('Done: ', movie.title);
      })
      .catch(reason => {
        // Skip error and continue
        console.error('Error JSON:', JSON.stringify(reason, null, 2));
      });

    promisesBatch.push(promise);

    if (promisesBatch.length === 100) {
      await Promise.all(promisesBatch);
      // Clear batch
      promisesBatch = [];
    }
  }

  // Final block
  await Promise.all(promisesBatch);

  console.error('Done!');
})();

Đoạn chương trình thực hiện các công việc:

  1. Tạo đối tượng docClient, kết nối tới DynamoDB.
  2. Đọc nội dung file moviedata.json. Cast từ json string thành một array object.
  3. Lặp qua từng object của array ở trên. Với mỗi object, tạo ra một promise để chèn dữ liệu tương ứng vào DynamoDB. Promise này nếu bị lỗi (nhảy vào catch block) thì chỉ thông báo ra màn hình, việc thêm 1 object bị lỗi sẽ không ảnh hướng tới những object khác ở bước sau.
  4. Mỗi khi gom đủ 100 promises ở bước trên, sẽ chờ cho việc chèn 100 object kết thúc rồi tiếp tục quay lại bước 3, cho tới khi duyệt hết các phần tử mảng.
  5. Đợi chèn hết những object cuối cùng vào DynamoDB, những object là phần lẻ của việc xử lý trong vòng lặp.

Việc này sẽ mất khoảng 2 phút (tùy máy tính), sẽ có khoảng hơn 4000 phần tử sẽ được thêm vào cơ sở dữ liệu. Việc bỏ đi các đoạn console.log tròng vòng lặp có thể giúp cho tiến trình chạy nhanh hơn chút.

Chúng ta bắt đầu chạy tiến trình bằng lệnh:

ts-node ./src/migrate/load-simple-movies.ts

Sau khi hoàn thành, quay trở lại trang http://localhost:8000/tables/Movies chúng ta sẽ thấy các record trong bảng Movies:

Kết thúc phần 02

Tổng kết cho phần này: Cài đặt DynamoDB local, tạo ra các đoạn script cho việc tạo bảng và insert dữ liệu mẫu vào bảng.

Ở phần tiếp theo chúng ta sẽ tạo đối tượng Entity tương ứng cho bảng Movies, các entity này đại diện cho các bảng trong DB, thông qua đây chúng ta có thể biết một record trong bảng sẽ chứa các thông tin gì…

Đây là những gì được thêm vào, thay đổi so với Phần 01: Commit

Đây là project sau khi kết thúc phần 02: Github