2.7.18
с Java 17
и Maven.У меня есть следующие объекты:
@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<Comment> comments;
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(fetch = FetchType.EAGER, mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
List<Metadata> 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;
}
и соответствующие DTO:
@Builder(toBuilder = true)
@Getter
@Jacksonized
public class PostDTO {
Long id;
MetadataDTO metadata;
List<CommentDTO> comments;
PostStatus status;
}
и
@ToString
@Value(staticConstructor = "of")
public class MetadataDTO {
Map<String, String> metadata;
@JsonCreator
@Builder(toBuilder = true)
public MetadataDTO(@JsonProperty("metadata") final Map<String, String> 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<Long> 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<PostDTO> findAllByReferenceId(final Long referenceId) {
List<Post> 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<PostDTO> 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<CommentDTO> commentsBeforeUpdate = List.of(comment1);
List<CommentDTO> 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<PostDTO> 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: Duplicate key origin (attempted merging values test and test)
metadata
в Post
имеет orphanRemoval = true
и по какой-то причине только «reference_id» удаляется и «вставляется» снова (из-за cascade.ALL
).posts.stream().map(persistablePostMapper::mapToPost).collect(Collectors.toList());
где сопоставляется метаданные:трассировка стека в точках Duplicate key exception
здесь (до toMap
):
default MetadataDTO mapToMetadataDTO(final List<Metadata> persistableMetadata) {
Map<String, String> metadata = persistableMetadata
.stream()
.filter(content -> content.getKey() != null && content.getValue() != null)
.collect(Collectors.toMap(Metadata::getKey, Metadata::getValue));
return MetadataDTO.builder()
.metadata(metadata)
.build();
}
Может ли кто-нибудь объяснить мне, почему удаление потерянных файлов не воссоздает/удаляет всю коллекцию метаданных, а только «reference_id»? Почему метаданные с ключом «происхождение» также не воссоздаются?
В вашем случае проблема возникает из-за того, что существующие метаданные не удаляются должным образом, несмотря на то, что атрибут orphanRemoval = true
установлен в поле метаданных сущности Post
.
Hibernate orphanRemoval
удаляет только те объекты, которые были разыменованы. Поэтому вам необходимо явно удалить сущности, на которые больше нет ссылок, из существующей коллекции метаданных.
Первый метод — добавить метод установки для поля metadata
в сущности Post и включить логику в remove the existing metadata when setting a new metadata list
.
public void setMetadata(List<Metadata> metadata) {
if (this.metadata == null) {
this.metadata = new ArrayList<>();
}
this.metadata.clear();
if (metadata != null) {
this.metadata.addAll(metadata);
}
}
Второй метод — удалить все существующие метаданные и заменить их новыми метаданными в методе save
файла PostService
.
public PostDTO save(final PostDTO postDTO) {
Post persistablePost = persistablePostMapper.mapToPersistablePost(postDTO);
if (persistablePost.getId() != null) {
Post existingPost = postRepository.findById(persistablePost.getId()).orElseThrow();
existingPost.getMetadata().clear();
existingPost.getMetadata().addAll(persistablePost.getMetadata());
persistablePost = existingPost;
}
Post savedPersistablePost = postRepository.save(persistablePost);
return persistablePostMapper.mapToPost(savedPersistablePost);
}
Думаю эта ссылка будет вам полезна.
Хорошего дня!
Почему вы используете второй if в методе setMetadata, потому что вы инициализируете поле this.metadata раньше, если оно имеет значение null. Я хочу сказать, что это if
всегда будет правдой
в первом случае, если он использует this.metadata
, то это относится к Post#metadata
, во втором он использует metadata
без (this
), поэтому он относится к аргументу метода. Поправьте меня, если я ошибаюсь. Кевин, большое спасибо, я ценю это.
Ваше наблюдение верно :) В setMetadata method of the Post entity
this.metadata относится к полю метаданных внутри самой сущности Post. В save method of PostService
persistablePost.getMetadata()
относится к метаданным нового сохраняемого объекта Post
, а existingPost.getMetadata()
относится к метаданным существующего объекта Post
, полученного из репозитория.
Проблема возникает из-за того, что параметр
orphanRemoval = true
в списке метаданных в сущностиPost
работает неправильно. Когда вы обновляете метаданные, Hibernate пытается удалить старые записи и добавить новые. Однако он не удаляет все старые записи должным образом, что приводит к появлению дубликатов.