DESC被传递到了Hibernate查询中,但顺序仍然是升序。为什么?

huangapple go评论99阅读模式
英文:

DESC is passed to a Hibernate query, but the order is still ascending. Why?

问题

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.

  1. @Test
  2. @Sql(executionPhase = BEFORE_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetPageTest/before.sql")
  3. @Sql(executionPhase = AFTER_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetPageTest/after.sql") // truncating
  4. public void getPageOneSizeFiveSortByIdDescTest() throws Exception {
  5. token = testUtil.getToken(testUsername, testPassword);
  6. MockHttpServletResponse response = mockMvc.perform(get(BASE_URI + "page/" + 1)
  7. .header(HttpHeaders.AUTHORIZATION, token)
  8. .param("pageSize", "5")
  9. .param("sortType", "ID_DESC"))
  10. .andExpect(status().isOk())
  11. .andReturn()
  12. .getResponse();
  13. // asserts omitted
  1. <!-- the before script -->
  2. TRUNCATE TABLE permissions CASCADE;
  3. <!-- the same for accounts, questions, tags, question_comments -->
  4. INSERT INTO permissions(id, name, created_date, modified_date)
  5. VALUES (1, 'ADMIN', current_timestamp, current_timestamp),
  6. (2, 'MODERATOR', current_timestamp, current_timestamp),
  7. (3, 'USER', current_timestamp, current_timestamp);
  8. INSERT INTO accounts(id, username, password, enabled, created_date, modified_date)
  9. VALUES (1, 'mickey_m', '$2y$10$ZdqfOo1vwdHJJGnkmGrw/OUelZcU9ZfRFaX/RMN3XniXH96eTkB1e', true, current_timestamp,
  10. current_timestamp),
  11. (2, 'minerva_m', '$2y$10$PiZxGGi904rCLdTSGY1ycuQhcEtQrP1u74KvQ2IEuk5Jh18Ml.6xO', true, current_timestamp,
  12. current_timestamp);
  13. INSERT INTO accounts_permissions(account_id, permission_id)
  14. VALUES (1, 3),
  15. (2, 3);
  16. INSERT INTO questions(id, created_date, modified_date, title, description, account_id)
  17. VALUES (1, current_timestamp, current_timestamp, 'title', 'description', 2);
  18. INSERT INTO tags(id, created_date, modified_date, name)
  19. VALUES (1, current_timestamp, current_timestamp, 'tag1'),
  20. (2, current_timestamp, current_timestamp, 'tag2'),
  21. (3, current_timestamp, current_timestamp, 'tag3');
  22. INSERT INTO questions_tags(question_id, tag_id)
  23. VALUES (1, 1), (1, 2), (1, 3);
  24. INSERT INTO question_comments(id, created_date, modified_date, text, account_id, question_id)
  25. VALUES (1, current_timestamp, current_timestamp, 'text', 1, 1),
  26. <!-- the middle rows are omitted -->
  27. (10, current_timestamp, current_timestamp, 'text', 1, 1);
  1. @Getter
  2. public class QuestionCommentResponseDto {
  3. private final Long id;
  4. private final Long questionId;
  5. private final LocalDateTime createdDate;
  6. private final LocalDateTime modifiedDate;
  7. private final String text;
  8. @Setter
  9. private AccountResponseDto owner;
  10. // constructors, equals(), hashcode()
  1. @AllArgsConstructor
  2. @NoArgsConstructor
  3. @Getter
  4. @Setter
  5. public class AccountResponseDto {
  6. private Long id;
  7. private String username;
  8. // equals(), hashcode()
  1. // the relevant controller method
  2. @GetMapping("/page/{pageNumber}")
  3. public ResponseEntity<Data<Page<QuestionCommentResponseDto>>> getPage(@PathVariable @Positive @NotNull Integer pageNumber,
  4. @RequestParam(defaultValue = "20") @NotNull Integer pageSize,
  5. @RequestParam(defaultValue = "ID_ASC") @NotNull SortType sortType) {
  6. PaginationParameters params = PaginationParameters.ofPageNumberSizeAndSortType(pageNumber, pageSize, sortType);
  7. // ↑ PaginationParameters is a simple record class defined as PaginationParameters(Integer pageNumber, Integer size, SortType sortType)
  8. Page<QuestionCommentResponseDto> page = dtoService.getPage(params);
  9. Data<Page<QuestionCommentResponseDto>> responseData = Data.build(page); // a two-bit wrapper
  10. return ResponseEntity.ok(responseData);
  11. }
  1. @RequiredArgsConstructor
  2. @Getter
  3. public enum SortType {
  4. ID_ASC(" id "),
  5. ID_DESC(" id DESC "),
  6. // some extra constants
  7. private String query;
  8. }
  1. // a basic utility class
  2. public class PaginationParametersProcessor {
  3. public static String extractSortingModifier(PaginationParameters params) {
  4. return params.sortType().getQuery();
  5. }
  6. public static int extractFirstResultIndex(PaginationParameters params) {
  7. return (params.pageNumber() - 1) * params.size();
  8. }
  9. public static int extractMaxResults(PaginationParameters params) {
  10. return params.size();
  11. }
  12. }

dtoService.getPage(params) eventually comes down to this

  1. @Override
  2. public List<QuestionCommentResponseDto> getDtosWithoutSetOwner(PaginationParameters params) {
  3. String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params); // assigns " id DESC "
  4. int offset = PaginationParametersProcessor.extractFirstResultIndex(params);
  5. int limit = PaginationParametersProcessor.extractMaxResults(params);
  6. return entityManager.createQuery("""
  7. SELECT new stack.overflow.model.dto.response.QuestionCommentResponseDto (
  8. qc.id, q.id, qc.createdDate, qc.modifiedDate, qc.text
  9. ) FROM QuestionComment qc JOIN qc.question q
  10. ORDER BY :sort
  11. """, QuestionCommentResponseDto.class)
  12. .setParameter("sort", sortingModifier)
  13. .setFirstResult(offset)
  14. .setMaxResults(limit)
  15. .getResultList();
  16. }

The :sort parameter is successfully passed into the query executed by Hibernate. Here's a snippet from my console output

  1. 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 ?
  2. 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

  1. 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
  2. <details>
  3. <summary>英文:</summary>
  4. I have a RESTful app. Here&#39;s a test method from it. It is intended to retrieve a page of `QuestionCommentResponseDto` objects in *descending* order
  5. ```java
  6. @Test
  7. @Sql(executionPhase = BEFORE_TEST_METHOD, value = BASE_SCRIPT_PATH + &quot;GetPageTest/before.sql&quot;)
  8. @Sql(executionPhase = AFTER_TEST_METHOD, value = BASE_SCRIPT_PATH + &quot;GetPageTest/after.sql&quot;) // truncating
  9. public void getPageOneSizeFiveSortByIdDescTest() throws Exception {
  10. token = testUtil.getToken(testUsername, testPassword);
  11. MockHttpServletResponse response = mockMvc.perform(get(BASE_URI + &quot;page/&quot; + 1)
  12. .header(HttpHeaders.AUTHORIZATION, token)
  13. .param(&quot;pageSize&quot;, &quot;5&quot;)
  14. .param(&quot;sortType&quot;, &quot;ID_DESC&quot;))
  15. .andExpect(status().isOk())
  16. .andReturn()
  17. .getResponse();
  18. // asserts omitted
  1. &lt;!-- the before script --&gt;
  2. TRUNCATE TABLE permissions CASCADE;
  3. &lt;!-- the same for accounts, questions, tags, question_comments --&gt;
  4. INSERT INTO permissions(id, name, created_date, modified_date)
  5. VALUES (1, &#39;ADMIN&#39;, current_timestamp, current_timestamp),
  6. (2, &#39;MODERATOR&#39;, current_timestamp, current_timestamp),
  7. (3, &#39;USER&#39;, current_timestamp, current_timestamp);
  8. INSERT INTO accounts(id, username, password, enabled, created_date, modified_date)
  9. VALUES (1, &#39;mickey_m&#39;, &#39;$2y$10$ZdqfOo1vwdHJJGnkmGrw/OUelZcU9ZfRFaX/RMN3XniXH96eTkB1e&#39;, true, current_timestamp,
  10. current_timestamp),
  11. (2, &#39;minerva_m&#39;, &#39;$2y$10$PiZxGGi904rCLdTSGY1ycuQhcEtQrP1u74KvQ2IEuk5Jh18Ml.6xO&#39;, true, current_timestamp,
  12. current_timestamp);
  13. INSERT INTO accounts_permissions(account_id, permission_id)
  14. VALUES (1, 3),
  15. (2, 3);
  16. INSERT INTO questions(id, created_date, modified_date, title, description, account_id)
  17. VALUES (1, current_timestamp, current_timestamp, &#39;title&#39;, &#39;description&#39;, 2);
  18. INSERT INTO tags(id, created_date, modified_date, name)
  19. VALUES (1, current_timestamp, current_timestamp, &#39;tag1&#39;),
  20. (2, current_timestamp, current_timestamp, &#39;tag2&#39;),
  21. (3, current_timestamp, current_timestamp, &#39;tag3&#39;);
  22. INSERT INTO questions_tags(question_id, tag_id)
  23. VALUES (1, 1), (1, 2), (1, 3);
  24. INSERT INTO question_comments(id, created_date, modified_date, text, account_id, question_id)
  25. VALUES (1, current_timestamp, current_timestamp, &#39;text&#39;, 1, 1),
  26. &lt;!-- the middle rows are omitted --&gt;
  27. (10, current_timestamp, current_timestamp, &#39;text&#39;, 1, 1);
  1. @Getter
  2. public class QuestionCommentResponseDto {
  3. private final Long id;
  4. private final Long questionId;
  5. private final LocalDateTime createdDate;
  6. private final LocalDateTime modifiedDate;
  7. private final String text;
  8. @Setter
  9. private AccountResponseDto owner;
  10. // constructors, equals(), hashcode()
  1. @AllArgsConstructor
  2. @NoArgsConstructor
  3. @Getter
  4. @Setter
  5. public class AccountResponseDto {
  6. private Long id;
  7. private String username;
  8. // equals(), hashcode()
  1. // the relevant controller method
  2. @GetMapping(&quot;/page/{pageNumber}&quot;)
  3. public ResponseEntity&lt;Data&lt;Page&lt;QuestionCommentResponseDto&gt;&gt;&gt; getPage(@PathVariable @Positive @NotNull Integer pageNumber,
  4. @RequestParam(defaultValue = &quot;20&quot;) @NotNull Integer pageSize,
  5. @RequestParam(defaultValue = &quot;ID_ASC&quot;) @NotNull SortType sortType) {
  6. PaginationParameters params = PaginationParameters.ofPageNumberSizeAndSortType(pageNumber, pageSize, sortType);
  7. // ↑ PaginationParameters is a simple record class defined as PaginationParameters(Integer pageNumber, Integer size, SortType sortType)
  8. Page&lt;QuestionCommentResponseDto&gt; page = dtoService.getPage(params);
  9. Data&lt;Page&lt;QuestionCommentResponseDto&gt;&gt; responseData = Data.build(page); // a two-bit wrapper
  10. return ResponseEntity.ok(responseData);
  11. }
  1. @RequiredArgsConstructor
  2. @Getter
  3. public enum SortType {
  4. ID_ASC(&quot; id &quot;),
  5. ID_DESC(&quot; id DESC &quot;),
  6. // some extra constants
  7. private String query;
  8. }
  1. // a basic utility class
  2. public class PaginationParametersProcessor {
  3. public static String extractSortingModifier(PaginationParameters params) {
  4. return params.sortType().getQuery();
  5. }
  6. public static int extractFirstResultIndex(PaginationParameters params) {
  7. return (params.pageNumber() - 1) * params.size();
  8. }
  9. public static int extractMaxResults(PaginationParameters params) {
  10. return params.size();
  11. }
  12. }

dtoService.getPage(params) eventually comes down to this

  1. @Override
  2. public List&lt;QuestionCommentResponseDto&gt; getDtosWithoutSetOwner(PaginationParameters params) {
  3. String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params); // assigns &quot; id DESC &quot;
  4. int offset = PaginationParametersProcessor.extractFirstResultIndex(params);
  5. int limit = PaginationParametersProcessor.extractMaxResults(params);
  6. return entityManager.createQuery(&quot;&quot;&quot;
  7. SELECT new stack.overflow.model.dto.response.QuestionCommentResponseDto (
  8. qc.id, q.id, qc.createdDate, qc.modifiedDate, qc.text
  9. ) FROM QuestionComment qc JOIN qc.question q
  10. ORDER BY :sort
  11. &quot;&quot;&quot;, QuestionCommentResponseDto.class)
  12. .setParameter(&quot;sort&quot;, sortingModifier)
  13. .setFirstResult(offset)
  14. .setMaxResults(limit)
  15. .getResultList();
  16. }

The :sort parameter is successfully passed into the query executed by Hibernate. Here's a snippet from my console output

  1. 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 ?
  2. 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

  1. Body = {&quot;data&quot;:{&quot;dtos&quot;:[{&quot;id&quot;:1,&quot;questionId&quot;:1,&quot;createdDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;modifiedDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;text&quot;:&quot;text&quot;,&quot;owner&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;mickey_m&quot;}},{&quot;id&quot;:2,&quot;questionId&quot;:1,&quot;createdDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;modifiedDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;text&quot;:&quot;text&quot;,&quot;owner&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;mickey_m&quot;}},{&quot;id&quot;:3,&quot;questionId&quot;:1,&quot;createdDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;modifiedDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;text&quot;:&quot;text&quot;,&quot;owner&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;mickey_m&quot;}},{&quot;id&quot;:4,&quot;questionId&quot;:1,&quot;createdDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;modifiedDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;text&quot;:&quot;text&quot;,&quot;owner&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;mickey_m&quot;}},{&quot;id&quot;:5,&quot;questionId&quot;:1,&quot;createdDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;modifiedDate&quot;:&quot;2023-06-04T16:46:52.942835&quot;,&quot;text&quot;:&quot;text&quot;,&quot;owner&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;mickey_m&quot;}}],&quot;count&quot;: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

UPD: Ok, so setParameter() encloses the passed value in parentheses thereby precluding any query modification (which makes sense considering the peril of SQL injections). What should I do then? Regular concatenation would make my code less pretty, don't you think?

  1. @Override
  2. public List&lt;QuestionCommentResponseDto&gt; getDtosWithoutSetOwner(PaginationParameters params) {
  3. String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params);
  4. int offset = PaginationParametersProcessor.extractFirstResultIndex(params);
  5. int limit = PaginationParametersProcessor.extractMaxResults(params);
  6. return entityManager.createQuery(&quot;&quot;&quot;
  7. SELECT new stack.overflow.model.dto.response.QuestionCommentResponseDto (
  8. qc.id, q.id, qc.createdDate, qc.modifiedDate, qc.text
  9. ) FROM QuestionComment qc JOIN qc.question q
  10. ORDER BY&quot;&quot;&quot; + sortingModifier, QuestionCommentResponseDto.class) // ← not nice
  11. .setFirstResult(offset)
  12. .setMaxResults(limit)
  13. .getResultList();
  14. }

What's worse, I now get error 500 in all "getPage" tests, including those that used to go smoothly

  1. Body = {&quot;error&quot;:&quot;could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet&quot;}

As I understand, it's because of the thing I mentioned above, id's ambiguity

  1. ERROR 7816 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: column reference &quot;id&quot; is ambiguous

Here's what I tried to resolve that ambiguity

  1. @Override
  2. public List&lt;QuestionCommentResponseDto&gt; getDtosWithoutSetOwner(PaginationParameters params) {
  3. String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params, QuestionComment.class);
  1. public class PaginationParametersProcessor {
  2. public static String extractSortingModifier(PaginationParameters params) {
  3. return params.sortType().getQuery();
  4. }
  5. public static String extractSortingModifier(PaginationParameters params,
  6. Class&lt;?&gt; entity) {
  7. String nonadaptedQuery = extractSortingModifier(params);
  8. String entityTableName = extractEntityTableName(entity);
  9. String adaptedQuery = nonadaptedQuery.replaceFirst(&quot;(?=\\w)&quot;, entityTableName + &quot;.&quot;);
  10. return adaptedQuery;
  11. }
  12. private static String extractEntityTableName(Class&lt;?&gt; entity) {
  13. if (entity.isAnnotationPresent(Entity.class)) {
  14. return entity.isAnnotationPresent(Table.class) ?
  15. entity.getAnnotation(Table.class).name() :
  16. entity.getSimpleName();
  17. } else {
  18. throw new IllegalArgumentException(String.format(&quot;%s is not an @Entity&quot;, entity.getName()));
  19. }
  20. }
  1. @AllArgsConstructor
  2. @NoArgsConstructor
  3. @Getter
  4. @Setter
  5. @Entity
  6. @EntityListeners(AuditingEntityListener.class)
  7. @Table(name = &quot;question_comments&quot;) // ← see?
  8. public class QuestionComment {

Result:

  1. ERROR 4440 --- [ main] o.h.hql.internal.ast.ErrorTracker : Invalid path: &#39;question_comments.id&#39;
  2. org.hibernate.hql.internal.ast.InvalidPathException: Invalid path: &#39;question_comments.id&#39;

I don't get it. Why is it invalid? I tried this too (after all, JPA operates with @Entities, not tables)

  1. private static String extractEntityTableName(Class&lt;?&gt; entity) {
  2. if (entity.isAnnotationPresent(Entity.class)) {
  3. return entity.getSimpleName();
  4. } else {
  5. // you know the rest

Result:

  1. Body = {&quot;error&quot;:&quot;Cannot read field \&quot;value\&quot; because \&quot;s1\&quot; is null&quot;}

I'm at my wits' end!

UPD2: This kind of works but is not very pretty

  1. @Override
  2. public List&lt;QuestionCommentResponseDto&gt; getDtosWithoutSetOwner(PaginationParameters params) {
  3. String sortingModifier = PaginationParametersProcessor.extractSortingModifier(params).trim();
  4. int offset = PaginationParametersProcessor.extractFirstResultIndex(params);
  5. int limit = PaginationParametersProcessor.extractMaxResults(params);
  6. return entityManager.createQuery(&quot;&quot;&quot;
  7. SELECT new stack.overflow.model.dto.response.QuestionCommentResponseDto (
  8. qc.id, q.id, qc.createdDate, qc.modifiedDate, qc.text
  9. ) FROM QuestionComment qc JOIN qc.question q
  10. ORDER BY qc.&quot;&quot;&quot; + sortingModifier, QuestionCommentResponseDto.class)
  11. .setFirstResult(offset)
  12. .setMaxResults(limit)
  13. .getResultList();
  14. }

But I still don't understand why neither of the previous two options worked

答案1

得分: 1

The :sort parameter is successfully passed into the query executed by Hibernate.

that is actually not true. The resulting query effectively looks like:

  1. Hibernate: select questionco0_.id as col_0_0_,
  2. question1_.id as col_1_0_,
  3. questionco0_.created_date as col_2_0_,
  4. questionco0_.modified_date as col_3_0_,
  5. questionco0_.text as col_4_0_
  6. from question_comments questionco0_
  7. inner join questions question1_ on
  8. questionco0_.question_id=question1_.id
  9. order by "id DESC" limit ?

That is clearly stated in HBN debug message:

  1. binding parameter [1] as [VARCHAR] - [ id DESC ]

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.

英文:

> The :sort parameter is successfully passed into the query executed by Hibernate.

that is actually not true. The resulting query effectively looks like:

  1. Hibernate: select questionco0_.id as col_0_0_,
  2. question1_.id as col_1_0_,
  3. questionco0_.created_date as col_2_0_,
  4. questionco0_.modified_date as col_3_0_,
  5. questionco0_.text as col_4_0_
  6. from question_comments questionco0_
  7. inner join questions question1_ on
  8. questionco0_.question_id=question1_.id
  9. order by &quot;id DESC&quot; limit ?

That is clearly stated in HBN debug message:

  1. binding parameter [1] as [VARCHAR] - [ id DESC ]

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.

答案2

得分: 1

只看这里的我的答案。你不能通过PreparedStatement参数来改变执行计划,将asc改成desc或反之会改变执行计划。所以无论是SQL还是Hibernate都不会这样做(如你所期望的那样)。

所以当你将它作为PreparedStatement参数传递时,实际上传递的是一个常量,因此你的DBMS只会运行以下查询:

  1. select ....
  2. order by 'id desc'

这是一个常量!

英文:

Just look at my answer here. You cannot change execution plan with PreparedStatement parameters, changing asc to desc 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:

  1. select ....
  2. order by &#39;id desc&#39;

which is a constant!

huangapple
  • 本文由 发表于 2023年6月4日 23:03:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76401034.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定