はじめに
こんにちは!マーケターから未経験のエンジニアに転身した社員Tです。
開発を始めて1年が経ち、サーバーサイド開発にも触れるようになりました。
今回は、Javaを使用したダッシュボードAPIの作成についてご紹介します。
これまでフロントエンド中心の開発を行っていた私が、サーバーサイドのAPI開発に挑戦していく様子をお届けします。
本記事の対象の方
- サーバーサイド開発をこれから学びたい方
- JavaでのAPI開発に興味がある方
- ダッシュボードの作成を通じて、実践的な技術を学びたい方
Javaとは?
Javaは、非常に人気のあるオブジェクト指向プログラミング言語で、さまざまなアプリケーションの開発に広く使用されています。
詳しい説明については、過去のブログ記事をご参照ください。
⇒関連記事はこちら
環境構築
環境構築に関しては、内容が広範囲にわたるため、別の記事で詳細に解説する予定です。
ダッシュボードAPIの作成
今回作成したいのは、こんなダッシュボード画面です。ユーザーがコースを選択することで、自分の学習状況を一目で確認できるようにすることを目指しています。
全体の作成の流れとしては、下記の通りとなります。
- ユーザー情報を取得
- コース情報を取得
- 各グラフ描画に必要なデータを取得
今回は、3.各グラフ描画に必要なデータを取得するAPIの実装について詳しく触れていきたいと思います。5つグラフがありますが、右下の同資格受験者との比較グラフを作成します。
1. ユーザー情報の取得については、過去のブログ記事をご参考ください。
⇒関連記事はこちら
2. コース情報の取得については、過去のブログ記事をご参考ください。
⇒関連記事はこちら
試験情報取得APIの設計
エンドポイント
/top/getScoreDistribution
- HTTPメソッド: POST
- リクエストボディ: ユーザーID、コースID
- レスポンス: 模擬試験のスコアリスト
リクエスト例
{
"courseId":"7",
"userId":"175"
}
今回はパスパラメータではなく、リクエストボディからユーザーIDとコースIDを受け取る設計にしています。これにより、柔軟なデータの受け渡しが可能となります。
レスポンス例
{
"scoreDistribution": {
"A": 5,
"B": 3,
"C": 2,
"D": 10
},
"errorMessageList": null
}
試験情報取得APIの実装
モデルクラスの作成
模擬試験履歴を表すエンティティクラスを作成します。このクラスはデータベースの trial_exam_history テーブルに対応し、各試験の履歴情報を管理します。
TrialExamHistoryEntity.java
1package jp.co.smsdatatech.questiongenerate.entity;
2
3import java.io.Serializable;
4import lombok.Data;
5
6/**
7 * TrialExamHistoryテーブルEntity
8 */
9@Data
10public class TrialExamHistoryEntity implements Serializable {
11
12 /** シリアルバージョンUID */
13 private static final long serialVersionUID = 1L;
14
15 /** 試験履歴ID */
16 private Integer trialExamHistoryId;
17
18 /** 試験ID */
19 private Integer trialExamId;
20
21 /** 試験回数 */
22 private Integer trialExamTimes;
23
24 /** 試験スコア */
25 private Integer trialScore;
26
27 /** ユーザーID */
28 private Integer userId;
29}
MyBatisのMapper設定
このクエリでは、RANK() 関数を用いて、各ユーザーの試験履歴をスコアの降順でランク付けし、最も高いスコアの試験のみを取得しています。
TrialExamHistoryMapper.xml
1<mapper namespace="jp.co.smsdatatech.questiongenerate.repository.TrialExamHistoryMapper">
2
3 <select id="selectLatestTrialExamHistoryList" resultMap="TrialExamHistoryResultMap" resultType="jp.co.smsdatatech.questiongenerate.entity.TrialExamHistoryEntity">
4 SELECT
5 highest_score_exam.user_id,
6 highest_score_exam.trial_exam_id,
7 highest_score_exam.trial_exam_score,
8 highest_score_exam.trial_exam_times
9 FROM
10 (
11 SELECT
12 teh.user_id,
13 teh.trial_exam_id,
14 teh.trial_exam_score,
15 teh.trial_exam_times,
16 RANK() OVER (PARTITION BY teh.user_id ORDER BY teh.trial_exam_score DESC, teh.trial_exam_times DESC) AS rank_num
17 FROM
18 trial_exam_history teh
19 JOIN
20 trial_exam AS exam
21 ON teh.trial_exam_id = exam.trial_exam_id
22 WHERE
23 exam.course_id = #{courseId}
24 ) AS highest_score_exam
25 WHERE
26 highest_score_exam.rank_num = 1;
27 </select>
28</mapper>
サービスクラスの作成
trialExamHistoryMapper.selectLatestTrialExamHistoryList(courseId) を呼び出し、試験履歴を取得します。
TrialExamHistoryService.java
1/**
2 * 指定したコースIDに基づいて、複数ユーザーの最新試験履歴を取得する。
3 *
4 * @param courseId コースID
5 * @return 最新の模擬試験履歴リスト
6 */
7@Transactional(readOnly = true)
8public List<TrialExamHistoryEntity> selectLatestTrialExamHistoryList(Integer courseId) {
9
10 log.debug("コースID {} の最新の模擬試験履歴を取得します。", courseId);
11
12 // データベースから複数ユーザーの最新試験履歴を取得
13 List<TrialExamHistoryEntity> latestTrialExamHistoryList = trialExamHistoryMapper
14 .selectLatestTrialExamHistoryList(courseId);
15
16 log.debug("取得した複数ユーザーの最新模擬試験履歴リスト: {}", latestTrialExamHistoryList);
17
18 return latestTrialExamHistoryList;
19}
コントローラークラスの作成
selectLatestTrialExamHistoryList(courseId) で最新の試験履歴を取得。取得した履歴データをもとに、スコア範囲ごとの人数をカウントし、レスポンスとして返却します。
TopRest.java
1/**
2 * 最新の模擬試験履歴を取得(スコア範囲ごとの人数分布のみ返却)。
3 *
4 * <h3>機能概要</h3>
5 * コースIDに基づいて、スコア範囲ごとの人数分布を取得する。
6 *
7 * <h3>処理フロー</h3>
8 * 1. リクエストデータのバリデーション<br>
9 * 2. サービスクラスを呼び出して、スコア範囲ごとの人数分布を取得<br>
10 * 3. スコア範囲ごとの人数分布が見つからない場合はエラーメッセージを設定<br>
11 * 4. スコア範囲ごとの人数分布を返却
12 *
13 * @param form UserIdForm
14 * @param result バリデーション結果やエラーを格納
15 * @return スコア範囲ごとの人数分布 ScoreDistributionResponseForm
16 */
17@PostMapping("/top/getScoreDistribution")
18public ScoreDistributionResponseForm getScoreDistribution(
19 @RequestBody @Validated({ ValidationGroups.Step1.class, ValidationGroups.Step2.class }) UserIdForm form,
20 BindingResult result) {
21 log.debug("TopRest (getScoreDistribution) 呼び出し開始");
22
23 ScoreDistributionResponseForm responseForm = new ScoreDistributionResponseForm();
24 List<String> errorMessageList = new ArrayList<>();
25
26 // バリデーションエラーがある場合、詳細なエラーメッセージを追加
27 if (result.hasErrors()) {
28 for (FieldError fieldError : result.getFieldErrors()) {
29 String errorMessage = messageSource.getMessage(fieldError, Locale.getDefault());
30 errorMessageList.add(errorMessage);
31 }
32 responseForm.setErrorMessageList(errorMessageList);
33 return responseForm;
34 }
35
36 Integer courseId = form.getCourseId();
37
38 // 最新の模擬試験履歴リストを取得
39 List<TrialExamHistoryEntity> latestTrialExamHistoryList = trialExamHistoryService
40 .selectLatestTrialExamHistoryList(courseId);
41
42 // リストが取得できなかった場合
43 if (CollectionUtils.isEmpty(latestTrialExamHistoryList)) {
44 errorMessageList
45 .add(messageSource.getMessage(ErrorMessages.TRIAL_EXAM_HISTORY_NOT_FOUND, null,
46 Locale.getDefault()));
47 responseForm.setErrorMessageList(errorMessageList);
48 log.debug("最新の模擬試験履歴が見つかりませんでした");
49 return responseForm;
50 }
51
52 // スコア範囲ごとの人数をカウントする
53 Map<String, Integer> scoreDistribution = new HashMap<>();
54 for (TrialExamHistoryEntity history : latestTrialExamHistoryList) {
55 int score = history.getTrialScore();
56 ScoreEnum range = ScoreEnum.getRange(score);
57 if (range != null) {
58 // スコア範囲に該当する人数を取り出し、1を加算する
59 int currentCount = scoreDistribution.getOrDefault(range.name(), 0);
60 scoreDistribution.put(range.name(), currentCount + 1);
61 }
62 }
63
64 // スコア分布をResponseFormにセット
65 responseForm.setScoreDistribution(scoreDistribution);
66
67 log.debug("TopRest (getScoreDistribution) 呼び出し終了");
68
69 return responseForm;
70}
Enumクラスの作成
今回は、試験のスコア範囲を管理するために ScoreEnum を作成しました。スコアがどのランク(A, B, C, D)に属するかを判定するために使います。
ScoreEnum.java
1package jp.co.smsdatatech.questiongenerate.enums;
2
3import lombok.Getter;
4
5/**
6 * スコア範囲を表す列挙型クラス。
7 *
8 * <p>
9 * 各範囲はスコアの最小値と最大値によって定義されます。
10 * スコアを基に適切なスコア範囲を判定する機能を提供します。
11 * </p>
12 */
13@Getter
14public enum ScoreEnum {
15 A(90, 100), // A範囲:90〜100
16 B(80, 89), // B範囲:80〜89
17 C(70, 79), // C範囲:70〜79
18 D(0, 69); // D範囲:0〜69
19
20 private final int minScore;
21 private final int maxScore;
22
23 // コンストラクタ
24 ScoreEnum(int minScore, int maxScore) {
25 this.minScore = minScore;
26 this.maxScore = maxScore;
27 }
28
29 // スコアがこの範囲に含まれるかどうかをチェック
30 public boolean isInRange(int score) {
31 return score >= minScore && score <= maxScore;
32 }
33}
実装内容の振り返り
今回の実装では、SQLのRANK()関数とENUMクラスを活用して、ダッシュボード画面のデータ集計を効率的に行いました。
1. SQLの RANK() 関数について
RANK()関数は、特定の条件でデータを順位付けするために使用しました。RANK()を活用することで、プログラム側でループを回して順位を計算する必要がなくなり、SQLだけで効率的にデータを取得できるようになりました。
2. ENUMクラス ScoreEnum について
ScoreEnumは、スコアをランク(A, B, C, D)に分類するために作成しました。ENUMを使うことで、スコアの評価ロジックを統一し、可読性を向上させることができました。また、メソッドを定義することで、スコアがどのランクに属するかを簡単に判定できるようになり、コードの保守性も向上しました。
まとめ
今回のブログ記事を通して、ダッシュボード画面のすべての実装が完了しました。まだまだ改善の余地はあるかと思いますが、より良い書き方を模索しながら、さらに保守性や拡張性の高い実装を目指していきたいと思います。
長らく見ていただいてありがとうございました。次回は、テストやデータベース操作について、自身の学びをまとめていきたいと思います。