首页 > 解决方案 > Spring REST API 列表始终为空

问题描述

因此,我想在 Intellij(使用 Gradle)中使用 Spring 创建一个 REST API 来管理国家/地区。每个国家/地区都有多个区域(作为列表),每个区域都有多个位置。我正在使用 Hibernate,并且我有一个名为 DatabaseLoader 的类,它可以在数据库中预加载条目。它预加载国家/地区,但它们的列表是空的。谁能解释我做错了什么?

这是国家级:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Countries")
public class Country {
    private @Id @GeneratedValue Long id;
    private String name;

    @Autowired
    @OneToMany(mappedBy="country")
    private List<Region> regions = new ArrayList<>();

    Country (String name, List<Region> regions) {
        this.name = name;
        this.regions = regions;
    }

    Country(String name) {
        this.name = name;
    }

    Country() {}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<Region> getRegions() {
        return regions;
    }

    public void setRegions(List<Region> regions) {
        this.regions = regions;
    }

    @Bean
    @Autowired
    public void addRegion(Region region) {
        regions.add(region);
    }

    @Bean
    @Autowired
    public void listRegions() {
        System.out.println(name);
        System.out.println("xxxx");
        for (Region region : regions) {
            region.listLocations();
        }
    }
}

和区域类:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Regions")
public class Region {
    private @Id @GeneratedValue Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    @OneToMany(mappedBy="region")
    private List<Location> locations = new ArrayList<>();

    public Country getCountry () {
        return country;
    }

    public void setCountry (Country country) {
        this.country = country;
    }

    public Region(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<Location> getLocations() {
        return locations;
    }

    public void setLocations(List<Location> locations) {
        this.locations = locations;
    }

    @Bean
    public void addLocation(Location location) {
        this.locations.add(location);
    }

    @Bean
    @Autowired
    public void listLocations() {
        System.out.println(this.name);
        System.out.println("xxx");
        for(int i =0; i < locations.size(); i++) {
            System.out.print(locations.get(i).getId() + " ");
            locations.get(i).showAvailableSports();
        }
    }
}

这是数据库加载器:

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class DatabaseLoader {
    private static final Logger log = LoggerFactory.getLogger(DatabaseLoader.class);

    @Bean
    CommandLineRunner initDatabase(CountryRepo repo) {
        return args -> {
            Region lyon = new Region("Lyon");
            List<Region> regions = new ArrayList<>();
            Country France = new Country("France");
            France.addRegion(lyon);
            log.info("Preloading " + repo.save(France)  + " " + France.getRegions().get(1).getName());
        };
    }
}

在输出中,它正确显示了区域的名称,但是当使用 Postman 进行 GET 请求时,列表为空。

标签: javaspringapihibernaterest

解决方案


由于您有两个不同的实体并且您在这里只保存一个(父级),因此您还需要将您的持久效果级联到子级,如下所示

@OneToMany(mappedBy="country", cascade = {CascadeType.PERSIST})
private List<Region> regions = new ArrayList<>();

这将指示 JPA 提供者(默认休眠)在父实体(保存国家/地区)时也保存子实体。

Cascade.PERSIST是一种级联类型,您可以在更新/删除父实体 ie 等时产生类似的效果Cascade.MERGECascade.REMOVE如果您想有选择地选择然后添加其中一些,否则使用Cascade.ALL,但要注意这将适用于所有效果,所以请阅读有关的文档它。

您在日志中看到的name区域由您设置,尝试打开休眠查询日志并查看查询是否被触发到子表。

编辑

简短回答:让你的两个实体保持同步

public void addRegion(Region region) {
   regions.add(region);
   region.setCountry(this);
}

TL;博士

在下面的评论之后,我看到了您的代码的另一个问题,而在写这个答案时我错过了它。

问题是您的实体之间存在双向关系,当您拥有这种关系时,您必须在对一个或另一个执行操作时使它们保持同步。

如果您要检查您的日志(如果您不知道,请在此答案的底部找到参考),然后 JPA(休眠)也会向您的数据库发出查询以获取区域实体。您还可以查询您的数据库并进行检查。

但是您会发现外键country_id为空,因此您的两个表之间的关系未建立,当您查询国家/地区时,hibernate 将发出第二个查询以获取相关区域(如果您的获取策略是 LAZY)

select regions0_.country_id as country_3_3_0_, regions0_.id as id1_3_0_, regions0_.id as id1_3_1_, regions0_.country_id as country_3_3_1_, regions0_.name as name2_3_1_ from region regions0_ where regions0_.country_id=?

由于country_idin 中的字段region将为空,因此您不会收到带有国家 ID 的区域。

因此,为了保持双向关系同步,您可以做两件事:

  • 要么在两个实体上调用 setter。
  • 或者,在addRegion您所在国家/地区实体的函数中调用区域设置器。

两个实体上的调用设置器

addRegion在国家实体中具有功能,该实体正在将区域添加到其List<Region>

public void addRegion(Region region) {
   regions.add(region);
}

并且您setCountry在区域实体中

public void setCountry (Country country) {
   this.country = country;
}

您需要在两个实体上的命令行运行程序中调用这两种方法,以便休眠可以在表中为您应用该外键。

Region lyon = new Region();
lyon.setName("Lyon");

Country france = new Country();
france.setName("France");
france.addRegion(lyon);

lyon.setCountry(france);

//other code below it to save

在 addRegion 函数中调用区域设置器

早期方法的问题是客户端必须知道它需要在两个实体上调用 setter。

要解决此问题,您可以在客户端内部函数中隐藏这两个操作addRegion,如下所示

public void addRegion(Region region) {
   regions.add(region);
   region.setCountry(this);
}

这将更新country_id区域表中的字段,您必须在获取请求中获取包含区域的国家/地区。

这些是它在应用程序运行时触发的查询(您的可能有更多字段)

insert into country (name) values (?)
insert into region (country_id, name) values (?, ?)

如果您想知道我是如何打印此 SQL 的,那么不要担心它很容易,您必须在开发中使用它来检查查询。

您可以简单地将这两个属性添加到您的application.properties

spring.datasource.jpa.show-sql=true
logging.level.org.hibernate.SQL=debug

注意:如果您返回实体对象作为响应,您必须@JsonIgnore在您的类getCountry()内部注释Region,否则它将通过 stackoverlow 错误,因为杰克逊试图递归序列化您的Country对象及其所有属性。

另外,为什么你有这些@Bean注释在你的实体类之上getters或之上?setters要么你误解了@Bean注释,要么你试图做一些你不应该做的事情。


推荐阅读