diff --git a/docs/dev/guidelines/database.rst b/docs/dev/guidelines/database.rst index 50df45339443..7898ab2caa33 100644 --- a/docs/dev/guidelines/database.rst +++ b/docs/dev/guidelines/database.rst @@ -155,7 +155,7 @@ In order to load the relationships of an entity on demand, we then use one of 3 @EntityGraph(type = LOAD, attributePaths = { "exercises", "exercises.categories", "exercises.teamAssignmentConfig" }) Course findWithEagerExercisesById(long courseId); -2. **JOIN FETCH**: The ``JOIN FETCH`` keyword is used in a custom query to specify a graph of relationships to fetch. It should be used when a query is custom and has a custom ``@Query`` annotation. Example: +2. **JOIN FETCH**: The ``JOIN FETCH`` keyword is used in a custom query to specify a graph of relationships to fetch. It should be used when a query is custom and has a custom ``@Query`` annotation. You can see the example below. Also, explicitly or implicitly limiting queries in Hibernate can lead to in-memory paging. For more details, see the 'In-memory paging' section. .. code-block:: java @@ -204,6 +204,73 @@ In order to load the relationships of an entity on demand, we then use one of 3 final Set fetchOptions = withGradingCriteria ? Set.of(GradingCriteria, AuxiliaryRepositories) : Set.of(AuxiliaryRepositories); var programmingExercise = programmingExerciseRepository.findByIdWithDynamicFetchElseThrow(exerciseId, fetchOptions); +4. **In memory paging**: Since the flag ``hibernate.query.fail_on_pagination_over_collection_fetch: true`` is now active, it is crucial to carefully craft database queries that involve FETCH statements with collections and thoroughly test the changes. In-memory paging would cause performance decrements and is, therefore, disabled. Any use of it will lead to runtime errors. +Queries that may result in this error can return Page<> and contain JOIN FETCHES or involve internal limiting in Hibernate, such as findFirst, findLast, or findOne. One solution is to split the original query into multiple queries and a default method. The first query fetches only the IDs of entities whose full dependencies need to be fetched. The second query eagerly fetches all necessary dependencies, and the third query uses counting to build Pages, if they are utilized. +When possible, use the default Spring Data/JPA methods for second(fetching) and third(counting) queries. +An example implementation could look like this: + + .. code-block:: java + + // Repository interface + default Page searchAllByLoginOrNameInCourseAndReturnPage(Pageable pageable, String loginOrName, long courseId) { + List userIds = findUserIdsByLoginOrNameInCourse(loginOrName, courseId, pageable).stream().map(DomainObject::getId).toList();; + + if (userIds.isEmpty()) { + return new PageImpl<>(Collections.emptyList(), pageable, 0); + } + + List users = findUsersWithGroupsByIds(userIds); + long total = countUsersByLoginOrNameInCourse(loginOrName, courseId); + + return new PageImpl<>(users, pageable, total); + } + + @Query(""" + SELECT DISTINCT user + FROM User user + JOIN user.groups userGroup + JOIN Course course ON course.id = :courseId + WHERE user.isDeleted = FALSE + AND ( + user.login LIKE :#{#loginOrName}% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + ) + AND (course.studentGroupName = userGroup + OR course.teachingAssistantGroupName = userGroup + OR course.editorGroupName = userGroup + OR course.instructorGroupName = userGroup + ) + """) + List findUsersByLoginOrNameInCourse(@Param("loginOrName") String loginOrName, @Param("courseId") long courseId, Pageable pageable); + + @Query(""" + SELECT DISTINCT user + FROM User user + LEFT JOIN FETCH user.groups userGroup + WHERE user.id IN :ids + """) + List findUsersWithGroupsByIds(@Param("ids") List ids); + + @Query(""" + SELECT COUNT(DISTINCT user) + FROM User user + JOIN user.groups userGroup + JOIN Course course ON course.id = :courseId + WHERE user.isDeleted = FALSE + AND ( + user.login LIKE :#{#loginOrName}% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + ) + AND ( + course.studentGroupName = userGroup + OR course.teachingAssistantGroupName = userGroup + OR course.editorGroupName = userGroup + OR course.instructorGroupName = userGroup + ) + """) + long countUsersByLoginOrNameInCourse(@Param("loginOrName") String loginOrName, @Param("courseId") long courseId); + + Best Practices diff --git a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java index dbce9f159046..23fe8dbaa66e 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.repository; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; import java.time.Duration; import java.time.ZonedDateTime; @@ -10,6 +11,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -33,9 +35,29 @@ public interface BuildJobRepository extends ArtemisJpaRepository Optional findBuildJobByResult(Result result); - // TODO: rewrite this query, pageable does not work well with EntityGraph - @EntityGraph(attributePaths = { "result", "result.participation", "result.participation.exercise", "result.submission" }) - Page findAll(Pageable pageable); + @Query(""" + SELECT b.id + FROM BuildJob b + """) + List findAllIds(Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = { "result", "result.participation", "result.participation.exercise", "result.submission" }) + List findWithDataByIdIn(List ids); + + /** + * Retrieves a paginated list of all {@link BuildJob} entities. + * + * @param pageable the pagination information. + * @return a paginated list of {@link BuildJob} entities. If no entities are found, returns an empty page. + */ + default Page findAllWithData(Pageable pageable) { + List ids = findAllIds(pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List result = findWithDataByIdIn(ids); + return new PageImpl<>(result, pageable, count()); + } // Cast to string is necessary. Otherwise, the query will fail on PostgreSQL. @Query(""" @@ -56,17 +78,6 @@ Page findAllByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus @Param("startDate") ZonedDateTime startDate, @Param("endDate") ZonedDateTime endDate, @Param("searchTerm") String searchTerm, @Param("courseId") Long courseId, @Param("durationLower") Duration durationLower, @Param("durationUpper") Duration durationUpper, Pageable pageable); - @Query(""" - SELECT b - FROM BuildJob b - LEFT JOIN FETCH b.result r - LEFT JOIN FETCH r.participation p - LEFT JOIN FETCH p.exercise - LEFT JOIN FETCH r.submission - WHERE b.id IN :buildJobIds - """) - List findAllByIdWithResults(@Param("buildJobIds") List buildJobIds); - @Query(""" SELECT new de.tum.in.www1.artemis.service.connectors.localci.dto.DockerImageBuild( b.dockerImage, @@ -77,9 +88,30 @@ Page findAllByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus """) Set findAllLastBuildDatesForDockerImages(); - // TODO: rewrite this query, pageable does not work well with EntityGraph - @EntityGraph(attributePaths = { "result", "result.participation", "result.participation.exercise", "result.submission" }) - Page findAllByCourseId(long courseId, Pageable pageable); + @Query(""" + SELECT b.id + FROM BuildJob b + WHERE b.courseId = :courseId + """) + List findIdsByCourseId(@Param("courseId") long courseId, Pageable pageable); + + long countBuildJobByCourseId(long courseId); + + /** + * Retrieves a paginated list of all {@link BuildJob} entities that have a given course id. + * + * @param courseId the course id. + * @param pageable the pagination information. + * @return a paginated list of {@link BuildJob} entities. If no entities are found, returns an empty page. + */ + default Page findAllWithDataByCourseId(long courseId, Pageable pageable) { + List ids = findIdsByCourseId(courseId, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List result = findWithDataByIdIn(ids); + return new PageImpl<>(result, pageable, countBuildJobByCourseId(courseId)); + } @Query(""" SELECT new de.tum.in.www1.artemis.service.connectors.localci.dto.ResultBuildJob( diff --git a/src/main/java/de/tum/in/www1/artemis/repository/PersistenceAuditEventRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/PersistenceAuditEventRepository.java index 550b9f360db8..815b57746081 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/PersistenceAuditEventRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/PersistenceAuditEventRepository.java @@ -9,8 +9,11 @@ import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import de.tum.in.www1.artemis.domain.PersistentAuditEvent; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; @@ -23,14 +26,55 @@ public interface PersistenceAuditEventRepository extends ArtemisJpaRepository findByPrincipalAndAuditEventDateAfterAndAuditEventType(String principle, Instant after, String type); - @EntityGraph(type = LOAD, attributePaths = { "data" }) - // TODO: rewrite this query, pageable does not work well with EntityGraph - Page findAllByAuditEventDateBetween(Instant fromDate, Instant toDate, Pageable pageable); + @Query(""" + SELECT p.id + FROM PersistentAuditEvent p + WHERE p.auditEventDate BETWEEN :fromDate AND :toDate + """) + List findIdsByAuditEventDateBetween(@Param("fromDate") Instant fromDate, @Param("toDate") Instant toDate, Pageable pageable); - @NotNull - @EntityGraph(type = LOAD, attributePaths = { "data" }) - // TODO: rewrite this query, pageable does not work well with EntityGraph - Page findAll(@NotNull Pageable pageable); + @EntityGraph(type = LOAD, attributePaths = "data") + List findWithDataByIdIn(List ids); + + long countByAuditEventDateBetween(Instant fromDate, Instant toDate); + + /** + * Retrieves a paginated list of {@link PersistentAuditEvent} entities that have an audit event date between the specified fromDate and toDate. + * + * @param fromDate the start date of the audit event date range (inclusive). + * @param toDate the end date of the audit event date range (inclusive). + * @param pageable the pagination information. + * @return a paginated list of {@link PersistentAuditEvent} entities within the specified date range. If no entities are found, returns an empty page. + */ + default Page findAllWithDataByAuditEventDateBetween(Instant fromDate, Instant toDate, Pageable pageable) { + List ids = findIdsByAuditEventDateBetween(fromDate, toDate, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List result = findWithDataByIdIn(ids); + return new PageImpl<>(result, pageable, countByAuditEventDateBetween(fromDate, toDate)); + } + + @Query(""" + SELECT p.id + FROM PersistentAuditEvent p + """) + List findAllIds(Pageable pageable); + + /** + * Retrieves a paginated list of {@link PersistentAuditEvent} entities. + * + * @param pageable the pagination information. + * @return a paginated list of {@link PersistentAuditEvent} entities. If no entities are found, returns an empty page. + */ + default Page findAllWithData(@NotNull Pageable pageable) { + List ids = findAllIds(pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List result = findWithDataByIdIn(ids); + return new PageImpl<>(result, pageable, count()); + } @NotNull @EntityGraph(type = LOAD, attributePaths = { "data" }) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java index 0e9adffb55fd..e913d375a633 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java @@ -3,12 +3,14 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; +import java.util.Collections; import java.util.List; import java.util.Optional; import jakarta.validation.constraints.NotNull; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; @@ -17,6 +19,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; +import de.tum.in.www1.artemis.service.dto.ProgrammingSubmissionIdAndSubmissionDateDTO; /** * Spring Data JPA repository for the ProgrammingSubmission entity. @@ -44,36 +47,72 @@ default ProgrammingSubmission findFirstByParticipationIdAndCommitHashOrderByIdDe return findByParticipationIdAndCommitHashOrderByIdDescWithFeedbacksAndTeamStudents(participationId, commitHash).stream().findFirst().orElse(null); } - @EntityGraph(type = LOAD, attributePaths = "results") - Optional findFirstByParticipationIdOrderBySubmissionDateDesc(long participationId); + @Query(value = """ + SELECT new de.tum.in.www1.artemis.service.dto.ProgrammingSubmissionIdAndSubmissionDateDTO(ps.id, ps.submissionDate) + FROM ProgrammingSubmission ps + WHERE ps.participation.id = :participationId ORDER BY ps.submissionDate DESC + """) + List findFirstIdByParticipationIdOrderBySubmissionDateDesc(@Param("participationId") long participationId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = { "results" }) + Optional findProgrammingSubmissionWithResultsById(long programmingSubmissionId); /** - * Provide a list of graded submissions. To be graded a submission must: - * - be of type 'INSTRUCTOR' or 'TEST' - * - have a submission date before the exercise due date - * - or related to an exercise without a due date + * Finds the first programming submission by participation ID, including its results, ordered by submission date in descending order. To avoid in-memory paging by retrieving + * the first submission directly from the database. * - * @param participationId to which the submissions belong. - * @param pageable Pageable - * @return ProgrammingSubmission list (can be empty!) + * @param programmingSubmissionId the ID of the participation to find the submission for + * @return an {@code Optional} containing the first {@code ProgrammingSubmission} with results, ordered by submission date in descending order, + * or an empty {@code Optional} if no submission is found */ + default Optional findFirstByParticipationIdWithResultsOrderBySubmissionDateDesc(long programmingSubmissionId) { + Pageable pageable = PageRequest.of(0, 1); // fetch the first row + // probably is not the prettiest variant, but we need a way to fetch the first row only, as sql limit does not work with JPQL, as the latter is SQL agnostic + List result = findFirstIdByParticipationIdOrderBySubmissionDateDesc(programmingSubmissionId, pageable); + if (result.isEmpty()) { + return Optional.empty(); + } + long id = result.getFirst().programmingSubmissionId(); + return findProgrammingSubmissionWithResultsById(id); + } + @Query(""" - SELECT s + SELECT new de.tum.in.www1.artemis.service.dto.ProgrammingSubmissionIdAndSubmissionDateDTO(s.id, s.submissionDate) FROM ProgrammingSubmission s - LEFT JOIN s.participation p - LEFT JOIN p.exercise e - LEFT JOIN FETCH s.results r + JOIN s.participation p + JOIN p.exercise e WHERE p.id = :participationId - AND ( - s.type = de.tum.in.www1.artemis.domain.enumeration.SubmissionType.INSTRUCTOR + AND (s.type = de.tum.in.www1.artemis.domain.enumeration.SubmissionType.INSTRUCTOR OR s.type = de.tum.in.www1.artemis.domain.enumeration.SubmissionType.TEST OR e.dueDate IS NULL - OR s.submissionDate <= e.dueDate - ) + OR s.submissionDate <= e.dueDate) ORDER BY s.submissionDate DESC """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - List findGradedByParticipationIdOrderBySubmissionDateDesc(@Param("participationId") long participationId, Pageable pageable); + List findSubmissionIdsAndDatesByParticipationId(@Param("participationId") long participationId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = { "results" }) + List findSubmissionsWithResultsByIdIn(List ids); + + /** + * Provide a list of graded submissions. To be graded a submission must: + * - be of type 'INSTRUCTOR' or 'TEST' + * - have a submission date before the exercise due date + * - or related to an exercise without a due date + * + * @param participationId to which the submissions belong. + * @param pageable Pageable + * @return ProgrammingSubmission list (can be empty!) + */ + default List findGradedByParticipationIdWithResultsOrderBySubmissionDateDesc(long participationId, Pageable pageable) { + List ids = findSubmissionIdsAndDatesByParticipationId(participationId, pageable).stream().map(ProgrammingSubmissionIdAndSubmissionDateDTO::programmingSubmissionId) + .toList(); + + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + return findSubmissionsWithResultsByIdIn(ids); + } @EntityGraph(type = LOAD, attributePaths = "results.feedbacks") Optional findWithEagerResultsAndFeedbacksById(long submissionId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java index 01d6f7b4e939..d0a20634642a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java @@ -7,6 +7,7 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,6 +22,7 @@ import org.springframework.stereotype.Repository; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.ExampleSubmission; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.Feedback; @@ -63,17 +65,9 @@ public interface ResultRepository extends ArtemisJpaRepository { @EntityGraph(type = LOAD, attributePaths = { "submission", "feedbacks" }) List findWithEagerSubmissionAndFeedbackByParticipationExerciseId(long exerciseId); - /** - * Get the latest results for each programming exercise student participation in an exercise from the database together with the list of feedback items. - * - * @param exerciseId the id of the exercise to load from the database - * @return a list of results. - */ @Query(""" SELECT DISTINCT r FROM Result r - LEFT JOIN FETCH r.feedbacks f - LEFT JOIN FETCH f.testCase LEFT JOIN TREAT (r.participation AS ProgrammingExerciseStudentParticipation) sp WHERE r.completionDate = ( SELECT MAX(rr.completionDate) @@ -87,20 +81,109 @@ LEFT JOIN TREAT (rr.participation AS ProgrammingExerciseStudentParticipation) sp AND sp.student IS NOT NULL ORDER BY r.completionDate ASC """) - List findLatestAutomaticResultsWithEagerFeedbacksForExercise(@Param("exerciseId") long exerciseId); + List findLatestAutomaticResultsForExercise(@Param("exerciseId") long exerciseId); @EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase" }) - Optional findFirstWithFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(long participationId); + List findResultsWithFeedbacksAndTestCaseByIdIn(List ids); + + /** + * Get the latest results for each programming exercise student participation in an exercise from the database together with the list of feedback items. + * + * @param exerciseId the id of the exercise to load from the database + * @return a list of results. + */ + default List findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExercise(long exerciseId) { + List ids = findLatestAutomaticResultsForExercise(exerciseId).stream().map(DomainObject::getId).toList(); - @EntityGraph(type = LOAD, attributePaths = { "submission", "feedbacks", "feedbacks.testCase" }) - Optional findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(long participationId); + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + return findResultsWithFeedbacksAndTestCaseByIdIn(ids); + } - @EntityGraph(type = LOAD, attributePaths = "submission") Optional findFirstByParticipationIdOrderByCompletionDateDesc(long participationId); + @EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase" }) + Optional findResultWithFeedbacksAndTestCasesById(long resultId); + + /** + * Finds the first result by participation ID, including its feedback and test cases, ordered by completion date in descending order. + * This method avoids in-memory paging by retrieving the first result directly from the database. + * + * @param participationId the ID of the participation to find the result for + * @return an {@code Optional} containing the first {@code Result} with feedback and test cases, ordered by completion date in descending order, + * or an empty {@code Optional} if no result is found + */ + default Optional findFirstWithFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(long participationId) { + var resultOptional = findFirstByParticipationIdOrderByCompletionDateDesc(participationId); + if (resultOptional.isEmpty()) { + return Optional.empty(); + } + var id = resultOptional.get().getId(); + return findResultWithFeedbacksAndTestCasesById(id); + } + + @EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase", "submission" }) + Optional findResultWithSubmissionAndFeedbacksTestCasesById(long resultId); + + /** + * Finds the first result by participation ID, including its submission, feedback, and test cases, ordered by completion date in descending order. + * This method avoids in-memory paging by retrieving the first result directly from the database. + * + * @param participationId the ID of the participation to find the result for + * @return an {@code Optional} containing the first {@code Result} with submission, feedback, and test cases, ordered by completion date in descending order, + * or an empty {@code Optional} if no result is found + */ + default Optional findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(long participationId) { + var resultOptional = findFirstByParticipationIdOrderByCompletionDateDesc(participationId); + if (resultOptional.isEmpty()) { + return Optional.empty(); + } + var id = resultOptional.get().getId(); + return findResultWithSubmissionAndFeedbacksTestCasesById(id); + } + @EntityGraph(type = LOAD, attributePaths = "submission") + Optional findResultWithSubmissionsById(long resultId); + + /** + * Finds the first result by participation ID, including its submissions, ordered by completion date in descending order. + * This method avoids in-memory paging by retrieving the first result directly from the database. + * + * @param participationId the ID of the participation to find the result for + * @return an {@code Optional} containing the first {@code Result} with submissions, ordered by completion date in descending order, + * or an empty {@code Optional} if no result is found + */ + default Optional findFirstWithSubmissionsByParticipationIdOrderByCompletionDateDesc(long participationId) { + var resultOptional = findFirstByParticipationIdOrderByCompletionDateDesc(participationId); + if (resultOptional.isEmpty()) { + return Optional.empty(); + } + var id = resultOptional.get().getId(); + return findResultWithSubmissionsById(id); + } + Optional findFirstByParticipationIdAndRatedOrderByCompletionDateDesc(long participationId, boolean rated); + /** + * Finds the first rated or unrated result by participation ID, including its submission, ordered by completion date in descending order. + * This method avoids in-memory paging by retrieving the first result directly from the database. + * + * @param participationId the ID of the participation to find the result for + * @param rated a boolean indicating whether to find a rated or unrated result + * @return an {@code Optional} containing the first {@code Result} with submissions, ordered by completion date in descending order, + * or an empty {@code Optional} if no result is found + */ + default Optional findFirstByParticipationIdAndRatedWithSubmissionOrderByCompletionDateDesc(long participationId, boolean rated) { + var resultOptional = findFirstByParticipationIdAndRatedOrderByCompletionDateDesc(participationId, rated); + if (resultOptional.isEmpty()) { + return Optional.empty(); + } + var id = resultOptional.get().getId(); + return findResultWithSubmissionsById(id); + } + Optional findDistinctBySubmissionId(long submissionId); @EntityGraph(type = LOAD, attributePaths = "feedbacks") @@ -715,7 +798,7 @@ default ResultWithPointsPerGradingCriterionDTO calculatePointsPerGradingCriterio */ default Optional findLatestResultWithFeedbacksForParticipation(long participationId, boolean withSubmission) { if (withSubmission) { - return findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participationId); + return findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participationId); } else { return findFirstWithFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participationId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java index 118a8e6b8f8a..6f7746473011 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/SolutionProgrammingExerciseParticipationRepository.java @@ -60,7 +60,7 @@ default SolutionProgrammingExerciseParticipation findWithEagerResultsAndSubmissi default SolutionProgrammingExerciseParticipation findByExerciseIdElseThrow(final Specification specification, long exerciseId) { final Specification hasExerciseIdSpec = (root, query, criteriaBuilder) -> criteriaBuilder .equal(root.get(TemplateProgrammingExerciseParticipation_.PROGRAMMING_EXERCISE).get(DomainObject_.ID), exerciseId); - return getValueElseThrow(findOne(specification.and(hasExerciseIdSpec))); + return getValueElseThrow(findOneBySpec(specification.and(hasExerciseIdSpec))); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 309c3d440f6f..8cd15a15b573 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -20,6 +20,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; @@ -643,30 +644,51 @@ List findByExerciseIdWithLatestSubmissionWithoutManualResu """) List findAllWithEagerSubmissionsAndEagerResultsAndEagerAssessorByExerciseIdIgnoreTestRuns(@Param("exerciseId") long exerciseId); - @Query(value = """ - SELECT p + @Query(""" + SELECT p.id FROM StudentParticipation p - LEFT JOIN FETCH p.submissions s - LEFT JOIN FETCH p.results r + JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId AND ( p.student.firstName LIKE %:partialStudentName% OR p.student.lastName LIKE %:partialStudentName% ) AND r.completionDate IS NOT NULL - """, countQuery = """ + """) + List findIdsByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = { "submissions", "submissions.results" }) + List findStudentParticipationWithSubmissionsAndResultsByIdIn(List ids); + + @Query(""" SELECT COUNT(p) FROM StudentParticipation p - LEFT JOIN p.submissions s - LEFT JOIN p.results r + JOIN Result r ON r.participation.id = p.id WHERE p.exercise.id = :exerciseId AND ( p.student.firstName LIKE %:partialStudentName% OR p.student.lastName LIKE %:partialStudentName% ) AND r.completionDate IS NOT NULL """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page findAllWithEagerSubmissionsAndEagerResultsByExerciseId(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName, - Pageable pageable); + long countByExerciseIdAndStudentName(@Param("exerciseId") long exerciseId, @Param("partialStudentName") String partialStudentName); + + /** + * Retrieves a paginated list of {@link StudentParticipation} entities associated with a specific exercise, + * and optionally filtered by a partial student name. The entities are fetched with eager loading of submissions and results. + * + * @param exerciseId the ID of the exercise. + * @param partialStudentName the partial name of the student to filter by (can be empty or null to include all students). + * @param pageable the pagination information. + * @return a paginated list of {@link StudentParticipation} entities associated with the specified exercise and student name filter. + * If no entities are found, returns an empty page. + */ + default Page findAllWithEagerSubmissionsAndResultsByExerciseId(long exerciseId, String partialStudentName, Pageable pageable) { + List ids = findIdsByExerciseIdAndStudentName(exerciseId, partialStudentName, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List result = findStudentParticipationWithSubmissionsAndResultsByIdIn(ids); + return new PageImpl<>(result, pageable, countByExerciseIdAndStudentName(exerciseId, partialStudentName)); + } @Query(""" SELECT DISTINCT p diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java index 5fc0036ae03c..ee171c7922d3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/TemplateProgrammingExerciseParticipationRepository.java @@ -55,7 +55,7 @@ default TemplateProgrammingExerciseParticipation findWithEagerResultsAndSubmissi default TemplateProgrammingExerciseParticipation findByExerciseIdElseThrow(final Specification specification, long exerciseId) { final Specification hasExerciseIdSpec = (root, query, criteriaBuilder) -> criteriaBuilder .equal(root.get(TemplateProgrammingExerciseParticipation_.PROGRAMMING_EXERCISE).get(DomainObject_.ID), exerciseId); - return getValueElseThrow(findOne(specification.and(hasExerciseIdSpec))); + return getValueElseThrow(findOneBySpec(specification.and(hasExerciseIdSpec))); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index 2da593f700c2..36a7a44ae042 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -13,6 +13,7 @@ import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -23,6 +24,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -38,6 +40,7 @@ import de.tum.in.www1.artemis.domain.ConversationNotificationRecipientSummary; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.Organization; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.SortingOrder; @@ -263,84 +266,121 @@ ORDER BY CONCAT(user.firstName, ' ', user.lastName) """) List searchByNameInGroups(@Param("groupNames") Set groupNames, @Param("nameOfUser") String nameOfUser); - /** - * Search for all users by login or name in a group - * - * @param pageable Pageable configuring paginated access (e.g. to limit the number of records returned) - * @param loginOrName Search query that will be searched for in login and name field - * @param groupName Name of group in which to search for users - * @return all users matching search criteria in the group converted to DTOs - */ - @Query(value = """ - SELECT user + @Query(""" + SELECT user.id FROM User user - LEFT JOIN FETCH user.groups userGroup + LEFT JOIN user.groups userGroup WHERE user.isDeleted = FALSE AND :groupName = userGroup AND ( - user.login LIKE :#{#loginOrName}% - OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + user.login LIKE %:loginOrName% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:loginOrName% ) - """, countQuery = """ + """) + List findUserIdsByLoginOrNameInGroup(@Param("loginOrName") String loginOrName, @Param("groupName") String groupName, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = "groups") + List findUsersWithGroupsByIdIn(List ids); + + @Query(""" SELECT COUNT(user) FROM User user LEFT JOIN user.groups userGroup WHERE user.isDeleted = FALSE AND :groupName = userGroup AND ( - user.login LIKE :#{#loginOrName}% - OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + user.login LIKE %:loginOrName% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:loginOrName% ) """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page searchAllByLoginOrNameInGroup(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("groupName") String groupName); + long countUsersByLoginOrNameInGroup(@Param("loginOrName") String loginOrName, @Param("groupName") String groupName); - @Query(value = """ - SELECT user + /** + * Search for all users by login or name in a group + * + * @param pageable Pageable configuring paginated access (e.g. to limit the number of records returned) + * @param loginOrName Search query that will be searched for in login and name field + * @param groupName Name of group in which to search for users + * @return all users matching search criteria in the group converted to DTOs + */ + default Page searchAllWithGroupsByLoginOrNameInGroup(Pageable pageable, String loginOrName, String groupName) { + List ids = findUserIdsByLoginOrNameInGroup(loginOrName, groupName, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersWithGroupsByIdIn(ids); + return new PageImpl<>(users, pageable, countUsersByLoginOrNameInGroup(loginOrName, groupName)); + } + + @Query(""" + SELECT user.id FROM User user - LEFT JOIN FETCH user.groups userGroup + LEFT JOIN user.groups userGroup WHERE user.isDeleted = FALSE AND userGroup IN :groupNames AND ( - user.login LIKE :#{#loginOrName}% - OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + user.login LIKE %:loginOrName% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:loginOrName% ) AND user.id <> :idOfUser + """) + List findUserIdsByLoginOrNameInGroupsNotUserId(@Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames, @Param("idOfUser") long idOfUser, + Pageable pageable); + + @Query(""" + SELECT user + FROM User user + LEFT JOIN FETCH user.groups userGroup + WHERE user.id IN :ids ORDER BY CONCAT(user.firstName, ' ', user.lastName) - """, countQuery = """ + """) + List findUsersByIdsWithGroupsOrdered(@Param("ids") List ids); + + @Query(""" SELECT COUNT(user) FROM User user LEFT JOIN user.groups userGroup WHERE user.isDeleted = FALSE AND userGroup IN :groupNames AND ( - user.login LIKE :#{#loginOrName}% - OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% + user.login LIKE %:loginOrName% + OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:loginOrName% ) AND user.id <> :idOfUser - ORDER BY CONCAT(user.firstName, ' ', user.lastName) """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page searchAllByLoginOrNameInGroupsNotUserId(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames, - @Param("idOfUser") long idOfUser); + long countUsersByLoginOrNameInGroupsNotUserId(@Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames, @Param("idOfUser") long idOfUser); /** - * Search for all users by login or name within the provided groups + * Searches for {@link User} entities by login or name within specified groups, excluding a specific user ID. + * The results are paginated. * - * @param pageable Pageable configuring paginated access (e.g. to limit the number of records returned) - * @param loginOrName Search query that will be searched for in login and name field - * @param groupNames Names of groups in which to search for users - * @return All users matching search criteria + * @param pageable the pagination information. + * @param loginOrName the login or name to search for. + * @param groupNames the set of group names to limit the search within. + * @param idOfUser the ID of the user to exclude from the search results. + * @return a paginated list of {@link User} entities matching the search criteria. If no entities are found, returns an empty page. */ - @Query(value = """ - SELECT user + default Page searchAllWithGroupsByLoginOrNameInGroupsNotUserId(Pageable pageable, String loginOrName, Set groupNames, long idOfUser) { + List ids = findUserIdsByLoginOrNameInGroupsNotUserId(loginOrName, groupNames, idOfUser, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersByIdsWithGroupsOrdered(ids); + return new PageImpl<>(users, pageable, countUsersByLoginOrNameInGroupsNotUserId(loginOrName, groupNames, idOfUser)); + } + + @Query(""" + SELECT user.id FROM User user - LEFT JOIN FETCH user.groups userGroup + LEFT JOIN user.groups userGroup WHERE user.isDeleted = FALSE AND userGroup IN :groupNames AND ( user.login LIKE :#{#loginOrName}% OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) - """, countQuery = """ + """) + List findUserIdsByLoginOrNameInGroups(@Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames, Pageable pageable); + + @Query(""" SELECT COUNT(user) FROM User user LEFT JOIN user.groups userGroup @@ -351,13 +391,28 @@ SELECT COUNT(user) OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page searchAllByLoginOrNameInGroups(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames); + long countUsersByLoginOrNameInGroups(@Param("loginOrName") String loginOrName, @Param("groupNames") Set groupNames); - @Query(value = """ + /** + * Search for all users by login or name within the provided groups + * + * @param pageable Pageable configuring paginated access (e.g. to limit the number of records returned) + * @param loginOrName Search query that will be searched for in login and name field + * @param groupNames Names of groups in which to search for users + * @return All users matching search criteria + */ + default Page searchAllWithGroupsByLoginOrNameInGroups(Pageable pageable, String loginOrName, Set groupNames) { + List ids = findUserIdsByLoginOrNameInGroups(loginOrName, groupNames, pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersWithGroupsByIdIn(ids); + return new PageImpl<>(users, pageable, countUsersByLoginOrNameInGroups(loginOrName, groupNames)); + } + + @Query(""" SELECT DISTINCT user FROM User user - LEFT JOIN FETCH user.groups JOIN ConversationParticipant conversationParticipant ON conversationParticipant.user.id = user.id JOIN Conversation conversation ON conversation.id = conversationParticipant.conversation.id WHERE user.isDeleted = FALSE @@ -367,7 +422,10 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% OR user.login LIKE :#{#loginOrName}% OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) - """, countQuery = """ + """) + List findUsersByLoginOrNameInConversation(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId, Pageable pageable); + + @Query(""" SELECT COUNT(DISTINCT user) FROM User user JOIN ConversationParticipant conversationParticipant ON conversationParticipant.user.id = user.id @@ -380,23 +438,45 @@ SELECT COUNT(DISTINCT user) OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page searchAllByLoginOrNameInConversation(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId); + long countUsersByLoginOrNameInConversation(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId); - @Query(value = """ - SELECT DISTINCT user - FROM User user - LEFT JOIN FETCH user.groups userGroup - JOIN ConversationParticipant conversationParticipant ON conversationParticipant.user.id = user.id - JOIN Conversation conversation ON conversation.id = conversationParticipant.conversation.id - WHERE user.isDeleted = FALSE + /** + * Searches for {@link User} entities by login or name within a specific conversation. + * The results are paginated. + * + * @param pageable the pagination information. + * @param loginOrName the login or name to search for. + * @param conversationId the ID of the conversation to limit the search within. + * @return a paginated list of {@link User} entities matching the search criteria. If no entities are found, returns an empty page. + */ + default Page searchAllWithGroupsByLoginOrNameInConversation(Pageable pageable, String loginOrName, long conversationId) { + List ids = findUsersByLoginOrNameInConversation(loginOrName, conversationId, pageable).stream().map(DomainObject::getId).toList(); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersWithGroupsByIdIn(ids); + long total = countUsersByLoginOrNameInConversation(loginOrName, conversationId); + return new PageImpl<>(users, pageable, total); + } + + @Query(""" + SELECT DISTINCT user + FROM User user + JOIN user.groups userGroup + JOIN ConversationParticipant conversationParticipant ON conversationParticipant.user.id = user.id + JOIN Conversation conversation ON conversation.id = conversationParticipant.conversation.id + WHERE user.isDeleted = FALSE AND conversation.id = :conversationId AND ( :loginOrName = '' OR user.login LIKE :#{#loginOrName}% OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) AND userGroup IN :groupNames - """, countQuery = """ + """) + List findUsersByLoginOrNameInConversationWithCourseGroups(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId, + @Param("groupNames") Set groupNames, Pageable pageable); + + @Query(""" SELECT COUNT(DISTINCT user) FROM User user JOIN user.groups userGroup @@ -410,14 +490,33 @@ SELECT COUNT(DISTINCT user) OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) AND userGroup IN :groupNames """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - Page searchAllByLoginOrNameInConversationWithCourseGroups(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId, + long countUsersByLoginOrNameInConversationWithCourseGroups(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId, @Param("groupNames") Set groupNames); - @Query(value = """ + /** + * Searches for {@link User} entities by login or name within a specific conversation and course groups. + * The results are paginated. + * + * @param pageable the pagination information. + * @param loginOrName the login or name to search for. + * @param conversationId the ID of the conversation to limit the search within. + * @param groupNames the set of course group names to limit the search within. + * @return a paginated list of {@link User} entities matching the search criteria. If no entities are found, returns an empty page. + */ + default Page searchAllWithCourseGroupsByLoginOrNameInConversation(Pageable pageable, String loginOrName, long conversationId, Set groupNames) { + List ids = findUsersByLoginOrNameInConversationWithCourseGroups(loginOrName, conversationId, groupNames, pageable).stream().map(DomainObject::getId).toList(); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersWithGroupsByIdIn(ids); + long total = countUsersByLoginOrNameInConversationWithCourseGroups(loginOrName, conversationId, groupNames); + return new PageImpl<>(users, pageable, total); + } + + @Query(""" SELECT DISTINCT user FROM User user - JOIN FETCH user.groups userGroup + JOIN user.groups userGroup JOIN ConversationParticipant conversationParticipant ON conversationParticipant.user.id = user.id JOIN Conversation conversation ON conversation.id = conversationParticipant.conversation.id WHERE user.isDeleted = FALSE @@ -427,7 +526,10 @@ Page searchAllByLoginOrNameInConversationWithCourseGroups(Pageable pageabl OR user.login LIKE :#{#loginOrName}% OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) AND conversationParticipant.isModerator = TRUE - """, countQuery = """ + """) + List findModeratorsByLoginOrNameInConversation(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId, Pageable pageable); + + @Query(""" SELECT COUNT(DISTINCT user) FROM User user JOIN user.groups userGroup @@ -441,8 +543,26 @@ SELECT COUNT(DISTINCT user) OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% ) AND conversationParticipant.isModerator = TRUE """) - // TODO: rewrite this query, pageable does not work well with left join fetch - Page searchChannelModeratorsByLoginOrNameInConversation(Pageable pageable, @Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId); + long countModeratorsByLoginOrNameInConversation(@Param("loginOrName") String loginOrName, @Param("conversationId") long conversationId); + + /** + * Searches for channel moderator {@link User} entities by login or name within a specific conversation. + * The results are paginated. + * + * @param pageable the pagination information. + * @param loginOrName the login or name to search for. + * @param conversationId the ID of the conversation to limit the search within. + * @return a paginated list of channel moderator {@link User} entities matching the search criteria. If no entities are found, returns an empty page. + */ + default Page searchChannelModeratorsWithGroupsByLoginOrNameInConversation(Pageable pageable, String loginOrName, long conversationId) { + List ids = findModeratorsByLoginOrNameInConversation(loginOrName, conversationId, pageable).stream().map(DomainObject::getId).toList(); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findDistinctUsersWithGroupsByIdIn(ids); // these users are moderators + long total = countModeratorsByLoginOrNameInConversation(loginOrName, conversationId); + return new PageImpl<>(users, pageable, total); + } /** * Search for all users by login or name in a group and convert them to {@link UserDTO} @@ -453,7 +573,7 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% * @return all users matching search criteria in the group converted to {@link UserDTO} */ default Page searchAllUsersByLoginOrNameInGroupAndConvertToDTO(Pageable pageable, String loginOrName, String groupName) { - Page users = searchAllByLoginOrNameInGroup(pageable, loginOrName, groupName); + Page users = searchAllWithGroupsByLoginOrNameInGroup(pageable, loginOrName, groupName); return users.map(UserDTO::new); } @@ -484,18 +604,10 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% """) Page searchAllByLoginOrName(Pageable page, @Param("loginOrName") String loginOrName); - /** - * Searches for users in a course by their login or full name. - * - * @param page Pageable related info (e.g. for page size) - * @param loginOrName Either a login (e.g. ga12abc) or name (e.g. Max Mustermann) by which to search - * @param courseId Id of the course the user has to be a member of - * @return list of found users that match the search criteria - */ - @Query(value = """ + @Query(""" SELECT DISTINCT user FROM User user - LEFT JOIN FETCH user.groups userGroup + JOIN user.groups userGroup JOIN Course course ON course.id = :courseId WHERE user.isDeleted = FALSE AND ( @@ -506,11 +618,17 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% OR course.teachingAssistantGroupName = userGroup OR course.editorGroupName = userGroup OR course.instructorGroupName = userGroup - ) - """, countQuery = """ + ) + """) + List findUsersByLoginOrNameInCourse(@Param("loginOrName") String loginOrName, @Param("courseId") long courseId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = "groups") + List findDistinctUsersWithGroupsByIdIn(List ids); + + @Query(""" SELECT COUNT(DISTINCT user) FROM User user - LEFT JOIN user.groups userGroup + JOIN user.groups userGroup JOIN Course course ON course.id = :courseId WHERE user.isDeleted = FALSE AND ( @@ -523,11 +641,79 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% OR course.instructorGroupName = userGroup ) """) - Page searchAllByLoginOrNameInCourse(Pageable page, @Param("loginOrName") String loginOrName, @Param("courseId") long courseId); + long countUsersByLoginOrNameInCourse(@Param("loginOrName") String loginOrName, @Param("courseId") long courseId); - // TODO: rewrite this query, pageable does not work well with left join fetch - @EntityGraph(type = LOAD, attributePaths = { "groups" }) - Page findAllWithGroupsByIsDeletedIsFalse(Pageable pageable); + /** + * Searches for users by login or name within a course and returns a list of distinct users along with their groups. + * This method avoids in-memory paging by retrieving the user IDs directly from the database. + * + * @param pageable the pagination information + * @param loginOrName the login or name of the users to search for + * @param courseId the ID of the course to search within + * @return a list of distinct users with their groups, or an empty list if no users are found + */ + default List searchAllWithGroupsByLoginOrNameInCourseAndReturnList(Pageable pageable, String loginOrName, long courseId) { + List userIds = findUsersByLoginOrNameInCourse(loginOrName, courseId, pageable).stream().map(DomainObject::getId).toList(); + + if (userIds.isEmpty()) { + return Collections.emptyList(); + } + + return findDistinctUsersWithGroupsByIdIn(userIds); + } + + /** + * Searches for users by login or name within a course and returns a paginated list of distinct users along with their groups. + * This method avoids in-memory paging by retrieving the user IDs directly from the database. + * + * @param pageable the pagination information + * @param loginOrName the login or name of the users to search for + * @param courseId the ID of the course to search within + * @return a {@code Page} containing a list of distinct users with their groups, or an empty page if no users are found + */ + default Page searchAllWithGroupsByLoginOrNameInCourseAndReturnPage(Pageable pageable, String loginOrName, long courseId) { + List userIds = findUsersByLoginOrNameInCourse(loginOrName, courseId, pageable).stream().map(DomainObject::getId).toList(); + + if (userIds.isEmpty()) { + return new PageImpl<>(Collections.emptyList(), pageable, 0); + } + + List users = findDistinctUsersWithGroupsByIdIn(userIds); + long total = countUsersByLoginOrNameInCourse(loginOrName, courseId); + + return new PageImpl<>(users, pageable, total); + } + + @Query(""" + SELECT user.id + FROM User user + WHERE user.isDeleted = FALSE + """) + List findUserIdsByIsDeletedIsFalse(Pageable pageable); + + @Query(""" + SELECT COUNT(user) + FROM User user + WHERE user.isDeleted = FALSE + """) + long countUsersByIsDeletedIsFalse(); + + /** + * Retrieves a paginated list of {@link User} entities that are not marked as deleted, + * with their associated groups. + * + * @param pageable the pagination information. + * @return a paginated list of {@link User} entities that are not marked as deleted. If no entities are found, returns an empty page. + */ + default Page findAllWithGroupsByIsDeletedIsFalse(Pageable pageable) { + List ids = findUserIdsByIsDeletedIsFalse(pageable); + if (ids.isEmpty()) { + return Page.empty(pageable); + } + List users = findUsersWithGroupsByIdIn(ids); + long total = countUsersByIsDeletedIsFalse(); + return new PageImpl<>(users, pageable, total); + } @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalse(); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/base/ArtemisJpaRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/base/ArtemisJpaRepository.java index 70254912dbee..583caebef280 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/base/ArtemisJpaRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/base/ArtemisJpaRepository.java @@ -2,6 +2,7 @@ import java.util.Optional; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.NoRepositoryBean; @@ -63,4 +64,21 @@ public interface ArtemisJpaRepository extends JpaRepository { * @return the entity with the given id */ T findByIdElseThrow(ID id); + + /** + * Find an entity by its id and given specification without using limiting internally. + * + * @param spec the specification to apply + * @param id the id of the entity to find, it will augment spec with an and operator + * @return the entity that corresponds to spec and has the given id + */ + Optional findOneById(Specification spec, ID id); + + /** + * Find an entity by given specification without using limiting internally. + * + * @param spec the specification to apply + * @return the entity that satisfies the given specification + */ + Optional findOneBySpec(Specification spec); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/base/RepositoryImpl.java b/src/main/java/de/tum/in/www1/artemis/repository/base/RepositoryImpl.java index f8cd880cf7dd..a1aba8b81ad7 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/base/RepositoryImpl.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/base/RepositoryImpl.java @@ -3,8 +3,10 @@ import java.util.Optional; import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; import jakarta.validation.constraints.NotNull; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; @@ -38,19 +40,6 @@ public RepositoryImpl(Class domainClass, EntityManager entityManager) { this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager); } - /** - * Find an entity by its id and given specification. - * - * @param specification the specification to apply - * @param id the id of the entity to find - * @return the entity with the given id - */ - @NotNull - Optional findOneById(Specification specification, ID id) { - final Specification hasIdSpec = (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(DomainObject_.ID), id); - return findOne(specification.and(hasIdSpec)); - } - /** * Find an entity by its id and given specification or throw an EntityNotFoundException if it does not exist. * @@ -122,4 +111,63 @@ public U getArbitraryValueElseThrow(Optional optional, String id) { public T findByIdElseThrow(ID id) { return getValueElseThrow(findById(id), id); } + + /** + * Find an entity by its id and given specification without using limiting internally. + * + * @param spec the specification to apply + * @param id the id of the entity to find, it will augment spec with an and operator + * @return the entity that corresponds to spec and has the given id + */ + @NotNull + public Optional findOneById(Specification spec, ID id) { + try { + final Specification hasIdSpec = (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(DomainObject_.ID), id); + return Optional.of(this.getQuery(spec.and(hasIdSpec), Sort.unsorted()).getSingleResult()); + } + catch (NoResultException noResultException) { + return Optional.empty(); + } + } + + /** + * Find an entity by given specification without using limiting internally. + * + * @param spec the specification to apply + * @return the entity that satisfies the given specification + */ + @NotNull + public Optional findOneBySpec(Specification spec) { + try { + return Optional.of(this.getQuery(spec, Sort.unsorted()).getSingleResult()); + } + catch (NoResultException noResultException) { + return Optional.empty(); + } + } + + /** + * Find an entity by its id and given specification without using limiting internally or throw if none found. + * + * @param spec the specification to apply + * @param id the id of the entity to find, it will augment spec with an and operator + * @return the entity that corresponds to spec and has the given id + */ + @NotNull + public T findOneByIdOrElseThrow(Specification spec, ID id) { + Optional optional = findOneById(spec, id); + return optional.orElseThrow(); + } + + /** + * Find an entity by given specification without using limiting internally or throw if none found. + * + * @param spec the specification to apply + * @return the entity that satisfies the given specification + */ + @NotNull + public T findOneBySpecOrElseThrow(Specification spec) { + Optional optional = findOneBySpec(spec); + return optional.orElseThrow(); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/hestia/CoverageReportRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/hestia/CoverageReportRepository.java index 5a1c5fbe4988..b093d30782a2 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/hestia/CoverageReportRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/hestia/CoverageReportRepository.java @@ -1,20 +1,25 @@ package de.tum.in.www1.artemis.repository.hestia; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; +import java.util.Collections; import java.util.List; import java.util.Optional; import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.hestia.CoverageReport; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; +import de.tum.in.www1.artemis.service.dto.CoverageReportAndSubmissionDateDTO; /** * Spring Data JPA repository for the CoverageReport entity. @@ -30,31 +35,56 @@ public interface CoverageReportRepository extends ArtemisJpaRepository de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL) ORDER BY s.submissionDate DESC """) - // TODO: rewrite this query, pageable does not work well with left join fetch - List getLatestCoverageReportsForLegalSubmissionsForProgrammingExercise(@Param("programmingExerciseId") Long programmingExerciseId, Pageable pageable); + List findCoverageReportsByProgrammingExerciseId(@Param("programmingExerciseId") Long programmingExerciseId, Pageable pageable); - @Query(""" - SELECT DISTINCT r - FROM CoverageReport r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH r.fileReports f - LEFT JOIN FETCH f.testwiseCoverageEntries - JOIN ProgrammingExercise pe ON s.participation = pe.solutionParticipation - WHERE pe.id = :programmingExerciseId - AND (s.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL) - ORDER BY s.submissionDate DESC - """) - // TODO: rewrite this query, pageable does not work well with left join fetch, it needs to transfer all results and only page in java - List getLatestCoverageReportsForLegalSubmissionsForProgrammingExerciseWithEagerFileReportsAndEntries(@Param("programmingExerciseId") Long programmingExerciseId, - Pageable pageable); + @EntityGraph(type = LOAD, attributePaths = "submission") + List findCoverageReportsWithSubmissionByIdIn(List ids); + + /** + * Retrieves the latest coverage reports with legal submissions for a specific programming exercise, with pagination support. + * This method avoids in-memory paging by retrieving the coverage report IDs directly from the database. + * + * @param programmingExerciseId the ID of the programming exercise to retrieve the coverage reports for + * @param pageable the pagination information + * @return a list of {@code CoverageReport} with legal submissions, or an empty list if no reports are found + */ + default List getLatestCoverageReportsWithLegalSubmissionsForProgrammingExercise(Long programmingExerciseId, Pageable pageable) { + List ids = findCoverageReportsByProgrammingExerciseId(programmingExerciseId, pageable).stream().map(CoverageReportAndSubmissionDateDTO::coverageReport) + .map(DomainObject::getId).toList(); + if (ids.isEmpty()) { + return Collections.emptyList(); + } + return findCoverageReportsWithSubmissionByIdIn(ids); + } + + @EntityGraph(type = LOAD, attributePaths = { "submission", "fileReports", "fileReports.testwiseCoverageEntries" }) + List findDistinctCoverageReportsWithEagerRelationshipsByIdIn(List ids); + + /** + * Retrieves the latest coverage reports with legal submissions for a specific programming exercise, including eager loading of file reports and entries, with pagination + * support. + * This method avoids in-memory paging by retrieving the coverage report IDs directly from the database. + * + * @param programmingExerciseId the ID of the programming exercise to retrieve the coverage reports for + * @param pageable the pagination information + * @return a list of distinct {@code CoverageReport} with eager relationships, or an empty list if no reports are found + */ + default List getLatestCoverageReportsForLegalSubmissionsForProgrammingExerciseWithEagerFileReportsAndEntries(Long programmingExerciseId, Pageable pageable) { + List ids = findCoverageReportsByProgrammingExerciseId(programmingExerciseId, pageable).stream().map(CoverageReportAndSubmissionDateDTO::coverageReport) + .map(DomainObject::getId).toList(); + if (ids.isEmpty()) { + return Collections.emptyList(); + } + return findDistinctCoverageReportsWithEagerRelationshipsByIdIn(ids); + } @Query(""" SELECT DISTINCT r diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisCourseChatSessionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisCourseChatSessionRepository.java index 124feae04d42..6ba1121f72fe 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisCourseChatSessionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisCourseChatSessionRepository.java @@ -1,13 +1,18 @@ package de.tum.in.www1.artemis.repository.iris; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; + +import java.util.Collections; import java.util.List; import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.iris.session.IrisCourseChatSession; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -35,14 +40,35 @@ public interface IrisCourseChatSessionRepository extends ArtemisJpaRepository findByCourseIdAndUserId(@Param("courseId") long courseId, @Param("userId") long userId); @Query(""" - SELECT s - FROM IrisCourseChatSession s - LEFT JOIN FETCH s.messages - WHERE s.course.id = :courseId - AND s.user.id = :userId - ORDER BY s.creationDate DESC + SELECT s + FROM IrisCourseChatSession s + WHERE s.course.id = :courseId + AND s.user.id = :userId + ORDER BY s.creationDate DESC """) - List findLatestByCourseIdAndUserIdWithMessages(@Param("courseId") long courseId, @Param("userId") long userId, Pageable pageable); + List findSessionsByCourseIdAndUserId(@Param("courseId") long courseId, @Param("userId") long userId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = "messages") + List findSessionsWithMessagesByIdIn(List ids); + + /** + * Finds the latest chat sessions by course ID and user ID, including their messages, with pagination support. + * This method avoids in-memory paging by retrieving the session IDs directly from the database. + * + * @param courseId the ID of the course to find the chat sessions for + * @param userId the ID of the user to find the chat sessions for + * @param pageable the pagination information + * @return a list of {@code IrisCourseChatSession} with messages, or an empty list if no sessions are found + */ + default List findLatestByCourseIdAndUserIdWithMessages(long courseId, long userId, Pageable pageable) { + List ids = findSessionsByCourseIdAndUserId(courseId, userId, pageable).stream().map(DomainObject::getId).toList(); + + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + return findSessionsWithMessagesByIdIn(ids); + } /** * Finds a list of chat sessions or throws an exception if none are found. diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisExerciseChatSessionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisExerciseChatSessionRepository.java index c52b5b7f862c..20f32d0b36f6 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisExerciseChatSessionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisExerciseChatSessionRepository.java @@ -1,13 +1,18 @@ package de.tum.in.www1.artemis.repository.iris; +import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; + +import java.util.Collections; import java.util.List; import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.iris.session.IrisExerciseChatSession; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -38,12 +43,33 @@ public interface IrisExerciseChatSessionRepository extends ArtemisJpaRepository< @Query(""" SELECT s FROM IrisExerciseChatSession s - LEFT JOIN FETCH s.messages WHERE s.exercise.id = :exerciseId AND s.user.id = :userId ORDER BY s.creationDate DESC """) - List findLatestByExerciseIdAndUserIdWithMessages(@Param("exerciseId") Long exerciseId, @Param("userId") Long userId, Pageable pageable); + List findSessionsByExerciseIdAndUserId(@Param("exerciseId") Long exerciseId, @Param("userId") Long userId, Pageable pageable); + + @EntityGraph(type = LOAD, attributePaths = "messages") + List findSessionsWithMessagesByIdIn(List ids); + + /** + * Finds the latest chat sessions by exercise ID and user ID, including their messages, with pagination support. + * This method avoids in-memory paging by retrieving the session IDs directly from the database. + * + * @param exerciseId the ID of the exercise to find the chat sessions for + * @param userId the ID of the user to find the chat sessions for + * @param pageable the pagination information + * @return a list of {@code IrisExerciseChatSession} with messages, or an empty list if no sessions are found + */ + default List findLatestByExerciseIdAndUserIdWithMessages(Long exerciseId, Long userId, Pageable pageable) { + List ids = findSessionsByExerciseIdAndUserId(exerciseId, userId, pageable).stream().map(DomainObject::getId).toList(); + + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + return findSessionsWithMessagesByIdIn(ids); + } /** * Finds a list of chat sessions or throws an exception if none are found. diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java index 8c8287a43a27..1bf770a0f75b 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import jakarta.validation.constraints.NotNull; @@ -40,6 +41,25 @@ JOIN TREAT (m.session AS IrisChatSession) s """) int countLlmResponsesOfUserWithinTimeframe(@Param("userId") long userId, @Param("start") ZonedDateTime start, @Param("end") ZonedDateTime end); + Optional findFirstBySessionIdAndSenderOrderBySentAtDesc(long sessionId, @NotNull IrisMessageSender sender); + @EntityGraph(type = LOAD, attributePaths = { "content" }) - IrisMessage findFirstWithContentBySessionIdAndSenderOrderBySentAtDesc(long sessionId, @NotNull IrisMessageSender sender); + IrisMessage findIrisMessageById(long irisMessageId); + + /** + * Finds the first message with content by session ID and sender, ordered by the sent date in descending order. + * This method avoids in-memory paging by retrieving the message directly from the database. + * + * @param sessionId the ID of the session to find the message for + * @param sender the sender of the message + * @return the first {@code IrisMessage} with content, ordered by sent date in descending order, + * or null if no message is found + */ + default IrisMessage findFirstWithContentBySessionIdAndSenderOrderBySentAtDesc(long sessionId, @NotNull IrisMessageSender sender) { + var irisMessage = findFirstBySessionIdAndSenderOrderBySentAtDesc(sessionId, sender); + if (irisMessage.isEmpty()) { + return null; + } + return findIrisMessageById(irisMessage.get().getId()); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/plagiarism/PlagiarismResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/plagiarism/PlagiarismResultRepository.java index 6b3bd6d1a7d3..c2fde4c95d8a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/plagiarism/PlagiarismResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/plagiarism/PlagiarismResultRepository.java @@ -23,12 +23,27 @@ @Repository public interface PlagiarismResultRepository extends ArtemisJpaRepository, Long> { - @EntityGraph(type = LOAD, attributePaths = { "comparisons" }) Optional> findFirstByExerciseIdOrderByLastModifiedDateDesc(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = "comparisons") + PlagiarismResult findPlagiarismResultById(long plagiarismResultId); + + /** + * Finds the first plagiarism result by exercise ID, including its comparisons, ordered by last modified date in descending order. + * If no plagiarism result is found, this method returns null. This method avoids in-memory paging by retrieving the result directly from the database. + * + * @param exerciseId the ID of the exercise to find the plagiarism result for + * @return the first {@code PlagiarismResult} with comparisons, ordered by last modified date in descending order, + * or null if no result is found + */ @Nullable - default PlagiarismResult findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(long exerciseId) { - return findFirstByExerciseIdOrderByLastModifiedDateDesc(exerciseId).orElse(null); + default PlagiarismResult findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(long exerciseId) { + var plagiarismResultIdOrEmpty = findFirstByExerciseIdOrderByLastModifiedDateDesc(exerciseId); + if (plagiarismResultIdOrEmpty.isEmpty()) { + return null; + } + var id = plagiarismResultIdOrEmpty.get().getId(); + return findPlagiarismResultById(id); } /** @@ -38,7 +53,8 @@ default PlagiarismResult findFirstByExerciseIdOrderByLastModifiedDateDescOrNu * @return the saved result */ default PlagiarismResult savePlagiarismResultAndRemovePrevious(PlagiarismResult result) { - Optional> optionalPreviousResult = findFirstByExerciseIdOrderByLastModifiedDateDesc(result.getExercise().getId()); + Optional> optionalPreviousResult = Optional + .ofNullable(findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(result.getExercise().getId())); result = save(result); optionalPreviousResult.ifPresent(this::delete); return result; diff --git a/src/main/java/de/tum/in/www1/artemis/service/AuditEventService.java b/src/main/java/de/tum/in/www1/artemis/service/AuditEventService.java index 6f297fee5efc..8e541325f112 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/AuditEventService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/AuditEventService.java @@ -33,11 +33,11 @@ public AuditEventService(PersistenceAuditEventRepository persistenceAuditEventRe } public Page findAll(Pageable pageable) { - return persistenceAuditEventRepository.findAll(pageable).map(auditEventConverter::convertToAuditEvent); + return persistenceAuditEventRepository.findAllWithData(pageable).map(auditEventConverter::convertToAuditEvent); } public Page findByDates(Instant fromDate, Instant toDate, Pageable pageable) { - return persistenceAuditEventRepository.findAllByAuditEventDateBetween(fromDate, toDate, pageable).map(auditEventConverter::convertToAuditEvent); + return persistenceAuditEventRepository.findAllWithDataByAuditEventDateBetween(fromDate, toDate, pageable).map(auditEventConverter::convertToAuditEvent); } public Optional find(Long id) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java index ebe39ea5e91b..d0160044a60a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java @@ -822,8 +822,7 @@ private void prepareComplaintAndSubmission(Complaint complaint, Submission submi public SearchResultPageDTO getSubmissionsOnPageWithSize(SearchTermPageableSearchDTO search, Long exerciseId) { final var pageable = PageUtil.createDefaultPageRequest(search, PageUtil.ColumnMapping.STUDENT_PARTICIPATION); String searchTerm = search.getSearchTerm(); - Page studentParticipationPage = studentParticipationRepository.findAllWithEagerSubmissionsAndEagerResultsByExerciseId(exerciseId, searchTerm, - pageable); + Page studentParticipationPage = studentParticipationRepository.findAllWithEagerSubmissionsAndResultsByExerciseId(exerciseId, searchTerm, pageable); var latestSubmissions = studentParticipationPage.getContent().stream().map(Participation::findLatestSubmission).filter(Optional::isPresent).map(Optional::get).toList(); final Page submissionPage = new PageImpl<>(latestSubmissions, pageable, latestSubmissions.size()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java index e3563ed8d80d..bdaea1f46fd5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java @@ -310,7 +310,7 @@ public Page getFilteredFinishedBuildJobs(FinishedBuildJobPageableSearc List buildJobIds = buildJobIdsPage.toList(); // Fetch the build jobs with results. Since this query used "IN" clause, the order of the results is not guaranteed. We need to order them by the order of the ids. - List unorderedBuildJobs = buildJobRepository.findAllByIdWithResults(buildJobIds); + List unorderedBuildJobs = buildJobRepository.findWithDataByIdIn(buildJobIds); Map buildJobMap = unorderedBuildJobs.stream().collect(Collectors.toMap(BuildJob::getId, Function.identity())); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java index d36813fdc84b..d57a175dffad 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java @@ -203,7 +203,7 @@ public void onNewResult(StudentParticipation participation) { return; } - Optional result = resultRepository.findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); + Optional result = resultRepository.findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); if (result.isEmpty()) { log.error("onNewResult triggered for participation {} but no result could be found", participation.getId()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/CoverageReportAndSubmissionDateDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/CoverageReportAndSubmissionDateDTO.java new file mode 100644 index 000000000000..404ba4b0d51b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/CoverageReportAndSubmissionDateDTO.java @@ -0,0 +1,11 @@ +package de.tum.in.www1.artemis.service.dto; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.hestia.CoverageReport; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CoverageReportAndSubmissionDateDTO(CoverageReport coverageReport, ZonedDateTime submissionDate) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/ProgrammingSubmissionIdAndSubmissionDateDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/ProgrammingSubmissionIdAndSubmissionDateDTO.java new file mode 100644 index 000000000000..293c3eac02fc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/ProgrammingSubmissionIdAndSubmissionDateDTO.java @@ -0,0 +1,9 @@ +package de.tum.in.www1.artemis.service.dto; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ProgrammingSubmissionIdAndSubmissionDateDTO(long programmingSubmissionId, ZonedDateTime submissionDate) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java index 76f4f3d6e60d..dbd7e90afb51 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java @@ -99,8 +99,8 @@ public ProgrammingExerciseGitDiffReport updateReport(ProgrammingExercise program var templateParticipation = templateParticipationOptional.get(); var solutionParticipation = solutionParticipationOptional.get(); - var templateSubmissionOptional = programmingSubmissionRepository.findFirstByParticipationIdOrderBySubmissionDateDesc(templateParticipation.getId()); - var solutionSubmissionOptional = programmingSubmissionRepository.findFirstByParticipationIdOrderBySubmissionDateDesc(solutionParticipation.getId()); + var templateSubmissionOptional = programmingSubmissionRepository.findFirstByParticipationIdWithResultsOrderBySubmissionDateDesc(templateParticipation.getId()); + var solutionSubmissionOptional = programmingSubmissionRepository.findFirstByParticipationIdWithResultsOrderBySubmissionDateDesc(solutionParticipation.getId()); if (templateSubmissionOptional.isEmpty() || solutionSubmissionOptional.isEmpty()) { return null; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java index aa431bba14ca..1141f72bbc4b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java @@ -290,7 +290,7 @@ private Map calculateAndSaveUniqueLineCountsByFilePath(Coverage * if a report exists for the latest submission, otherwise an empty Optional */ public Optional getCoverageReportForLatestSolutionSubmissionFromProgrammingExercise(long exerciseId) { - var reports = coverageReportRepository.getLatestCoverageReportsForLegalSubmissionsForProgrammingExercise(exerciseId, Pageable.ofSize(1)); + var reports = coverageReportRepository.getLatestCoverageReportsWithLegalSubmissionsForProgrammingExercise(exerciseId, Pageable.ofSize(1)); if (reports.isEmpty()) { return Optional.empty(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java b/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java index f27a5c22e902..fcc75c920e2b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java @@ -371,9 +371,9 @@ public Page searchMembersOfConversation(Course course, Conversation conver Optional filter) { if (filter.isEmpty()) { if (conversation instanceof Channel channel && channel.getIsCourseWide()) { - return userRepository.searchAllByLoginOrNameInCourse(pageable, searchTerm, course.getId()); + return userRepository.searchAllWithGroupsByLoginOrNameInCourseAndReturnPage(pageable, searchTerm, course.getId()); } - return userRepository.searchAllByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); + return userRepository.searchAllWithGroupsByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); } else { var groups = new HashSet(); @@ -389,16 +389,16 @@ public Page searchMembersOfConversation(Course course, Conversation conver if (!(conversation instanceof Channel)) { throw new IllegalArgumentException("The filter CHANNEL_MODERATOR is only allowed for channels!"); } - return userRepository.searchChannelModeratorsByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); + return userRepository.searchChannelModeratorsWithGroupsByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); } default -> throw new IllegalArgumentException("The filter is not supported."); } if (conversation instanceof Channel channel && channel.getIsCourseWide()) { - return userRepository.searchAllByLoginOrNameInGroups(pageable, searchTerm, groups); + return userRepository.searchAllWithGroupsByLoginOrNameInGroups(pageable, searchTerm, groups); } - return userRepository.searchAllByLoginOrNameInConversationWithCourseGroups(pageable, searchTerm, conversation.getId(), groups); + return userRepository.searchAllWithCourseGroupsByLoginOrNameInConversation(pageable, searchTerm, conversation.getId(), groups); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseGradingService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseGradingService.java index 2118a1453026..af1e9e150a89 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseGradingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseGradingService.java @@ -1008,7 +1008,7 @@ public ProgrammingExerciseGradingStatisticsDTO generateGradingStatistics(Long ex categoryIssuesStudentsMap.put(category.getName(), new HashMap<>()); } - final var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksForExercise(exerciseId); + final var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExercise(exerciseId); for (Result result : results) { // number of detected issues per category for this result final var categoryIssuesMap = new HashMap(); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java index 73f710ddd31d..a14ff5f906e9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java @@ -354,8 +354,8 @@ private Optional findLatestPendingSubmissionForParticipat private Optional findLatestPendingSubmissionForParticipation(final long participationId, final boolean isGraded) { final var optionalSubmission = isGraded - ? programmingSubmissionRepository.findGradedByParticipationIdOrderBySubmissionDateDesc(participationId, PageRequest.of(0, 1)).stream().findFirst() - : programmingSubmissionRepository.findFirstByParticipationIdOrderBySubmissionDateDesc(participationId); + ? programmingSubmissionRepository.findGradedByParticipationIdWithResultsOrderBySubmissionDateDesc(participationId, PageRequest.of(0, 1)).stream().findFirst() + : programmingSubmissionRepository.findFirstByParticipationIdWithResultsOrderBySubmissionDateDesc(participationId); if (optionalSubmission.isEmpty() || optionalSubmission.get().getLatestResult() != null) { // This is not an error case, it is very likely that there is no pending submission for a participation. diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 7bb1d0e12ba8..0c2749ccb50d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -1058,7 +1058,7 @@ public ResponseEntity> searchUsersInCourse(@PathVariable groups.add(course.getInstructorGroupName()); } User searchingUser = userRepository.getUser(); - var originalPage = userRepository.searchAllByLoginOrNameInGroupsNotUserId(PageRequest.of(0, 25), loginOrName, groups, searchingUser.getId()); + var originalPage = userRepository.searchAllWithGroupsByLoginOrNameInGroupsNotUserId(PageRequest.of(0, 25), loginOrName, groups, searchingUser.getId()); var resultDTOs = new ArrayList(); for (var user : originalPage) { @@ -1152,7 +1152,7 @@ public ResponseEntity> searchMembersOfCourse(@PathVari authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); var searchTerm = loginOrName != null ? loginOrName.toLowerCase().trim() : ""; - List searchResults = userRepository.searchAllByLoginOrNameInCourse(Pageable.ofSize(10), searchTerm, course.getId()).stream() + List searchResults = userRepository.searchAllWithGroupsByLoginOrNameInCourseAndReturnList(Pageable.ofSize(10), searchTerm, course.getId()).stream() .map(UserNameAndLoginDTO::of).toList(); return ResponseEntity.ok().body(searchResults); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java index 739cd37df64f..f8ee4659f3ff 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java @@ -410,7 +410,8 @@ public ResponseEntity> getPlagiari log.debug("REST request to get the latest plagiarism result for the modeling exercise with id: {}", exerciseId); ModelingExercise modelingExercise = modelingExerciseRepository.findByIdWithStudentParticipationsSubmissionsResultsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, modelingExercise, null); - var plagiarismResult = (ModelingPlagiarismResult) plagiarismResultRepository.findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(modelingExercise.getId()); + var plagiarismResult = (ModelingPlagiarismResult) plagiarismResultRepository + .findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(modelingExercise.getId()); plagiarismResultRepository.prepareResultForClient(plagiarismResult); return buildPlagiarismResultResponse(plagiarismResult); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java index 3e8b82b2d8ed..c12a2a4b56b8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java @@ -973,7 +973,7 @@ private StudentParticipation participationForQuizWithResult(QuizExercise quizExe participation.setResults(new HashSet<>()); // add the appropriate result - Result result = resultRepository.findFirstByParticipationIdAndRatedOrderByCompletionDateDesc(participation.getId(), true).orElse(null); + Result result = resultRepository.findFirstByParticipationIdAndRatedWithSubmissionOrderByCompletionDateDesc(participation.getId(), true).orElse(null); if (result != null) { // find the submitted answers (they are NOT loaded eagerly anymore) var quizSubmission = (QuizSubmission) result.getSubmission(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index c06e0373b98c..853e39e8c861 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -563,7 +563,7 @@ public ResponseEntity> getPlagiarismRe log.debug("REST request to get the latest plagiarism result for the text exercise with id: {}", exerciseId); TextExercise textExercise = textExerciseRepository.findByIdWithStudentParticipationsAndSubmissionsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, textExercise, null); - var plagiarismResult = (TextPlagiarismResult) plagiarismResultRepository.findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); + var plagiarismResult = (TextPlagiarismResult) plagiarismResultRepository.findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); plagiarismResultRepository.prepareResultForClient(plagiarismResult); return buildPlagiarismResultResponse(plagiarismResult); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java index 1f5a472f451e..710b77697490 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java @@ -230,7 +230,7 @@ public ResponseEntity> getResponsibleUsersForCodeOfCond var course = courseRepository.findByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, requestingUser); - var responsibleUsers = userRepository.searchAllByLoginOrNameInGroups(Pageable.unpaged(), "", Set.of(course.getInstructorGroupName())) + var responsibleUsers = userRepository.searchAllWithGroupsByLoginOrNameInGroups(Pageable.unpaged(), "", Set.of(course.getInstructorGroupName())) .map((user) -> new ResponsibleUserDTO(user.getName(), user.getEmail())).toList(); return ResponseEntity.ok(responsibleUsers); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExercisePlagiarismResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExercisePlagiarismResource.java index ff9a7007cc3f..b1e3041f854e 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExercisePlagiarismResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExercisePlagiarismResource.java @@ -77,7 +77,7 @@ public ResponseEntity> getPlagiarismRe log.debug("REST request to get the latest plagiarism result for the programming exercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - var plagiarismResult = (TextPlagiarismResult) plagiarismResultRepository.findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(programmingExercise.getId()); + var plagiarismResult = (TextPlagiarismResult) plagiarismResultRepository.findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(programmingExercise.getId()); plagiarismResultRepository.prepareResultForClient(plagiarismResult); return buildPlagiarismResultResponse(plagiarismResult); } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index de2add43bcdf..b216a43a2bf6 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -163,7 +163,7 @@ spring: hibernate.generate_statistics: false hibernate.order_inserts: true hibernate.order_updates: true -# hibernate.query.fail_on_pagination_over_collection_fetch: true # not appropriate in our case: https://vladmihalcea.com/hibernate-query-fail-on-pagination-over-collection-fetch/ + hibernate.query.fail_on_pagination_over_collection_fetch: true # prevents issues, see https://vladmihalcea.com/hibernate-query-fail-on-pagination-over-collection-fetch hibernate.query.in_clause_parameter_padding: true hibernate.cache.use_second_level_cache: true hibernate.cache.use_query_cache: false diff --git a/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java index af2946c821aa..8030437f115f 100644 --- a/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java @@ -138,7 +138,7 @@ void getAllAuditEventsByDate() throws Exception { var auditEvents = request.getList("/api/admin/audits?fromDate=" + pastDate + "&toDate=" + currentDate, HttpStatus.OK, PersistentAuditEvent.class); assertThat(auditEvents).hasSize(1); var auditEvent = auditEvents.getFirst(); - var auditEventsInDb = persistenceAuditEventRepository.findAllByAuditEventDateBetween(Instant.now().minus(2, ChronoUnit.DAYS), Instant.now(), Pageable.unpaged()); + var auditEventsInDb = persistenceAuditEventRepository.findAllWithDataByAuditEventDateBetween(Instant.now().minus(2, ChronoUnit.DAYS), Instant.now(), Pageable.unpaged()); assertThat(auditEventsInDb.getTotalElements()).isEqualTo(1); assertThat(auditEvent.getPrincipal()).isEqualTo(auditEventsInDb.get().findFirst().orElseThrow().getPrincipal()); } diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java index 79ec3ac7bace..b11bae2a8cc5 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java @@ -345,7 +345,7 @@ void onNewResultNoResultForUser() { doReturn(course).when(courseRepository).findByIdWithEagerOnlineCourseConfigurationElseThrow(course.getId()); doReturn(clientRegistration).when(onlineCourseConfigurationService).getClientRegistration(any()); doReturn(Collections.singletonList(launch)).when(launchRepository).findByUserAndExercise(user, exercise); - doReturn(Optional.empty()).when(resultRepository).findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); + doReturn(Optional.empty()).when(resultRepository).findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); doReturn(Optional.of(ltiPlatformConfiguration)).when(ltiPlatformConfigurationRepository).findByRegistrationId(any()); lti13Service.onNewResult(participation); @@ -378,7 +378,7 @@ void onNewResultNoScoreUrl() { doReturn(course).when(courseRepository).findByIdWithEagerOnlineCourseConfigurationElseThrow(course.getId()); doReturn(clientRegistration).when(onlineCourseConfigurationService).getClientRegistration(any()); doReturn(Collections.singletonList(launch)).when(launchRepository).findByUserAndExercise(user, exercise); - doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); + doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); doReturn(Optional.of(ltiPlatformConfiguration)).when(ltiPlatformConfigurationRepository).findByRegistrationId(any()); lti13Service.onNewResult(participation); @@ -407,7 +407,7 @@ void onNewResultTokenFails() { doReturn(course).when(courseRepository).findByIdWithEagerOnlineCourseConfigurationElseThrow(course.getId()); doReturn(clientRegistration).when(onlineCourseConfigurationService).getClientRegistration(any()); doReturn(Collections.singletonList(launch)).when(launchRepository).findByUserAndExercise(user, exercise); - doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); + doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); doReturn(null).when(tokenRetriever).getToken(eq(clientRegistration), eq(Scopes.AGS_SCORE)); doReturn(Optional.of(ltiPlatformConfiguration)).when(ltiPlatformConfigurationRepository).findByRegistrationId(any()); @@ -441,7 +441,7 @@ void onNewResult() throws JsonProcessingException { ClientRegistration clientRegistration = state.clientRegistration(); doReturn(Collections.singletonList(launch)).when(launchRepository).findByUserAndExercise(user, exercise); - doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); + doReturn(Optional.of(result)).when(resultRepository).findFirstWithSubmissionAndFeedbacksAndTestCasesByParticipationIdOrderByCompletionDateDesc(participation.getId()); doReturn(course).when(courseRepository).findByIdWithEagerOnlineCourseConfigurationElseThrow(course.getId()); doReturn(Optional.of(ltiPlatformConfiguration)).when(ltiPlatformConfigurationRepository).findByRegistrationId(clientRegistrationId); doReturn(clientRegistration).when(onlineCourseConfigurationService).getClientRegistration(any()); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java index 4ff4a207b975..feccb1c19975 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java @@ -1305,7 +1305,7 @@ void shouldCalculateCorrectStatistics() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldGetCorrectLatestAutomaticResults() { createTestParticipationsWithResults(); - var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksForExercise(programmingExerciseSCAEnabled.getId()); + var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExercise(programmingExerciseSCAEnabled.getId()); assertThat(results).hasSize(5); } @@ -1314,7 +1314,7 @@ void shouldGetCorrectLatestAutomaticResults() { void shouldGetCorrectLatestAutomaticResultsWithMultipleResults() { createTestParticipationsWithMultipleResults(); // this method is tested. It should probably be improved as there is an inner query - var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksForExercise(programmingExerciseSCAEnabled.getId()); + var results = resultRepository.findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExercise(programmingExerciseSCAEnabled.getId()); var allResults = resultRepository.findAllByParticipationExerciseId(programmingExerciseSCAEnabled.getId()); assertThat(results).hasSize(5); assertThat(allResults).hasSize(6); diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java index cb3f7f76089e..24264d37eb52 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java @@ -98,7 +98,7 @@ void shouldCreateFullTestwiseCoverageReport() { var fileReportsByTestName = TestwiseCoverageTestUtil.generateCoverageFileReportByTestName(); testwiseCoverageService.createTestwiseCoverageReport(fileReportsByTestName, programmingExercise, solutionSubmission); - var reports = coverageReportRepository.getLatestCoverageReportsForLegalSubmissionsForProgrammingExercise(programmingExercise.getId(), Pageable.ofSize(1)); + var reports = coverageReportRepository.getLatestCoverageReportsWithLegalSubmissionsForProgrammingExercise(programmingExercise.getId(), Pageable.ofSize(1)); assertThat(reports).hasSize(1); var report = reports.getFirst(); // 18/50 lines covered = 32% diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java index 6f78e3358527..e8e4e6d05f23 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java @@ -414,7 +414,7 @@ void testPush_studentAssignmentRepository_tooManySubmissions() throws Exception localVCLocalCITestService.mockTestResults(dockerClient, PARTLY_SUCCESSFUL_TEST_RESULTS_PATH, LOCALCI_WORKING_DIRECTORY + LOCALCI_RESULTS_DIRECTORY); localVCLocalCITestService.testPushSuccessful(assignmentRepository.localGit, student1Login, projectKey1, assignmentRepositorySlug); - await().until(() -> resultRepository.findFirstByParticipationIdOrderByCompletionDateDesc(participation.getId()).isPresent()); + await().until(() -> resultRepository.findFirstWithSubmissionsByParticipationIdOrderByCompletionDateDesc(participation.getId()).isPresent()); // Second push should fail. localVCLocalCITestService.testPushReturnsError(assignmentRepository.localGit, student1Login, projectKey1, assignmentRepositorySlug, FORBIDDEN); diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCITestService.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCITestService.java index c555bc27c0ff..aaabc2bf53ff 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCITestService.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCITestService.java @@ -553,7 +553,7 @@ public void testLatestSubmission(Long participationId, String expectedCommitHash int expectedCodeIssueCount, Integer timeoutInSeconds) { // wait for result to be persisted Duration timeoutDuration = timeoutInSeconds != null ? Duration.ofSeconds(timeoutInSeconds) : Duration.ofSeconds(DEFAULT_AWAITILITY_TIMEOUT_IN_SECONDS); - await().atMost(timeoutDuration).until(() -> resultRepository.findFirstByParticipationIdOrderByCompletionDateDesc(participationId).isPresent()); + await().atMost(timeoutDuration).until(() -> resultRepository.findFirstWithSubmissionsByParticipationIdOrderByCompletionDateDesc(participationId).isPresent()); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); List submissions = programmingSubmissionRepository.findAllByParticipationIdWithResults(participationId); diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java index 7fbfb778189d..ba884947d781 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java @@ -219,7 +219,7 @@ void testDeletePlagiarismComparisons_editor() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testDeletePlagiarismComparisons_instructor() throws Exception { request.delete("/api/exercises/" + textExercise.getId() + "/plagiarism-results/" + textPlagiarismResult.getId() + "/plagiarism-comparisons?deleteAll=false", HttpStatus.OK); - var result = plagiarismResultRepository.findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); + var result = plagiarismResultRepository.findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); assertThat(result).isNotNull(); assertThat(result.getComparisons()).hasSize(1); } @@ -228,7 +228,7 @@ void testDeletePlagiarismComparisons_instructor() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testDeletePlagiarismComparisons_instructor_deleteAll() throws Exception { request.delete("/api/exercises/" + textExercise.getId() + "/plagiarism-results/" + textPlagiarismResult.getId() + "/plagiarism-comparisons?deleteAll=true", HttpStatus.OK); - var result = plagiarismResultRepository.findFirstByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); + var result = plagiarismResultRepository.findFirstWithComparisonsByExerciseIdOrderByLastModifiedDateDescOrNull(textExercise.getId()); assertThat(result).isNull(); } diff --git a/src/test/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionTestRepository.java b/src/test/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionTestRepository.java index 1e6a2c180109..7567788bd4b8 100644 --- a/src/test/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionTestRepository.java +++ b/src/test/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionTestRepository.java @@ -38,15 +38,35 @@ public interface ProgrammingSubmissionTestRepository extends ArtemisJpaRepositor * @return the latest legal submission for the participation */ default Optional findFirstByParticipationIdWithResultsOrderByLegalSubmissionDateDesc(Long participationId) { - return findFirstByTypeNotAndTypeNotNullAndParticipationIdAndResultsNotNullOrderBySubmissionDateDesc(SubmissionType.ILLEGAL, participationId); + return findFirstWithResultsByTypeNotAndTypeNotNullAndParticipationIdAndResultsNotNullOrderBySubmissionDateDesc(SubmissionType.ILLEGAL, participationId); } - @EntityGraph(type = LOAD, attributePaths = "results") Optional findFirstByTypeNotAndTypeNotNullAndParticipationIdAndResultsNotNullOrderBySubmissionDateDesc(SubmissionType type, Long participationId); @EntityGraph(type = LOAD, attributePaths = "results") + Optional findProgrammingSubmissionById(long programmingSubmissionId); + + default Optional findFirstWithResultsByTypeNotAndTypeNotNullAndParticipationIdAndResultsNotNullOrderBySubmissionDateDesc(SubmissionType type, + Long participationId) { + var programmingSubmissionOptional = findFirstByTypeNotAndTypeNotNullAndParticipationIdAndResultsNotNullOrderBySubmissionDateDesc(type, participationId); + if (programmingSubmissionOptional.isEmpty()) { + return Optional.empty(); + } + var id = programmingSubmissionOptional.get().getId(); + return findProgrammingSubmissionById(id); + } + Optional findFirstByParticipationIdOrderBySubmissionDateDesc(Long participationId); + default Optional findFirstWithResultsByParticipationIdOrderBySubmissionDateDesc(Long participationId) { + var programmingSubmissionOptional = findFirstByParticipationIdOrderBySubmissionDateDesc(participationId); + if (programmingSubmissionOptional.isEmpty()) { + return Optional.empty(); + } + var id = programmingSubmissionOptional.get().getId(); + return findProgrammingSubmissionById(id); + } + @EntityGraph(type = LOAD, attributePaths = { "buildLogEntries" }) Optional findWithEagerBuildLogEntriesById(Long submissionId); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 5ffb1e4ff93a..bba3939e92f3 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -109,7 +109,7 @@ spring: hibernate.generate_statistics: false hibernate.order_inserts: true hibernate.order_updates: true -# hibernate.query.fail_on_pagination_over_collection_fetch: true # not appropriate in our case: https://vladmihalcea.com/hibernate-query-fail-on-pagination-over-collection-fetch/ + hibernate.query.fail_on_pagination_over_collection_fetch: true # prevents issues, see https://vladmihalcea.com/hibernate-query-fail-on-pagination-over-collection-fetch hibernate.query.in_clause_parameter_padding: true hibernate.cache.use_second_level_cache: true hibernate.cache.use_query_cache: false