- Я использую Spring Boot 2.7.18 с Java 17 и maven.
- Пожалуйста, относитесь к этому исключительно в образовательных целях ( не кричите на меня
что фильтрация и обновление должны работать не так, я это знаю). - Я создал этот простой пример после прочтения статьи о
'лучший способ обработки исключения MultipleBagFetchException в спящем режиме' и наткнулся
на проблему с правильным удалением потерянных объектов, отсюда и вопрос.
Код: Выделить всё
@Entity
@Getter
@Setter
@Table(name = "post")
@SequenceGenerator(name = "post_seq", sequenceName = "post_id_seq", allocationSize = 1)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_seq")
Long id;
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
List comments;
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(fetch = FetchType.EAGER, mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
List metadata;
@Enumerated(EnumType.STRING)
PostStatus status;
}
Код: Выделить всё
@Entity
@Getter
@Setter
@Table(name = "metadata")
@SequenceGenerator(name = "post_metadata_seq", sequenceName = "post_metadata_id_seq", allocationSize = 1)
public class Metadata {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_metadata_seq")
Long id;
@ManyToOne
@JoinColumn(name = "post_id")
Post post;
@Column(name = "\"key\"")
String key;
@Column(name = "\"value\"")
String value;
}
Код: Выделить всё
@Entity
@Getter
@Setter
@Table(name = "comment")
@SequenceGenerator(name = "post_comment_seq", sequenceName = "post_comment_id_seq", allocationSize = 1)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_comment_seq")
Long id;
@ManyToOne
@JoinColumn(name = "post_id")
Post post;
String text;
}
Код: Выделить всё
@Builder(toBuilder = true)
@Getter
@Jacksonized
public class PostDTO {
Long id;
MetadataDTO metadata;
List comments;
PostStatus status;
}
Код: Выделить всё
@ToString
@Value(staticConstructor = "of")
public class MetadataDTO {
Map metadata;
@JsonCreator
@Builder(toBuilder = true)
public MetadataDTO(@JsonProperty("metadata") final Map metadata) {
this.metadata = Optional.ofNullable(metadata)
.map(HashMap::new)
.map(Collections::unmodifiableMap)
.orElse(Map.of());
}
}
Код: Выделить всё
@Service
@RequiredArgsConstructor
public class PostService {
public final PersistablePostMapper persistablePostMapper;
public final PostRepository postRepository;
public final EntityManager entityManager;
@Transactional
public PostDTO saveAll(final PostDTO postDTO) throws BadRequestException {
String referenceId = postDTO.getMetadata().getMetadata().get("reference_id");
List alreadyClosedPostIds = findAllByReferenceId(Long.valueOf(referenceId)).stream()
.filter(p -> PostStatus.CLOSED.equals(p.getStatus()))
.map(PostDTO::getId)
.toList();
if (alreadyClosedPostIds.contains(postDTO.getId())) {
throw new BadRequestException();
}
return save(postDTO);
}
public List
findAllByReferenceId(final Long referenceId) {
List posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.metadata m
where m.key=:key and m.value=:value""", Post.class)
.setParameter("key", "reference_id")
.setParameter("value", String.valueOf(referenceId))
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.comments l
where p in :posts"""
, Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
return posts.stream().map(persistablePostMapper::mapToPost).collect(Collectors.toList());
}
public PostDTO save(final PostDTO postDTO) {
Post persistablePost = persistablePostMapper.mapToPersistablePost(postDTO);
Post savedPersistablePost = postRepository.save(persistablePost);
return persistablePostMapper.mapToPost(savedPersistablePost);
}
}
Код: Выделить всё
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
@PostMapping
PostDTO createOrUpdatePosts(@RequestBody final PostDTO postDTO) throws BadRequestException {
return postService.saveAll(postDTO);
}
}
Код: Выделить всё
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles = {"test"})
@AutoConfigureMockMvc
class PostControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected PostService postService;
private static final TypeReference
POST_TYPE_REFERENCE = new TypeReference() {
};
@Test
void shouldUpdatePostComments() throws Exception {
//given
CommentDTO comment1 = CommentDTO.builder()
.text("test1")
.build();
CommentDTO comment2 = CommentDTO.builder()
.text("test2")
.build();
List commentsBeforeUpdate = List.of(comment1);
List commentsAfterUpdate = List.of(comment1, comment2);
PostDTO postWithOneComment = PostDTO.builder()
.status(PostStatus.OPEN)
.metadata(MetadataDTO.builder()
.metadata(Map.of(
"reference_id", "100",
"origin", "test"))
.build())
.comments(commentsBeforeUpdate)
.build();
PostDTO savedPost = postService.save(postWithOneComment);
List postBeforeUpdate = postService.findAllByReferenceId(100L);
//when
MockHttpServletResponse response = mockMvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(savedPost.toBuilder()
.comments(commentsAfterUpdate)
.build())))
.andReturn().getResponse();
//then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
PostDTO returnedPost = objectMapper.readValue(response.getContentAsString(), POST_TYPE_REFERENCE);
PostDTO postAfterUpdate = postService.findAllByReferenceId(100L).get(0);
assertThat(returnedPost).isEqualTo(postAfterUpdate);
assertThat(postBeforeUpdate.size()).isEqualTo(1);
assertThat(postBeforeUpdate.get(0).getComments()).isEqualTo(commentsBeforeUpdate);
assertThat(postAfterUpdate.getComments()).isEqualTo(commentsAfterUpdate);
}
}
- Проблема в том, что тест не проходит из-за: java.lang.IllegalStateException: дублирование источника ключа (попытка слияния) проверки значений и тест)
- Сгенерированные SQL-запросы показывают, что удаляется только одна строка и только одни метаданные (тот, у которого есть «reference_id», а не тот, у которого есть «origin» ")
- Это происходит, вероятно, потому, что метаданные в Post имеют orphanRemoval = true и по какой-то причине удаляется только "reference_id" и ' put' снова (из-за cascade.ALL).
- Это означает, что во время запроса «обновления» в приведенном выше тесте у нас есть три метаданных в сообщении: 2x с ключом «origin» (старый и новый, поскольку orphanremoval его не удалил) и 1x с ключом «reference_id» (старый заменен новым).
- Это вызывает проблему в: Posts.stream().map(persistablePostMapper::mapToPost).collect(Collectors.toList()); где он отображает метаданные:
Код: Выделить всё
default MetadataDTO mapToMetadataDTO(final List persistableMetadata) {
Map metadata = persistableMetadata
.stream()
.filter(content -> content.getKey() != null && content.getValue() != null)
.collect(Collectors.toMap(Metadata::getKey, Metadata::getValue));
return MetadataDTO.builder()
.metadata(metadata)
.build();
}
Подробнее здесь: https://stackoverflow.com/questions/786 ... an-removal