🕌

それ、サーバでやりますか?フロントでやりますか?

2023/09/15に公開

個人開発で作っているもの

野球盤型競技専用スコアブックアプリ
https://cap-scorebook.com

野球"盤"型競技とは

よく正月に「リアル野球BAN」というエンタメが放送されていますが、打球の行方のみで走者の進塁権が決まる競技のことをざっくり野球"盤"型競技と呼んでいます。基本的に走者がいない(いわゆる透明ランナーが採用されている)競技のことです。
例:

  • キャップ野球
  • ウィッフルボール

走者が実際に走り、打球の行方と走者の判断などが複合して競技の結果が決まるのが野球型競技です。

  • 野球
  • ソフトボール
  • ベースボール5

スコアブックシステム

概要

主にDBには1打席ごとの結果を保持し、APIレスポンスで該当の試合の打席の結果を配列で返すようにしています。その配列を受け取って、フロント側で計算して揮発性のstateとして保持し、stateから現在の状況を参照して画面を構築・描画するというものです。

ER図(簡略)

なぜロジックをフロントに寄せているか

野球は、前の打席の結果が後ろの打者の状況を作る競技です。
仮に状況をレコードとしてデータベースに保持していたとして、途中で入力ミスがあった場合、後続の全ての状況に影響が出て、後続のレコードを全て更新する必要が出てきます。
特に、このアプリのターゲットは操作に慣れていない競技者たちであり、入力ミスによる再入力が起こると考えられます。それを踏まえて、サーバサイドの処理を簡略化しました。

フロントにロジックを寄せたことによる弊害

  1. 結構な計算量(打席ごとの塁状況、打点判定、得点判定、送信/表示用のサマリ構築、イニングごとの得点状況)を要求するので低スペックのスマホなどでは動作が重くなる
  2. ロジックがフロントにあるので、成績の集計などがサーバ側で行うことが難しく、ワンクッション(フロント側で計算した集計結果をサーバに送信してもらう必要がある)余計な手間がかかる
  3. 集計後に入力ミスが発覚すると、端末で打席結果を修正の上、集計結果を再送信することが必要となる

機能追加への対応(打撃成績補足機能)

試合ごとにロック処理(集計送信)をユーザに行ってもらっていますが、機能追加で集計する項目などを増やすと、既に集計済みの試合の再集計処理を行うのが非常に面倒(自責点などはユーザ側で計算してもらって入力してから送信してもらっているため)で、フロント側も別ページで別途集計して送信する処理を追加しています。

https://cap-scorebook.com/game/a5996d1e-5370-4492-92bd-2b7b1ece57fb/post_situation

人力でポチポチボタン押して送信する仕様なのですが、試合数が多く追いつかないため、puppeteerのスクリプトを書いて定期的に自動で送信するようにしています。

決勝点算出機能の実装

文字通り誰が決勝打を打ったか、というのを自動的に算出する仕組みです。

そのロジック、サーバでやりますか?フロントでやりますか?

当初は打撃成績補足機能を使ってフロント側で計算して新しい項目として保存するつもりだったのですが、
stateの構築の際に先攻/後攻を別々のデータを2回計算して結合しているため、ある打者がN点目の得点を挙げたときに相手チームの得点がいくつかというデータを参照し、

  • スコアが同点もしくはビハインドのとき、打席結果によって勝ち越していれば決勝点フラグを立てる
  • ただし、その後の状況で同点になった場合はそれより前のフラグを全て消さなくてはならない
  • 最後に残っていたフラグが決勝点である

データの持ち方やロジックが悪かったと思うのですが、打席結果の二次元配列を(インデックスで)読み出して処理しようと思うと面倒なことになってしまうことに気づきました。
幸い、打撃結果補足を拡張して、

  • 自チームの打席時点のスコア
  • 相手チームの打席時点のスコア
  • 打席での打点
    などを拡張することによって、
        select
            bsi.game_id
            , bsi.is_first
            , bsi.col_number
            , bsi.row_number
            , bsi.my_team_run
            , bsi.opponent_team_run
            , bsi.inning
            , bsi.rbi
            , bsi.bes
            , case 
                when bsi.is_first = 1 
                    then bsi.my_team_run + bsi.rbi + bsi.bes
                else bsi.opponent_team_run 
                end as first_run
            , case 
                when bsi.is_first = 0 
                    then bsi.my_team_run + bsi.rbi + bsi.bes 
                else bsi.opponent_team_run 
                end as last_run 
        from
            batting_situation bsi 
        where
            bsi.game_id = ?
        having
            -- 打席に立ったときに同点かビハインド
            bsi.my_team_run <= bsi.opponent_team_run
            -- 打席結果で逆転することが条件
            and ( 
                (bsi.is_first = 0 and last_run > first_run) 
                or (bsi.is_first = 1 and last_run < first_run)
            ) 
        order by
            inning desc
            , is_first asc
            -- concatすることによってどちらの打席が前か後ろか判別できるs
            , concat(col_number, row_number) desc
        limit 1

という形でSQLでは楽に取れることに気づきました。
あくまでこれはSQLを組むのとjs(ts)で適切に処理するのどちらが得意か、ということにもよりますが...。

  1. 未送信の試合の補足情報(打点、打席時点のスコア)をpuppeteerで回して送信する
  2. API側でリクエストを受け取ってレコードをinsertする
  3. insertが終わった段階で決勝点算出SQL(上記)を投げる
  4. 結果を取り出して該当レコードのフラグをupdateする

おわりに

puppetterスクリプトはどちらかというとサーバサイドなので、ロジックをどっちに寄せるのが楽なのかよくわからないですね...

Discussion