无法使用Java完全递归树层次结构

Pac*_*ver 5 java tree recursion spring-data-jpa spring-boot

已经创建了一个Spring Boot Microservice,它发出一个HTTP GET来从MySQL数据库中提取数据(每个节点),该数据基于Adjacency List Tree在单个表中设置。

能够在特定级别获得节点的子代,但还需要能够看到所有子代(即使它需要不同的REST调用和服务方法)。

我在技术堆栈中使用Java 1.8,Spring Boot 1.5.6.RELEASE,JPA和MySQL 5。

pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.6.RELEASE</version>
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
Run Code Online (Sandbox Code Playgroud)

Node.java(POJO):

@Entity
public class Node {

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

    @NotNull
    private String name;

    @Column(name = "parent_id")
    private Long parentId;

    // Getters & Setters Omitted for Brevity 
}
Run Code Online (Sandbox Code Playgroud)

NodeRepository:

@Repository
public interface NodeRepository extends JpaRepository<Node, Long> {

    @Query(value = "SELECT * FROM NODE WHERE parent_id = ?", nativeQuery = true)
    List<Node> findNodesByParentId(Long parentId);

    @Query(value = "SELECT * FROM NODE WHERE name = ?", nativeQuery = true)
    Node findByName(String name);
}
Run Code Online (Sandbox Code Playgroud)

我的服务:

public interface MyService {
    List<Node> getHierarchyPerNode(Node node);
    void removeNode(String node);
}
Run Code Online (Sandbox Code Playgroud)

MyServiceImpl:

@Service
public class MyServiceImpl implements MyService {

   @Autowired
   NodeRepository repository;

   @Override
   public List<Node> getHierarchyPerNode(Node node) {
        List<Node> nodes = new ArrayList<>();
        List<Node> children = new ArrayList<>();
        if (node != null) {
            Node aNode = repository.findByName(node.getName());
            nodes.add(aNode);
            Long parentId = aNode.getId();
            children = repository.findNodesByParentId(parentId);

            // Was trying this as recursion but kept throwing an NullPointerException.
            // for (Node child : children) {
            //      return getHierarchyPerNode(child);
            //  }
        }
        if (!children.isEmpty()) {
            return children;
        } 
        else { 
            return nodes;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

RestController:

@RestController
public class RestController {

    private HttpHeaders headers = null;

    @Autowired
    MyService myService;

    public RestController() {
        headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
    }

    @RequestMapping(
        value = {"/api/nodes"}, 
        method = RequestMethod.GET, 
        produces = "APPLICATION/JSON"
    )
    public ResponseEntity<Object> getHierarchyPerNode(Node node) {
        if (null == node) {
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        }
        List<Node> nodes = myService.getHierarchyPerNode(node);

        if (null == nodes) {
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        }

        return new ResponseEntity<Object>(nodes, headers, HttpStatus.OK);
    }
}
Run Code Online (Sandbox Code Playgroud)

DatabasePopulator(在Spring Boot启动期间,使用它来填充数据库):

@Component
public class DatabasePopulator implements ApplicationListener<ContextRefreshedEvent> {

    private final NodeRepository repository;

    public DatabasePopulator(NodeRepository repository) {
        this.repository = repository;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        Node root = new Node();
        root.setName("Store");
        root.setParentId(null);
        repository.save(root);

        // Populate Books Node (along with children)
        Node books = new Node();
        books.setName("Books");
        books.setParentId(root.getId());
        repository.save(books);

        Node horror = new Node();
        horror.setName("Horror");
        horror.setParentId(books.getId());
        repository.save(horror);

        Node romance = new Node();
        romance.setName("Romance");
        romance.setParentId(books.getId());
        repository.save(romance);

        Node fantasy = new Node();
        fantasy.setName("Fantasy");
        fantasy.setParentId(books.getId());
        repository.save(fantasy);

        // Populate Coffee Node (along with children)
        Node coffee = new Node();
        coffee.setName("Coffee");
        coffee.setParentId(root.getId());
        repository.save(coffee);

        Node mocha = new Node();
        mocha.setName("Mocha");
        mocha.setParentId(coffee.getId());
        repository.save(mocha);

        Node latte = new Node();
        latte.setName("Latte");
        latte.setParentId(coffee.getId());
        repository.save(latte);

        // Populate show espresso as a child underneath the Latte node.
        Node espresso = new Node();
        espresso.setName("Espresso");
        espresso.setParentId(latte.getId());
        repository.save(espresso);
    }
}
Run Code Online (Sandbox Code Playgroud)

显然,填充的数据表示数据库内部的这棵树:

Store
|______ Books
        |
        |______Horror
        |
        |______Romance
        |
        |______Fantasy

 |______Coffee
        |
        |______Mocha
        |
        |______Latte
               |
               |_____Espresso
Run Code Online (Sandbox Code Playgroud)

意见/问题:

通过我的RestController,我可以通过调用此REST端点来获取第一级记录:

http://localhost:8080/myapp/api/nodes?name=Products
Run Code Online (Sandbox Code Playgroud)

但是,它只给我第一级(不是Books&Coffee和Latte下的子节点):

[
  {
    "id": 2,
    "name": "Books",
    "parentId": 1
  },
  {
    "id": 6,
    "name": "Coffee",
    "parentId": 1
  }
]
Run Code Online (Sandbox Code Playgroud)

与其同时列出“恐怖”,“浪漫”,“书籍”和“摩卡”下的“幻想”,“咖啡”下的“拿铁咖啡”(以及“咖啡”下的咖啡)

现在,如果我使用parentNode(例如Books),它将显示子级(但仅显示第一级):

http://localhost:8080/myapp/api/nodes?name=Books
Run Code Online (Sandbox Code Playgroud)

JSON响应有效负载:

[
  {
    "id": 3,
    "name": "Horror",
    "parentId": 2
  },
  {
    "id": 4,
    "name": "Romance",
    "parentId": 2
  },
  {
    "id": 5,
    "name": "Fantasy",
    "parentId": 2
  }
]   
Run Code Online (Sandbox Code Playgroud)

尝试列出Coffee的所有孩子时:

http://localhost:8080/myapp/api/nodes?name=Coffee
Run Code Online (Sandbox Code Playgroud)

JSON响应有效负载:

[
  {
    "id": 7,
    "name": "Mocha",
    "parentId": 6
  },
  {
    "id": 8,
    "name": "Latte",
    "parentId": 6
  }
]
Run Code Online (Sandbox Code Playgroud)

看到,这个没有显示Espresso,必须调用Latte作为父项来显式查看:

http://localhost:8080/myapp/api/nodes?name=Latte
Run Code Online (Sandbox Code Playgroud)

JSON响应有效负载:

{
    "id": 9,
    "name": "Espresso",
    "parentId": 8
}
Run Code Online (Sandbox Code Playgroud)

可以使节点处于特定儿童级别...

如何使用递归获取所有级别的所有节点(我知道这将是不同的REST GET调用/ REST端点)?

需要使用递归来获取所有子子级/子级别,但不知道如何在两种情况下都这样做(获取子节点和删除节点)。

Mac*_*ski 5

不确定您为什么不在这里充分利用 JPA,首先是在实体级别,然后是在查询使用本机 SQL 而不是 JPQL 的地方。

1)如果您按如下方式更改您的实体:

@Entity
public class Node {

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

    @NotNull
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Node parentNode;

    @OneToMany(mappedBy = "parentNode", 
               cascade = { CascadeType.DELETE, CascadeType.PERSIST} )
    private List<Node> children;
}
Run Code Online (Sandbox Code Playgroud)

2)然后稍微更改您的查询以使其与JPQL兼容:

@Query(value = "select n from Node n inner join n.parentNode p where p.id = ?")
List<Node> findNodesByParentId(Long parentId);
Run Code Online (Sandbox Code Playgroud)

现在,默认情况下只会在此处获取顶级节点,因为默认情况下@OneToMany关系是延迟加载的。

3)此时您需要做的就是稍微改变您的递归方法以符合更改并获得您需要的内容:

控制器

@RequestMapping(
    value = {"/api/nodes"}, 
    method = RequestMethod.GET, 
    produces = "APPLICATION/JSON"
)
public ResponseEntity<Object> getNodeHierarchy(Node node) {
    if (null == node) {
        return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
    }
    List<Node> nodes = myService.getNodeHierarchy(node);

    if (null == nodes) {
        return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
    }

    return new ResponseEntity<Object>(nodes, headers, HttpStatus.OK);
}
Run Code Online (Sandbox Code Playgroud)

顶级节点检索

 @Override
 @Transactional(readOnly = true)
 public List<Node> getNodeHierarchy(Node inNode){
    Node node = repository.findByName(inNode.getName());

    traverseNodeAndFetchChildren(node);

    return node.getChildren();
 }
Run Code Online (Sandbox Code Playgroud)

递归遍历和获取

public void traverseNodeAndFetchChildren(Node node) {
   int size = node.getChildren().size();

   if(size > 0){
      for(Node childNode: node.getChildren()){
         traverseNodeAndFetchChildren(childNode);
      }
   }       
}
Run Code Online (Sandbox Code Playgroud)

node.getChildren().size()- 这使得 Persistence Context 延迟加载@OneToMany依赖项。

4)将您的服务方法标记为@Transactional(readOnly = true).