高等教育機関向けWebベース履修登録システムの実装

システム概要と設計方針

本プロジェクトは、教育機関における履修情報の管理プロセスを自動化し、データ欠落の防止と運用リソースの最適化を目的として構築されたWebアプリケーションである。Apache Tomcat環境での標準的なデプロイを前提とし、統一的なUIコンポーネントを用いながら、利用者の権限レベルに応じた動的なビューレンダリングとセッション管理を実装している。

機能要件とアクセス制御モデル

  • 認証とロールベースルーティング: 認証成功後、セッションに権限フラグを設定し、管理者・教員・学生それぞれに最適化されたダッシュボードへリダイレクトする。情報隔離を徹底し、他利用者のデータ参照・更新をシステムレベルでブロックする。
  • 管理者機能: 教員および学生の基本マスタ登録。教員ID・学生IDは所定の桁数・フォーマット検証を通過させる必要がある。登録完了と同時に初期認証アカウントを自動生成する。
  • 教員機能: 担当科目の開設・最大定員設定、自身プロフィールの編集、および開設科目の受講生名簿の取得。
  • 学生機能: プロフィール情報の更新(識別子は変更不可)、全科目カタログの閲覧、定員に空きがある講義への履修申請。
  • データ整合性担保: 履修処理時に定員超過を検知し、トランザクション制御を用いてカウント増分と受講記録の追加をアトミックに実行する。

データベーススキーマ設計

リレーショナルデータベース上に以下のエンティティを配置し、外部キー制約による参照整合性を維持する。

  • faculty_members: 教員情報(教員ID、氏名、性別、所属組織、職階)
  • enrolled_students: 学生情報(学生ID、氏名、性別、所属クラス、専攻分野)
  • course_catalog: 科目情報(システム主キー、科目コード、科目名、上限人数、現在登録数、担当教員名)
  • registration_mapping: 履修関連付け(科目コード、担当教員ID、学生ID、登録メタデータ)

コア実装アーキテクチャ

MVCパターンに準拠し、プレゼンテーション層(Servlet/JSP)、データアクセス層(DAO)、およびモデル層(POJO)を明確に分離する。JDBCリソースの管理はtry-with-resourcesを活用してメモリリークを防止し、全クエリでパラメータ化構文を採用してSQLインジェクション脆弱性を排除している。

1. 接続管理およびユーティリティ層

package academy.system.core;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabasePool {
    private static final String CONNECTION_URL = "jdbc:mysql://localhost:3306/campus_db?serverTimezone=UTC";
    private static final String DB_USER = "sys_admin";
    private static final String DB_PASS = "encrypted_credential";

    public static Connection getConnection() throws SQLException {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            return DriverManager.getConnection(CONNECTION_URL, DB_USER, DB_PASS);
        } catch (ClassNotFoundException e) {
            throw new SQLException("JDBCドライバの初期化に失敗しました", e);
        }
    }
}

2. データアクセス層 (DAO)

package academy.system.dao;

import academy.system.core.DatabasePool;
import academy.system.model.Faculty;
import academy.system.model.Student;
import academy.system.model.CourseOffering;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class CourseManagementDao {

    public int validateCredentials(String userId, String password) {
        String query = "SELECT role_type FROM user_credentials WHERE account_id = ? AND pass_hash = ?";
        try (Connection conn = DatabasePool.getConnection();
             PreparedStatement stmt = conn.prepareStatement(query)) {
            stmt.setString(1, userId);
            stmt.setString(2, password);
            try (ResultSet rs = stmt.executeQuery()) {
                return rs.next() ? rs.getInt("role_type") : 0;
            }
        } catch (SQLException e) {
            throw new RuntimeException("認証クエリの実行中に例外が発生しました", e);
        }
    }

    public boolean registerFaculty(Faculty f) {
        String sql = "INSERT INTO faculty_members (fid, full_name, gender, department, academic_rank) VALUES (?, ?, ?, ?, ?)";
        return executeInsert(sql, stmt -> {
            stmt.setString(1, f.getFid());
            stmt.setString(2, f.getFullName());
            stmt.setString(3, f.getGender());
            stmt.setString(4, f.getDepartment());
            stmt.setString(5, f.getRank());
        });
    }

    public boolean registerStudent(Student s) {
        String sql = "INSERT INTO enrolled_students (sid, full_name, gender, cohort, specialization) VALUES (?, ?, ?, ?, ?)";
        return executeInsert(sql, stmt -> {
            stmt.setString(1, s.getSid());
            stmt.setString(2, s.getFullName());
            stmt.setString(3, s.getGender());
            stmt.setString(4, s.getCohort());
            stmt.setString(5, s.getSpecialization());
        });
    }

    public boolean publishCourse(CourseOffering c, String instructorName) {
        String sql = "INSERT INTO course_catalog (course_code, title, max_capacity, enrolled_count, instructor_ref) VALUES (?, ?, ?, 0, ?)";
        return executeInsert(sql, stmt -> {
            stmt.setString(1, c.getCourseCode());
            stmt.setString(2, c.getTitle());
            stmt.setInt(3, c.getMaxCapacity());
            stmt.setString(4, instructorName);
        });
    }

    public boolean updateFacultyProfile(Faculty f) {
        String sql = "UPDATE faculty_members SET full_name=?, gender=?, department=?, academic_rank=? WHERE fid=?";
        return executeUpdate(sql, stmt -> {
            stmt.setString(1, f.getFullName());
            stmt.setString(2, f.getGender());
            stmt.setString(3, f.getDepartment());
            stmt.setString(4, f.getRank());
            stmt.setString(5, f.getFid());
        });
    }

    public List<CourseOffering> fetchAvailableCourses() {
        List<CourseOffering> catalog = new ArrayList<>();
        String sql = "SELECT * FROM course_catalog WHERE enrolled_count < max_capacity";
        try (Connection conn = DatabasePool.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            while (rs.next()) {
                catalog.add(mapToCourse(rs));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return catalog;
    }

    public boolean processEnrollment(String sid, String courseCode, int catalogId, int facultyId) {
        Connection conn = null;
        boolean success = false;
        try {
            conn = DatabasePool.getConnection();
            conn.setAutoCommit(false);

            String capacitySql = "UPDATE course_catalog SET enrolled_count = enrolled_count + 1 WHERE id = ? AND enrolled_count < max_capacity";
            try (PreparedStatement capStmt = conn.prepareStatement(capacitySql)) {
                capStmt.setInt(1, catalogId);
                int affected = capStmt.executeUpdate();

                if (affected > 0) {
                    String logSql = "INSERT INTO registration_mapping (course_code, faculty_id, student_id) VALUES (?, ?, ?)";
                    try (PreparedStatement logStmt = conn.prepareStatement(logSql)) {
                        logStmt.setString(1, courseCode);
                        logStmt.setInt(2, facultyId);
                        logStmt.setString(3, sid);
                        logStmt.executeUpdate();
                    }
                    success = true;
                }
                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try { if (conn != null) conn.close(); } catch (SQLException ignored) {}
        }
        return success;
    }

    private CourseOffering mapToCourse(ResultSet rs) throws SQLException {
        return new CourseOffering(
            rs.getInt("id"), rs.getString("course_code"), rs.getString("title"),
            rs.getInt("max_capacity"), rs.getInt("enrolled_count"), rs.getString("instructor_ref")
        );
    }

    private interface ParameterBinder {
        void bind(PreparedStatement stmt) throws SQLException;
    }

    private boolean executeInsert(String sql, ParameterBinder binder) {
        try (Connection conn = DatabasePool.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            binder.bind(stmt);
            return stmt.executeUpdate() > 0;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    private boolean executeUpdate(String sql, ParameterBinder binder) {
        return executeInsert(sql, binder);
    }
}

3. フロントコントローラー (Servlet)

package academy.system.web;

import academy.system.dao.CourseManagementDao;
import academy.system.model.CourseOffering;
import academy.system.model.Faculty;
import academy.system.model.Student;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

@WebServlet("/academic-router")
public class CourseRegistrationController extends HttpServlet {
    private final CourseManagementDao dao = new CourseManagementDao();

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        String operation = req.getParameter("action");

        if ("login".equals(operation)) {
            authenticate(req, resp);
        } else if ("add_faculty".equals(operation)) {
            addFaculty(req, resp);
        } else if ("add_student".equals(operation)) {
            addStudent(req, resp);
        } else if ("create_course".equals(operation)) {
            createCourse(req, resp);
        } else if ("enroll_student".equals(operation)) {
            enrollStudent(req, resp);
        }
    }

    private void authenticate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String uid = req.getParameter("uid");
        String pwd = req.getParameter("pwd");
        int role = dao.validateCredentials(uid, pwd);

        HttpSession session = req.getSession();
        session.setAttribute("user_role", role);
        session.setAttribute("session_uid", uid);
        resp.sendRedirect("portal.jsp");
    }

    private void createCourse(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        String instructorRef = (String) session.getAttribute("instructor_name");
        
        String code = req.getParameter("code");
        String title = req.getParameter("title");
        int limit = Integer.parseInt(req.getParameter("limit"));

        CourseOffering offering = new CourseOffering(0, code, title, limit, 0, instructorRef);
        boolean status = dao.publishCourse(offering, instructorRef);

        req.setAttribute("feedback", status ? "科目の公開が完了しました。" : "公開処理に失敗しました。");
        req.getRequestDispatcher("/faculty/dashboard.jsp").forward(req, resp);
    }

    private void enrollStudent(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        HttpSession session = req.getSession();
        String studentId = (String) session.getAttribute("session_uid");
        
        int recordId = Integer.parseInt(req.getParameter("record_id"));
        String code = req.getParameter("code");
        int facultyRef = Integer.parseInt(req.getParameter("faculty_ref"));

        boolean result = dao.processEnrollment(studentId, code, recordId, facultyRef);
        resp.sendRedirect("/student/confirmation.jsp?registered=" + result);
    }
}

Webデプロイと運用

プロジェクトはMavenまたは標準的なディレクトリ構成でWARアーカイブを生成し、Tomcatのwebappsディレクトリへ配置する。server.xmlまたはManager App経由でコンテキストをマウント後、http://[host]:[port]/[context-path]/academic-router?action=loginにアクセスすることでシステムを起動可能となる。接続先RDBMSのタイムゾーン設定とJDBCドライバのクラスパス整合性を事前に検証することで、初回デプロイ時のエラーを回避できる。

タグ: Java-Servlet JDBC-Implementation tomcat-deployment Academic-Management-System Relational-Schema

6月1日 16:33 投稿