10.10. データベースを Quarkus OptaPlanner 学校の時間割アプリケーションと統合する

Quarkus OptaPlanner 学校の時間割アプリケーションを作成したら、それをデータベースと統合し、ウェブベースのユーザーインターフェイスを作成して時間割を表示できます。

前提条件

  • Quarkus OptaPlanner 学校の時間割アプリケーションがあります。

手順

  1. Hibernate と Panache を使用して、TimeslotRoom、および Lesson インスタンスをデータベースに格納します。詳細については、Panache を使用した単純化された Hibernate ORM を参照してください。
  2. REST を介してインスタンスを公開します。詳細については、JSON REST サービスの記述 を参照してください。
  3. TimeTableResource クラスを更新して、単一のトランザクションで TimeTable インスタンスの読み取りと書き込みを行います。

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    import javax.transaction.Transactional;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    
    import io.quarkus.panache.common.Sort;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.Room;
    import org.acme.optaplanner.domain.TimeTable;
    import org.acme.optaplanner.domain.Timeslot;
    import org.optaplanner.core.api.score.ScoreManager;
    import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
    import org.optaplanner.core.api.solver.SolverManager;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    @Path("/timeTable")
    public class TimeTableResource {
    
        public static final Long SINGLETON_TIME_TABLE_ID = 1L;
    
        @Inject
        SolverManager<TimeTable, Long> solverManager;
        @Inject
        ScoreManager<TimeTable, HardSoftScore> scoreManager;
    
        // To try, open http://localhost:8080/timeTable
        @GET
        public TimeTable getTimeTable() {
            // Get the solver status before loading the solution
            // to avoid the race condition that the solver terminates between them
            SolverStatus solverStatus = getSolverStatus();
            TimeTable solution = findById(SINGLETON_TIME_TABLE_ID);
            scoreManager.updateScore(solution); // Sets the score
            solution.setSolverStatus(solverStatus);
            return solution;
        }
    
        @POST
        @Path("/solve")
        public void solve() {
            solverManager.solveAndListen(SINGLETON_TIME_TABLE_ID,
                    this::findById,
                    this::save);
        }
    
        public SolverStatus getSolverStatus() {
            return solverManager.getSolverStatus(SINGLETON_TIME_TABLE_ID);
        }
    
        @POST
        @Path("/stopSolving")
        public void stopSolving() {
            solverManager.terminateEarly(SINGLETON_TIME_TABLE_ID);
        }
    
        @Transactional
        protected TimeTable findById(Long id) {
            if (!SINGLETON_TIME_TABLE_ID.equals(id)) {
                throw new IllegalStateException("There is no timeTable with id (" + id + ").");
            }
            // Occurs in a single transaction, so each initialized lesson references the same timeslot/room instance
            // that is contained by the timeTable's timeslotList/roomList.
            return new TimeTable(
                    Timeslot.listAll(Sort.by("dayOfWeek").and("startTime").and("endTime").and("id")),
                    Room.listAll(Sort.by("name").and("id")),
                    Lesson.listAll(Sort.by("subject").and("teacher").and("studentGroup").and("id")));
        }
    
        @Transactional
        protected void save(TimeTable timeTable) {
            for (Lesson lesson : timeTable.getLessonList()) {
                // TODO this is awfully naive: optimistic locking causes issues if called by the SolverManager
                Lesson attachedLesson = Lesson.findById(lesson.getId());
                attachedLesson.setTimeslot(lesson.getTimeslot());
                attachedLesson.setRoom(lesson.getRoom());
            }
        }
    
    }

    この例には TimeTable インスタンスが含まれています。ただし、マルチテナンシーを有効にして、複数の学校の TimeTable インスタンスを並行して処理できます。

    getTimeTable() メソッドは、データベースから最新の時間割を返します。自動的に挿入される ScoreManager メソッドを使用して、そのタイムテーブルのスコアを計算し、UI で使用できるようにします。

    solve() メソッドは、ジョブを開始して、現在の時間割を解決し、時間枠と部屋の割り当てをデータベースに保存します。このメソッドは、SolverManager.solveAndListen() メソッドを使用して、中間の最適解をリッスンし、それに合わせてデータベースを更新します。バックエンドがまだ解決している間、UI はこれを使用して進行状況を表示します。

  4. TimeTableResourceTest クラスを更新して、solve() メソッドがすぐに戻ることを反映し、ソルバーが解決を完了するまで最新の解をポーリングするようにします。

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.TimeTable;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solveDemoDataUntilFeasible() throws InterruptedException {
            timeTableResource.solve();
            TimeTable timeTable = timeTableResource.getTimeTable();
            while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
                // Quick polling (not a Test Thread Sleep anti-pattern)
                // Test is still fast on fast machines and doesn't randomly fail on slow machines.
                Thread.sleep(20L);
                timeTable = timeTableResource.getTimeTable();
            }
            assertFalse(timeTable.getLessonList().isEmpty());
            for (Lesson lesson : timeTable.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(timeTable.getScore().isFeasible());
        }
    
    }
  5. これらの REST メソッドの上に Web UI を構築して、タイムテーブルを視覚的に表現します。
  6. クイックスタートソースコード を確認します。