There is a many-to-many relationship between User and Role. Just use Cascade.MERGE for cascade.
Role will have some initial data saved when the system starts, and can be added again. User can be bound to these Roles. Role will not be affected when User is deleted.
Below is the relevant code for the experiment, which should work.
bean / ForumPost.java
@Getter @Setter @ToString @NoArgsConstructor @Entity @Table(name="forum_post") public class ForumPost { @Id @GeneratedValue private Long id; private String title; @ManyToMany(cascade = {CascadeType.MERGE}, fetch = FetchType.EAGER) @JoinTable( name = "forum_post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private List<ForumTag> tags = new ArrayList<>(); }
bean/ForumTag.java
@Getter @Setter @ToString @NoArgsConstructor @Entity @Table(name="forum_tag") public class ForumTag { @Id @GeneratedValue private Long id; private String name; public ForumTag(String name){ this.name = name; } }
repository/ForumPostRepository.java
public interface ForumPostRepository extends JpaRepository<ForumPost, Long> { Optional<ForumPost> findByTitle(String title); }
repository/ForumTagRepository.java
public interface ForumTagRepository extends JpaRepository<ForumTag, Long> { Optional<ForumTag> findByName(String name); }
test/ForumTagRepositoryTest.java
@SpringBootTest public class ForumTagRepositoryTest { @Autowired private ForumTagRepository tagRepository; @Autowired private ForumPostRepository postRepository; @Test void injectedComponentsAreNotNull(){ assertNotNull(tagRepository); assertNotNull(postRepository); } private String tagName1 = "SpringBoot"; private String tagName2 = "SwiftUI"; private String tagName3 = "Java"; private String postTitle1 = "Tutorial about Springboot"; private String postTitle2 = "Tutorial about SwiftUI"; private String postTitle3 = "Tutorial about Java"; private ForumTag tag1 = new ForumTag(tagName1); private ForumTag tag2 = new ForumTag(tagName2); private ForumTag tag3 = new ForumTag(tagName3); private void detach() { // 1-detach if (!postRepository.findByTitle(postTitle1).isEmpty()) { ForumPost post1 = postRepository.findByTitle(postTitle1).get(); post1.setTags(null); postRepository.save(post1); } if (!postRepository.findByTitle(postTitle2).isEmpty()) { ForumPost post2 = postRepository.findByTitle(postTitle2).get(); post2.setTags(null); postRepository.save(post2); } if (!postRepository.findByTitle(postTitle3).isEmpty()) { ForumPost post3 = postRepository.findByTitle(postTitle3).get(); post3.setTags(null); postRepository.save(post3); } } public void clearData() { // 1-detach detach(); // 2- delete tagRepository.deleteAll(); postRepository.deleteAll(); } public void prepareData() { tagRepository.save(tag1); tagRepository.save(tag2); tagRepository.save(tag3); ForumPost post1 = new ForumPost(); post1.setTitle(postTitle1); post1.setTags(Arrays.asList(tag1)); ForumPost post2 = new ForumPost(); post2.setTitle(postTitle2); post2.setTags(Arrays.asList(tag1, tag2)); ForumPost post3 = new ForumPost(); post3.setTitle(postTitle3); post3.setTags(Arrays.asList(tag1, tag2, tag3)); postRepository.save(post1); postRepository.save(post2); postRepository.save(post3); } @Test public void test_save_tag_and_save_post() throws ResourceNotFoundException { clearData(); // we have a few pre-saved tags tagRepository.save(tag1); tagRepository.save(tag2); tagRepository.save(tag3); assertEquals(tagRepository.findByName(tagName1).get().getName(), tagName1); assertEquals(tagRepository.findByName(tagName2).get().getName(), tagName2); assertEquals(tagRepository.findByName(tagName3).get().getName(), tagName3); // see if we can save these posts and connect with tags ForumPost post1 = new ForumPost(); post1.setTitle(postTitle1); post1.setTags(Arrays.asList(tag1)); ForumPost post2 = new ForumPost(); post2.setTitle(postTitle2); post2.setTags(Arrays.asList(tag1, tag2)); ForumPost post3 = new ForumPost(); post3.setTitle(postTitle3); post3.setTags(Arrays.asList(tag1, tag2, tag3)); postRepository.save(post1); postRepository.save(post2); postRepository.save(post3); assertEquals(postRepository.findByTitle(postTitle1).get().getTitle(), postTitle1); assertEquals(postRepository.findByTitle(postTitle1).get().getTags().size(), 1); assertEquals(postRepository.findByTitle(postTitle1).get().getTags().get(0).getName(), tagName1); assertEquals(postRepository.findByTitle(postTitle2).get().getTitle(), postTitle2); assertEquals(postRepository.findByTitle(postTitle2).get().getTags().size(), 2); assertEquals(postRepository.findByTitle(postTitle2).get().getTags().get(0).getName(), tagName1); assertEquals(postRepository.findByTitle(postTitle2).get().getTags().get(1).getName(), tagName2); assertEquals(postRepository.findByTitle(postTitle3).get().getTitle(), postTitle3); assertEquals(postRepository.findByTitle(postTitle3).get().getTags().size(), 3); assertEquals(postRepository.findByTitle(postTitle3).get().getTags().get(0).getName(), tagName1); assertEquals(postRepository.findByTitle(postTitle3).get().getTags().get(1).getName(), tagName2); assertEquals(postRepository.findByTitle(postTitle3).get().getTags().get(2).getName(), tagName3); } @Test public void test_save_post() throws ResourceNotFoundException { clearData(); // let's save post directly to see how tags ForumPost post1 = new ForumPost(); post1.setTitle(postTitle1); post1.setTags(Arrays.asList(tag1)); ForumPost post2 = new ForumPost(); post2.setTitle(postTitle2); post2.setTags(Arrays.asList(tag1, tag2)); ForumPost post3 = new ForumPost(); post3.setTitle(postTitle3); post3.setTags(Arrays.asList(tag1, tag2, tag3)); Exception exception1 = assertThrows(InvalidDataAccessApiUsageException.class, () -> { postRepository.save(post1); }); Exception exception2 = assertThrows(InvalidDataAccessApiUsageException.class, () -> { postRepository.save(post2); }); Exception exception3 = assertThrows(InvalidDataAccessApiUsageException.class, () -> { postRepository.save(post3); }); assertEquals("org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.spring_data_mysql.forum.bean.ForumTag", exception1.getMessage()); assertEquals("org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.spring_data_mysql.forum.bean.ForumTag", exception2.getMessage()); assertEquals("org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.spring_data_mysql.forum.bean.ForumTag", exception3.getMessage()); } @Test public void test_delete_post_to_see_tag() { clearData(); prepareData(); // 1- delete post ForumPost post1 = postRepository.findByTitle(postTitle1).get(); postRepository.deleteById(post1.getId()); ForumPost post2 = postRepository.findByTitle(postTitle2).get(); postRepository.deleteById(post2.getId()); ForumPost post3 = postRepository.findByTitle(postTitle3).get(); postRepository.deleteById(post3.getId()); // 2- check tag assertEquals(tagRepository.findByName(tagName1).get().getName(), tagName1); assertEquals(tagRepository.findByName(tagName2).get().getName(), tagName2); assertEquals(tagRepository.findByName(tagName3).get().getName(), tagName3); } @Test public void test_delete_tag_to_see_post() { clearData(); prepareData(); // 1- delete tag ForumTag tag1 = tagRepository.findByName(tagName1).get(); ForumTag tag2 = tagRepository.findByName(tagName2).get(); ForumTag tag3 = tagRepository.findByName(tagName3).get(); assertThrows(DataIntegrityViolationException.class, () -> { tagRepository.deleteById(tag1.getId()); }); assertThrows(DataIntegrityViolationException.class, () -> { tagRepository.deleteById(tag2.getId()); }); assertThrows(DataIntegrityViolationException.class, () -> { tagRepository.deleteById(tag3.getId()); }); } @Test public void test_detach_post_then_delete_tag() { clearData(); prepareData(); detach(); ForumTag tag1 = tagRepository.findByName(tagName1).get(); tagRepository.deleteById(tag1.getId()); ForumTag tag2 = tagRepository.findByName(tagName2).get(); tagRepository.deleteById(tag2.getId()); ForumTag tag3 = tagRepository.findByName(tagName3).get(); tagRepository.deleteById(tag3.getId()); // check post assertEquals(postRepository.findByTitle(postTitle1).get().getTitle(), postTitle1); assertEquals(postRepository.findByTitle(postTitle1).get().getTags().size(), 0); assertEquals(postRepository.findByTitle(postTitle2).get().getTitle(), postTitle2); assertEquals(postRepository.findByTitle(postTitle2).get().getTags().size(), 0); assertEquals(postRepository.findByTitle(postTitle3).get().getTitle(), postTitle3); assertEquals(postRepository.findByTitle(postTitle3).get().getTags().size(), 0); } }
Regarding database testing, a real database is used. The following situations were tested
Test case | Status |
---|---|
After the Tag is saved, is it possible? Saving Post | Passed |
Tag has no data. Will the Tag be saved when saving Post? | No, because there is no Using Cascade.PERSIST |
Is it possible to delete Post separately? Tag will not affect | passing |
Is it possible to delete the Tag separately? | No, you need to contact the binding of the relevant Post first. Only when there is no binding, can you delete the Tag |
The complete code is in this repository, https://github.com/tutehub/sample-spring/tree/develop