10.10. データベースを Quarkus OptaPlanner 学校の時間割アプリケーションと統合する
Quarkus OptaPlanner 学校の時間割アプリケーションを作成したら、それをデータベースと統合し、ウェブベースのユーザーインターフェイスを作成して時間割を表示できます。
前提条件
- Quarkus OptaPlanner 学校の時間割アプリケーションがあります。
手順
-
Hibernate と Panache を使用して、
Timeslot
、Room
、およびLesson
インスタンスをデータベースに格納します。詳細については、Panache を使用した単純化された Hibernate ORM を参照してください。 - REST を介してインスタンスを公開します。詳細については、JSON REST サービスの記述 を参照してください。
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 はこれを使用して進行状況を表示します。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()); } }
- これらの REST メソッドの上に Web UI を構築して、タイムテーブルを視覚的に表現します。
- クイックスタートソースコード を確認します。