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

Đây là phần 05 của series. Ở phần trước – Phần 04 chúng ta đã tạo được một generic repository, nhưng lớp này lại chưa thể dùng để làm việc được với dữ liệu các bảng trong cơ sở dữ liệu. Ở phần này, chúng ta sẽ kết hợp Generic repository với Movie Entity class được tạo ở Phần 03 , để thực hiện tạo Movie repository, lớp này có thể được dùng để thao tác với dữ liệu trong bảng Movie trong csdl.

MovieRepository

IMovieKey

Làm việc với dữ liệu với trong DynamoDB phụ thuộc rất nhiều vào Partition key của các bảng. Với bảng Movie cũng như vậy, chúng ta tạo một interface ánh xạ kiểu dữ liệu của bảng Movie.

Cập nhật nội dung file src/entities/MovieDTO.ts

import { MovieInfoDTO } from './MovieInfoDTO';

export interface IMovieKey {
  title: string;
  year: number;
}

export class MovieDTO implements IMovieKey {
  public title: string;
  public year: number;

  public info?: MovieInfoDTO;
}

Vì key của bảng cũng là những trường dữ liệu bắt buộc của mỗi bản ghi, nên chúng ta sẽ có lớp MovieDTO kế thừa lại IMovieKey.

MovieRepository

Tạo file src/repositories/MovieRepository.ts

import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import { Movie } from '../entities/Movie';
import { IMovieKey } from '../entities/MovieDTO';
import { BaseRepository } from './base/BaseRepository';

export class MovieRepository extends BaseRepository<Movie, IMovieKey> {
  public static TABLE_NAME = 'Movies';

  constructor(docClient: DocumentClient) {
    super(docClient, MovieRepository.TABLE_NAME, Movie);
  }

  /**
   * Get movies of a year
   * @param {number} year Year to get
   * @returns {Promise<Movie[]>} List of movies
   */
  public getByYear(year: number): Promise<Movie[]> {
    return this.query({
      KeyConditionExpression: '#yr = :yyyy',
      ExpressionAttributeNames: {
        '#yr': 'year',
      },
      ExpressionAttributeValues: {
        ':yyyy': year,
      },
    });
  }
}

Lớp này kế thừa từ lớp BaseRepository<Movie, IMovieKey> :

  • Movie: Thay thế vị trí kiểu T, những phương thức trả lại kiểu T giờ sẽ trả lại kiểu là Movie.
  • IMovieKey: Thay thế vị trí kiểu K, những input đầu vào có kiểu là K giờ sẽ thay bằng kiểu IMovieKey

Hàm constructor, chúng ta truyền vào đối tượng DocumentClient, đối tượng này sẽ được truyền vào lớp cha, cùng với các giá trị như TABLE_NAME và kiểu class Movie (có được nhắc tới tại Phần 04)

Chúng ta có thêm phương thức getByYear – đây là phương thức mà chỉ ở MovieRepository mới có (không có ở BaseRepository). Phương thức này sử dụng lại phương thức query (ở lớp base):

  • Mục đích: Lấy danh sách các phim của một năm
  • Input: year - number Năm cần lấy danh sách phim
  • Output: Promise<Movie[]> – Danh sách phim trong năm

Lưu ý: Đây chỉ là một phương thức ví dụ cho việc thêm các phương thức đặc trưng cho repository. Với DynamoDb, mỗi lần bạn thực hiện query, bạn chỉ lấy được 1MB dữ liệu, nếu số lượng phim trong một năm lớn, và các bản ghi chứa nhiều thông tin, bạn phải sử dụng ExclusiveStartKey option trong query để đảm bảo lấy được hết dữ liệu đúng.

Làm việc với bảng Movie

Khi đã tạo được lớp MovieRepository, bạn có thể truyền lớp này vào các Service, hoặc bất kỳ đâu để làm việc được với dữ liệu trong bảng Movie.

Giờ chúng ta sẽ thử làm việc với dữ liệu trong bảng Movie, hãy đảm bảo bạn đã chạy DynamoDB ở local và file .env đã khai báo đủ thông tin (Phần 02).

Chúng ta sẽ tạo một instance của lớp MovieRepository, khởi tạo yêu cầu truyền vào đối tượng DocumentClient:

File ./src/index.ts

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

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

  const movieRepo = new MovieRepository(docClient);
})();

Chạy câu lệnh: npm run dev

Lúc này code sẽ chưa in ra gì, vì chúng ta chưa thực hiện gì cả.

Khi thực hiện các phần dưới đây, hãy chạy lại lệnh npm run dev . Chúng ta không có bất kỳ action nào trong stack, nên tiến trình watch sẽ bị ngừng.

Lấy một bản ghi theo key

Chúng ta sẽ lấy ra thông tin bộ phim có tên “Rush” được công chiếu năm 2013.

Sử dụng phương thức findOne

const movie = await movieRepo.findOne({ year: 2013, title: 'Rush' });

console.log('findOne', movie);

Output:

Movie {
  title: 'Rush',
  year: 2013,
  info:
   { actors: [ 'Daniel Bruhl', 'Chris Hemsworth', 'Olivia Wilde' ],
     release_date: '2013-09-02T00:00:00Z',
     plot:
      'A re-creation of the merciless 1970s rivalry between Formula One rivals James Hunt and Niki Lauda.',
     genres: [ 'Action', 'Biography', 'Drama', 'Sport' ],
     image_url:
      'http://ia.media-imdb.com/images/M/MV5BMTQyMDE0MTY0OV5BMl5BanBnXkFtZTcwMjI2OTI0OQ@@._V1_SX400_.jpg',
     directors: [ 'Ron Howard' ],
     rating: 8.3,
     rank: 2,
     running_time_secs: 7380 } }

Nếu các bạn sử dụng một IDE tốt và đã được cấu hình để làm việc với Typescript, thì mọi thứ sẽ rất dễ dàng. Những gợi ý sẽ hiện lên cho bạn biết chính xác những gì bạn có thể dùng, những gì bạn phải điền vào. Ví dụ: gọi hàm findOne của đối tượng movieRepo, chúng ta cần truyền vào một object có thuộc tính title và year. Phương thức này sẽ trả lại một Promise<Movie>, chúng ta dùng từ khóa await để lấy được dữ liệu cần dùng.

Các bạn có thể thấy kiểu dữ liệu output là Movie, có nghĩa là bạn có thể sử dụng các phương thức của Movie Entity (vd: getActors).

Thêm mới dữ liệu

const newMovie = await movieRepo.create({
  title: 'New movie',
  year: 2020,
  info: {
    actors: ['Keanu Reeves'],
  },
});
console.log('New movie\n', newMovie);

Output:

New movie
 Movie {
  title: 'New movie',
  year: 2020,
  info: { actors: [ 'Keanu Reeves' ] } }

Mọi thứ làm việc khá ổn, nhưng chúng ta gặp một vấn đề, lúc khởi tạo đối tượng cần truyền vào có kiểu Partial<Movie>, có nghĩa là mọi trường dữ liệu đều là tùy chọn. Nếu chúng ta tạo mới một bộ phim mà không có title (hoặc year) IDE sẽ không báo lỗi, nhưng khi chạy chúng ta lại nhận được lỗi

ValidationException: One of the required keys was not given a value

Chúng ta sẽ cải tiếng một chút cho hàm create của BaseRepository:

public async create(item: Omit<Partial<T>, keyof K> & K): Promise<T> {
  // Create default partition key
  const putParams: DocumentClient.PutItemInput = {
    ...this.getDefaultParam(),
    Item: item,
  };

  await this.docClient.put(putParams).promise();

  return new this.entityClass(putParams.Item);
}

Bây giờ, khi bạn không truyền vào title hoặc year lúc khởi tạo, IDE sẽ báo lỗi cho bạn biết ngay lúc bạn viết code.

Cập nhật dữ liệu

Cập nhật dữ liệu cho dữ liệu phim mới tạo ở trên:

newMovie.info!.rank = 10;
console.log('Updated movie\n', await movieRepo.update(newMovie, newMovie));

Output:

Updated movie
 Movie {
  title: 'New movie',
  year: 2020,
  info: { actors: [ 'Keanu Reeves' ], rank: 10 } }

Xóa dữ liệu

Chúng ta sẽ xóa bản ghi của bộ phim mới tạo ở trên:

try {
  const deleteResult = await movieRepo.delete({
    title: newMovie.title,
    year: newMovie.year,
  });
  console.log('Delete result', deleteResult);

  await movieRepo.findOne({
    title: newMovie.title,
    year: newMovie.year,
  });
} catch (e) {
  console.log(e);
}

Output:

Delete result true
{ EntityNotFoundException: Movies not found!
    at MovieRepository.findOne (/dynamodb-generic-repository/src/repositories/base/BaseRepository.ts:58:13)
    at processTicksAndRejections (internal/process/next_tick.js:81:5) name: 'EntityNotFoundException' }

Chúng ta có thấy lỗi EntityNotFoundException do thực hiện phương thức findOne với một bản ghi không tồn tại.

File ./src/index.ts chứa toàn bộ ví dụ:

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

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

  const movieRepo = new MovieRepository(docClient);

  // findOne
  const movie = await movieRepo.findOne({ year: 2013, title: 'Rush' });
  console.log('findOne\n', movie);

  // create
  const newMovie = await movieRepo.create({
    title: 'New movie',
    year: 2020,
    info: {
      actors: ['Keanu Reeves'],
    },
  });
  console.log('New movie\n', newMovie);

  // update
  newMovie.info!.rank = 10;
  console.log('Updated movie\n', await movieRepo.update(newMovie, newMovie));

  // delete
  try {
    const deleteResult = await movieRepo.delete({
      title: newMovie.title,
      year: newMovie.year,
    });
    console.log('Delete result', deleteResult);

    await movieRepo.findOne({
      title: newMovie.title,
      year: newMovie.year,
    });
  } catch (e) {
    console.log(e);
  }
})();

Kết thúc phần 05

Phần 05 đã hoàn thiện việc xây dựng các lớp để làm việc với cơ sở dữ liệu. Sử dụng những lớp đã tạo chúng ta có thể phát triển tiếp ứng dụng củ mình, có thể là CLI tool, Lambda Function, hay API RestFul…

Ở phần tiếp theo mình sẽ xây dựng một service cung cấp api restful đơn giản, sử dụng express framework.

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

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