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

标签 java spring-boot recursion tree spring-data-jpa

已经创建了一个 Spring Boot 微服务,它发出一个 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>

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 
}

节点库:

@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);
}

我的服务:

public interface MyService {
    List<Node> getHierarchyPerNode(Node node);
    void removeNode(String node);
}

我的服务实现:

@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;
        }
    }
}

休息 Controller :

@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);
    }
}

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);
    }
}

很明显,填充的数据代表了数据库中的这棵树:

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

 |______Coffee
        |
        |______Mocha
        |
        |______Latte
               |
               |_____Espresso

观察/问题:

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

http://localhost:8080/myapp/api/nodes?name=Products

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

[
  {
    "id": 2,
    "name": "Books",
    "parentId": 1
  },
  {
    "id": 6,
    "name": "Coffee",
    "parentId": 1
  }
]

不再在书籍和摩卡咖啡下列出恐怖、浪漫、奇幻,在咖啡下列出拿铁咖啡(以及拿铁咖啡下的浓缩咖啡)

现在,如果我使用 parentNode(例如 Books),它会显示子节点(但只显示第一层):

http://localhost:8080/myapp/api/nodes?name=Books

JSON 响应负载:

[
  {
    "id": 3,
    "name": "Horror",
    "parentId": 2
  },
  {
    "id": 4,
    "name": "Romance",
    "parentId": 2
  },
  {
    "id": 5,
    "name": "Fantasy",
    "parentId": 2
  }
]   

当试图列出 Coffee 的所有 child 时:

http://localhost:8080/myapp/api/nodes?name=Coffee

JSON 响应负载:

[
  {
    "id": 7,
    "name": "Mocha",
    "parentId": 6
  },
  {
    "id": 8,
    "name": "Latte",
    "parentId": 6
  }
]

看,这个不显示 Espresso,必须调用 Latte 作为父项才能明确查看:

http://localhost:8080/myapp/api/nodes?name=Latte

JSON 响应负载:

{
    "id": 9,
    "name": "Espresso",
    "parentId": 8
}

可以获取特定 child 级别的节点...

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

需要使用递归来获取所有子子节点/子级别,但不知道如何针对这两种情况(获取子节点和删除节点)进行操作。

最佳答案

不确定您为什么不在这里充分利用 JPA,首先是在实体级别,然后是在使用 native 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;
}

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

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

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

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

Controller

@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);
}

顶级节点检索

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

    traverseNodeAndFetchChildren(node);

    return node.getChildren();
 }

递归遍历和抓取

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

   if(size > 0){
      for(Node childNode: node.getChildren()){
         traverseNodeAndFetchChildren(childNode);
      }
   }       
}

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

4) 将您的服务方法标记为 @Transactional(readOnly = true) 可能也是个好主意。

关于java - 无法使用 Java 完全递归树层次结构,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56358596/

相关文章:

java - 哪个相当于reactor.netty.http.server包中的旧方法startAndAwait?

java - 如何创建由 gradle 预编译运行的注释处理器以将代码添加到方法中?

java - 使用自定义提供程序的 Spring Boot 安全性

windows - 跨子文件夹运行批处理脚本(递归)

java - 如何在Java中找到一个单词的N个词组?

java - 如何使用嵌套循环先向上再向下计数?

java - 在 Java 中通过循环创建乘法表

javascript - 这个递归函数是如何解析的,为什么会输出它所做的

java - 使用 Collection.max() 在 HashSet 中查找最大值

java - Spring Boot - 无法确定数据库类型 NONE 的嵌入式数据库驱动程序类