🎆

node-schedule バッチ開発

2022/08/19に公開約4,400字

概要

サッカー選手の試合成績を集計し、ユーザが口コミ投稿できるアプリを開発中です。

対象選手はマンチェスター・シティの全 27 選手です。

今回は API-FOOTBALL (3.9.2)の下記エンドポイントを使用してプレミアリーグの試合成績を集計しました。

実現したいこと

選手の最新情報を取得して画面に表示したいです。
そのために、決まった日時・時刻に API コールし最新情報をローカル DB に反映する定期バッチを開発します。

開発手法/背景

選手詳細ページに表示する試合成績を常に最新の状態に維持するためには、試合が行われる度にデータを更新する必要があります。

API のデータを直接表示しても良いのですが下記理由のため API のデータをローカル DB に保存して一定期間で更新する運用としました。

  • DB運用により対象データの操作性が上がる(SQLでのソート機能や不随情報の追加等)
  • 一日の使用制限を圧迫する可能性がある
  • ページ読み込みが遅くなる可能性がある

node-schedule の導入/実装

開発環境

Windows 11 Pro
mysql  Ver 8.0.28 for Win64 on x86_64 (MySQL Community Server - GPL)
package.json
"dependencies": {
    "ejs": "^3.1.6",
    "express": "~4.16.0",
    "node-schedule": "^2.1.0",
    "mysql": "^2.18.1",

手順

今回、バッチを定期実行するために「node-schedule」を用いました。

  • node-schedule をインストール

https://www.npmjs.com/package/node-schedule
  • app.js にバッチ処理を記載
app.js
const schedule = require('node-schedule');

const servicesPlayer = require.main.require('./app/services/player');

const job = schedule.scheduleJob('59 59 23 * * *', async() => {
  console.log('毎日23:59:59にDB更新します' + new Date());
  const res = await servicesPlayer.updateManCityPlayersStats();
  console.log('DB更新処理は終了しましたか?', res);
});
  • API から最新データを取得

API-FOOTBALL (3.9.2)の API Key を用いてこのようにデータ取得用の処理を記載します。

https://www.api-football.com/documentation-v3#tag/Players/operation/get-players-seasons
app\lib\utils.js
'use strict';
var request = require("request");
require('dotenv').config();
const env = process.env

module.exports.getPlayersListFromApiSports = (pageNum) => {
  const X_RAPIDAPI_KEY = env.X_RAPIDAPI_KEY;

  return new Promise(async (resolve, reject) => {
    var options = {
      method: 'GET',
      url: `https://v3.football.api-sports.io/players?league=39&season=2022&team=50&page=${pageNum}`,
      headers: {
        'x-rapidapi-host': 'v3.football.api-sports.io',
        'x-rapidapi-key': X_RAPIDAPI_KEY
      }
    };
    request(options, (error, response, body) => {
      if (error) {
        throw new Error(error);
      }
      resolve(body);
    })
  })
};

  • ローカル DB に更新する
app\services\player.js
module.exports.updateManCityPlayersStats = async() => {
  //ここでAPIコール
  const player = await utils.getPlayersListFromApiSports();

  //APIから取得した最新の選手成績をstatsに保持
  let stats = [];
  stats.push(...player[1]);

  try {
    for (let i = 0; i < stats?.length; i++) {
      transaction = await MySQLClient.beginTransaction();
      transaction.executeQuery(
        await sql("UPDATE_PLAYER_STATS_BY_PID"), //選手IDを元にAPIの最新データで更新していく
        [
          ...
        ]
      );
      //ループ内で毎回コミットしないと次回の更新時に処理落ちする
      await transaction.commit();
    }
    //DB更新処理が終わればtrueを返す
    return true;
  } catch (error) {
    await transaction.rollback();
    console.error(error);
  }
}

おまけ

ローカル DB の updated カラムの値と現在時刻の差分が指定のインターバル設定値を超えた場合のみ API コールしてローカル DB を更新する手法でも実装してみました。

app\services\player.js
router.get("/:id", async (req, res, next) => {
  const pid = req.params.id;

  //DBの更新時刻を取得
  const updatedDate = await modelsPlayer.getPlayerStatsUpdatedDate(pid);
  const dateDb = updatedDate.getDate();

  //現在時刻を取得
  const now = new Date();
  const dateNow = new Date(now).getDate();

  //差分
  let dateDiff = Number(dateNow) - Number(dateDb)

  //差分が2<==date<=30なら更新
  if(2 <= dateDiff && dateDiff <=30){
    //APIで情報を取得
    const data = await servicesPlayer.getManCityPlayersListFromApiSports();
    //更新用SQL発行
    //player, stats配列の何番目の要素をSQLの引数として持つか
    let Num;
    const player = data?.player;
    const stats = data?.stats;
    for (let i = 0; i < player.length; i++) {
      if(Number(pid) === player[i].id){
        Num = i
      }
    }
    try {
      transaction = await MySQLClient.beginTransaction();
      transaction.executeQuery(
        await sql("UPDATE_PLAYER_STATS_BY_PID"),
        [
          ...
        ]
      );
      await transaction.commit();
      res.render("./player/index.ejs", {
        info: playerInfo,
        stats: playerStats,
        replys: playerReplys,
      });
    } catch (error) {
      await transaction.rollback();
      next(err);
    }
  } else if (dateDiff <= 0){
    //何もせずにそのままフロントにローカルDBの値を渡す
  }
})

しかしながら、現在の仕様では選手詳細ページにアクセスした際に連続処理(DB情報取得⇒更新判定⇒API コール⇒更新 SQL 発行)が発生するためページ描画かかなり遅延してしまう状況となりました。

個人の感覚としてはクリックした選手の成績は1秒以内に表示されないとイライラします。

そのため画面描画を著しく低下させるこちらの実装は処理が重かったので”没案”としました。

以上となります!

最後までお読みいただきありがとうございました。

参考

https://qiita.com/coffeePlusPlus/items/c0d84d4f4b272a6593b8
GitHubで編集を提案

Discussion

ログインするとコメントできます