I have a RESTful app. Here’s a test method from it. It is intended to retrieve a page of QuestionCommentResponseDto
objects in descending order
@Test
@Sql(executionPhase = BEFORE_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetPageTest/before.sql")
@Sql(executionPhase = AFTER_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetPageTest/after.sql") // truncating
public void getPageOneSizeFiveSortByIdDescTest() throws Exception {
token = testUtil.getToken(testUsername, testPassword);
MockHttpServletResponse response = mockMvc.perform(get(BASE_URI + "page/" + 1)
.header(HttpHeaders.AUTHORIZATION, token)
.param("pageSize", "5")
.param("sortType", "ID_DESC"))
.andExpect(status().isOk())
.andReturn()
.getResponse();
// asserts omitted
<!-- the before script -->
TRUNCATE TABLE permissions CASCADE;
<!-- the same for accounts, questions, tags, question_comments -->
INSERT INTO permissions(id, name, created_date, modified_date)
VALUES (1, 'ADMIN', current_timestamp, current_timestamp),
(2, 'MODERATOR', current_timestamp, current_timestamp),
(3, 'USER', current_timestamp, current_timestamp);
INSERT INTO accounts(id, username, password, enabled, created_date, modified_date)
VALUES (1, 'mickey_m', '$2y$10$ZdqfOo1vwdHJJGnkmGrw/OUelZcU9ZfRFaX/RMN3XniXH96eTkB1e', true, current_timestamp,
current_timestamp),
(2, 'minerva_m', '$2y$10$PiZxGGi904rCLdTSGY1ycuQhcEtQrP1u74KvQ2IEuk5Jh18Ml.6xO', true, current_timestamp,
current_timestamp);
INSERT INTO accounts_permissions(account_id, permission_id)
VALUES (1, 3),
(2, 3);
INSERT INTO questions(id, created_date, modified_date, title, description, account_id)
VALUES (1, current_timestamp, current_timestamp, 'title', 'description', 2);
INSERT INTO tags(id, created_date, modified_date, name)
VALUES (1, current_timestamp, current_timestamp, 'tag1'),
(2, current_timestamp, current_timestamp, 'tag2'),
(3, current_timestamp, current_timestamp, 'tag3');
INSERT INTO questions_tags(question_id, tag_id)
VALUES (1, 1), (1, 2), (1, 3);
INSERT INTO question_comments(id, created_date, modified_date, text, account_id, question_id)
VALUES (1, current_timestamp, current_timestamp, 'text', 1, 1),
<!-- the middle rows are omitted -->
(10, current_timestamp, current_timestamp, 'text', 1, 1);
@Getter
public class QuestionCommentResponseDto {
private final Long id;
private final Long questionId;
private final LocalDateTime createdDate;
private final LocalDateTime modifiedDate;
private final String text;
@Setter
private AccountResponseDto owner;
// constructors, equals(), hashcode()
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AccountResponseDto {
private Long id;
private String username;
// equals(), hashcode()
// the relevant controller method
@GetMapping("/page/{pageNumber}")
public ResponseEntity<Data<Page<QuestionCommentResponseDto>>> getPage(@PathVariable @Positive @NotNull Integer pageNumber,
@RequestParam(defaultValue = "20") @NotNull Integer pageSize,
@RequestParam(defaultValue = "ID_ASC") @NotNull SortType sortType) {
PaginationParameters params = PaginationParameters.ofPageNumberSizeAndSortType(pageNumber, pageSize, sortType);
// ↑ PaginationParameters is a simple record class defined as PaginationParameters(Integer pageNumber, Integer size, SortType sortType)
Page<QuestionCommentResponseDto> page = dtoService.getPage(params);
Data<Page<QuestionCommentResponseDto>> responseData = Data.build(page); // a two-bit wrapper
return ResponseEntity.ok(responseData);
}
@RequiredArgsConstructor
@Getter
public enum SortType {
ID_ASC(" id "),
ID_DESC(" id DESC "),
// some extra constants
private String query;
}
// a basic utility class
public class PaginationParametersProcessor {
public static String extractSortingModifier(PaginationParameters params) {
return params.sortType().getQuery();
}
public static int extractFirstResultIndex(PaginationParameters params) {
return (params.pageNumber() - 1) * params.size();
}
public static int extractMaxResults(PaginationParameters params) {
return params.size();
}
}
dtoService.getPage(params)
eventually comes down to this
@Override
public List<QuestionCommentResponseDto> getDtosWithoutSetOwner(PaginationParameters params) {
String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params); // assigns " id DESC "
int offset = PaginationParametersProcessor.extractFirstResultIndex(params);
int limit = PaginationParametersProcessor.extractMaxResults(params);
return entityManager.createQuery("""
SELECT new stack.overflow.model.dto.response.QuestionCommentResponseDto (
qc.id, q.id, qc.createdDate, qc.modifiedDate, qc.text
) FROM QuestionComment qc JOIN qc.question q
ORDER BY :sort
""", QuestionCommentResponseDto.class)
.setParameter("sort", sortingModifier)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
The :sort
parameter is successfully passed into the query executed by Hibernate. Here’s a snippet from my console output
Hibernate: select questionco0_.id as col_0_0_, question1_.id as col_1_0_, questionco0_.created_date as col_2_0_, questionco0_.modified_date as col_3_0_, questionco0_.text as col_4_0_ from question_comments questionco0_ inner join questions question1_ on questionco0_.question_id=question1_.id order by ? limit ?
2023-06-04 16:46:53.164 TRACE 3172 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [ id DESC ]
But the ids in the resulting list are in an ascending, not descending order
Body = {"data":{"dtos":[{"id":1,"questionId":1,"createdDate":"2023-06-04T16:46:52.942835","modifiedDate":"2023-06-04T16:46:52.942835","text":"text","owner":{"id":1,"username":"mickey_m"}},{"id":2,"questionId":1,"createdDate":"2023-06-04T16:46:52.942835","modifiedDate":"2023-06-04T16:46:52.942835","text":"text","owner":{"id":1,"username":"mickey_m"}},{"id":3,"questionId":1,"createdDate":"2023-06-04T16:46:52.942835","modifiedDate":"2023-06-04T16:46:52.942835","text":"text","owner":{"id":1,"username":"mickey_m"}},{"id":4,"questionId":1,"createdDate":"2023-06-04T16:46:52.942835","modifiedDate":"2023-06-04T16:46:52.942835","text":"text","owner":{"id":1,"username":"mickey_m"}},{"id":5,"questionId":1,"createdDate":"2023-06-04T16:46:52.942835","modifiedDate":"2023-06-04T16:46:52.942835","text":"text","owner":{"id":1,"username":"mickey_m"}}],"count":10}}
Why is that and how do I fix it? Is it because Postgres can’t figure out which id
I mean (QuestionComment
‘s or Question
‘s)? Suggestions?
In case you need to look at the entire project, here’s the repo
2
Answers
that is actually not true. The resulting query effectively looks like:
That is clearly stated in HBN debug message:
Basically, you are sorting by a constant, but not by a column. Anticipating a follow-up Q about how to parameterise column name in
order by
clause: you can’t, you can parameterise constants, but not columns.Just look at my answer here. You cannot change execution plan with
PreparedStatement
parameters, changingasc
todesc
or vice-versa changes the execution plan. So neither SQL nor Hibernate won’t do that (as you except).So when you are passing that as
PreparedStatement
parameter, you are passing a constant, so your DBMS just runs the query as:which is a constant!