在 Spring Boot 中测试 @OneToMany

问题描述

我有以下实体:

@Getter
@Setter
@NoArgsConstructor
@MappedSuperclass
public class BaseEntity implements Serializable{
    
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;


    public BaseEntity(Long id) {
        this.id = id;
    }

    public boolean isNew(){
        return this.id == null;
    }
}

@Setter
@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate
@Table(name = "project")
public class Project extends BaseEntity{

    private static final long serialVersionUID = 1L;

    @Column
    @NotBlank(message = "Name property cannot be null or empty")
    private String name;

    @Column
    private String description;

    @Column
    private LocalDateTime created;

    @Column
    private Long parentId;
    
    @OnetoMany(mappedBy = "parentId",cascade = { CascadeType.ALL },fetch = FetchType.EAGER)
    private Set<Project> subprojects = new HashSet<>();

    @Builder
    public Project(Long id,String name,String description,LocalDateTime created,Long parentId) {
        super(id);
        this.name = name;
        this.description = description;
        this.created = created;
        this.parentId = parentId;
}

和通常的存储库:

@Repository
public interface ProjectRepository extends JpaRepository<Project,Long>{
}

这里是集成测试类:

@DataJpaTest
class ProjectRepositoryIT {
    
    @Autowired
    TestEntityManager testEntityManager;
    
    @Autowired
    ProjectRepository projectRepository;
    
    @Test
    @Transactional
    void testSaveSubproject() {
        Project parent = Project.builder().name("parent").build();
        parent = testEntityManager.persistAndFlush(parent);
        
        Project child = Project.builder().name("child").parentId(parent.getId()).build();
        child = testEntityManager.persistAndFlush(child);
        
        var optionalParent = projectRepository.findById(parent.getId());
        
        if(optionalParent.isPresent()) {
            var foundParent = optionalParent.get();
            
            assertEquals(parent.getId(),foundParent.getId());
            assertEquals(1,foundParent.getSubprojects().size());
            
            Optional<Project> matchigProject = foundParent.getSubprojects()
                .stream()
                .filter(p -> p.getId().equals(foundParent.getId()))
                .findFirst();
            
            assertTrue(matchigProject.isPresent());
    
            assertEquals(child.getId(),matchigProject.get().getId());
        }else {
            fail("cannot find parent project");
        }
    }
}

问题

我正在尝试测试是否可以通过 set parentId 属性正确标记子项目。我希望当设置 parentId 属性时,带有此 id 的项目应该在 subprojects 列表中包含有关子项目的信息。不幸的是,断言 assertEquals(1,foundParent.getSubprojects().size()); 失败,因为列表大小为 0。我想问一下我做错了什么?我需要宣布我在春天是个新人。

解决方法

这是一个安静的常见错误。 要使其工作,您可以执行以下热修复:

    Project parent = Project.builder().name("parent").build();
    Project child = Project.builder().name("child").build();
    parent.getSubprojects().add(child);

    parent = testEntityManager.persistAndFlush(parent);

    child.setParentId(parent.getId());
    child = testEntityManager.persistAndFlush(child);

Spring 不会自动将子实体中的 parentId 与父实体中的子项目集绑定,没有魔法。您必须将子实体放在父实体的一组子项目中保存,然后自己设置子实体的parentId并仔细管理。

,

感谢@Alex Crazy 的回答。我的目标是让它尽可能简单。我希望每个 Project 都有对父级的引用,当父级被删除时,所有子级也被删除。

我通过以下方式修复:

Project 只有 parentId 字段,没有任何 @OneToMany@ManyToOne 链接:

...
public class Project extends BaseEntity{
...
    @Column
    private Long parentId;
...
}

然后通过以下搜索扩展 ProjectRepository

@Repository
public interface ProjectRepository extends JpaRepository<Project,Long>{
    
    List<Project> findAllByParentId(Long id);
    
    void deleteAllByParentId(Long id);
}

和测试:

    @Test
    void testFindSubprojects() {
        // Prepare database
        Project parent = Project.builder().name("parent").build();
        parent = testEntityManager.persistAndFlush(parent);
        
        Project child = Project.builder().name("child").parentId(parent.getId()).build();
        child = testEntityManager.persistAndFlush(child);
        
        // Call repository
        Project foundParent = projectRepository.findById(parent.getId()).orElseThrow(() -> fail());
        assertNotNull(foundParent.getId());
        
        List<Project> subprojects = projectRepository.findAllByParentId(foundParent.getId());
        assertEquals(1,subprojects.size());
        assertEquals(child.getId(),subprojects.get(0).getId());
        assertEquals(child.getParentId(),foundParent.getId());
    }
    
    @Test
    void testDeleteAllByParentId() {
        // Prepare database
        Project parent = Project.builder().name("parent").build();
        parent = testEntityManager.persistAndFlush(parent);
        
        testEntityManager.persistAndFlush(Project.builder().name("child1").parentId(parent.getId()).build());
        testEntityManager.persistAndFlush(Project.builder().name("child2").parentId(parent.getId()).build());
        testEntityManager.persistAndFlush(Project.builder().name("child3").parentId(parent.getId()).build());
        
        var subprojects = projectRepository.findAllByParentId(parent.getId());
        assertEquals(3,subprojects.size());
        
        projectRepository.deleteAllByParentId(parent.getParentId());
        
        subprojects = projectRepository.findAllByParentId(parent.getParentId());
        assertEquals(0,subprojects.size());
    }

删除ass嵌套子项目将由服务层管理。我希望这是一个好的解决方案。