使用 Spring Data Rest 查询集合时获取重复项
Getting duplicate items when querying a collection with Spring Data Rest
我在这个简单模型的集合上有重复的结果:一个实体 Module
和一个实体 Page
。一个Module
有一组页面,一个Page
属于模块
这是用Spring Boot with Spring Data JPA and Spring Data Rest设置的。
完整代码可在 GitHub
上访问
实体
这是实体的代码。为简洁起见删除了大多数设置器:
Module.java
@Entity
@Table(name = "dt_module")
public class Module {
private Long id;
private String label;
private String displayName;
private Set<Page> pages;
@Id
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
public String getDisplayName() {
return displayName;
}
@OneToMany(mappedBy = "module")
public Set<Page> getPages() {
return pages;
}
public void addPage(Page page) {
if (pages == null) {
pages = new HashSet<>();
}
pages.add(page);
if (page.getModule() != this) {
page.setModule(this);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Module module = (Module) o;
return Objects.equals(label, module.label) && Objects.equals(displayName, module.displayName);
}
@Override
public int hashCode() {
return Objects.hash(label, displayName);
}
}
Page.java
@Entity
@Table(name = "dt_page")
public class Page {
private Long id;
private String name;
private String action;
private String description;
private Module module;
@Id
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getAction() {
return action;
}
public String getDescription() {
return description;
}
@ManyToOne
public Module getModule() {
return module;
}
public void setModule(Module module) {
this.module = module;
this.module.addPage(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Page page = (Page) o;
return Objects.equals(name, page.name) &&
Objects.equals(action, page.action) &&
Objects.equals(description, page.description) &&
Objects.equals(module, page.module);
}
@Override
public int hashCode() {
return Objects.hash(name, action, description, module);
}
}
存储库
现在是 Spring 存储库的代码,相当简单:
ModuleRepository.java
@RepositoryRestResource(collectionResourceRel = "module", path = "module")
public interface ModuleRepository extends PagingAndSortingRepository<Module, Long> {
}
PageRepository.java
@RepositoryRestResource(collectionResourceRel = "page", path = "page")
public interface PageRepository extends PagingAndSortingRepository<Page, Long> {
}
配置
配置来自2个文件:
Application.java
@EnableJpaRepositories
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.properties
spring.jpa.database = H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.data.rest.basePath=/api
数据库
最后是数据库模式和一些测试数据:
schema.sql
drop table if exists dt_page;
drop table if exists dt_module;
create table DT_MODULE (
id IDENTITY primary key,
label varchar(30) not NULL,
display_name varchar(40) not NULL
);
create table DT_PAGE (
id IDENTITY primary key,
name varchar(50) not null,
action varchar(50) not null,
description varchar(255),
module_id bigint not null REFERENCES dt_module(id)
);
data.sql
INSERT INTO DT_MODULE (label, display_name) VALUES ('mod1', 'Module 1'), ('mod2', 'Module 2'), ('mod3', 'Module 3');
INSERT INTO DT_PAGE (name, action, description, module_id) VALUES ('page1', 'action1', 'desc1', 1);
就是这样。现在,我运行因此从命令行启动应用程序:mvn spring-boot:run
。应用程序启动后,我可以像这样查询它的主要端点:
获取 API
$ curl http://localhost:8080/api
回复
{
"_links" : {
"page" : {
"href" : "http://localhost:8080/api/page{?page,size,sort}",
"templated" : true
},
"module" : {
"href" : "http://localhost:8080/api/module{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://localhost:8080/api/alps"
}
}
}
获取所有模块
curl http://localhost:8080/api/module
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module"
}
},
"_embedded" : {
"module" : [ {
"label" : "mod1",
"displayName" : "Module 1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1"
},
"pages" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
}
}, {
"label" : "mod2",
"displayName" : "Module 2",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/2"
},
"pages" : {
"href" : "http://localhost:8080/api/module/2/pages"
}
}
}, {
"label" : "mod3",
"displayName" : "Module 3",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/3"
},
"pages" : {
"href" : "http://localhost:8080/api/module/3/pages"
}
}
} ]
},
"page" : {
"size" : 20,
"totalElements" : 3,
"totalPages" : 1,
"number" : 0
}
}
获取一个模块的所有页面
curl http://localhost:8080/api/module/1/pages
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
}, {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
如您所见,我在这里两次访问相同的页面。怎么回事?
奖金问题:为什么这有效?
我正在清理提交此问题的代码,为了使其更紧凑,我将 Page
实体上的 JPA Annotations 移动到字段级别,如下所示:
Page.java
@Entity
@Table(name = "dt_page")
public class Page {
@Id
private Long id;
private String name;
private String action;
private String description;
@ManyToOne
private Module module;
...
其余class保持不变。这可以在分支 field-level.
上的同一个 github 回购协议中看到
事实证明,执行相同的请求并更改 API 将呈现预期的结果(在以与之前相同的方式启动服务器之后):
获取一个模块的所有页面
curl http://localhost:8080/api/module/1/pages
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
这导致了您的问题(页面实体):
public void setModule(Module module) {
this.module = module;
this.module.addPage(this); //this line right here
}
Hibernate 使用您的 setter 来初始化实体,因为您将 JPA 注释放在了 getter 上。
导致问题的初始化序列:
- 已创建模块对象
- 设置模块属性(页面集已初始化)
- 已创建页面对象
- 将创建的页面添加到 Module.pages
- 设置页面属性
- setModule 在 Page 对象上调用,这会将当前页面添加 (addPage) 到 Module.pages 第二次
您可以将 JPA 注释放在字段上,它会起作用,因为在初始化期间不会调用 setter(奖励问题)。
我遇到了这个问题,我刚刚将 fetch=FetchType.EAGER
更改为 fetch=FetchType.LAZY
这解决了我的问题!
我在这个简单模型的集合上有重复的结果:一个实体 Module
和一个实体 Page
。一个Module
有一组页面,一个Page
属于模块
这是用Spring Boot with Spring Data JPA and Spring Data Rest设置的。
完整代码可在 GitHub
上访问实体
这是实体的代码。为简洁起见删除了大多数设置器:
Module.java
@Entity
@Table(name = "dt_module")
public class Module {
private Long id;
private String label;
private String displayName;
private Set<Page> pages;
@Id
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
public String getDisplayName() {
return displayName;
}
@OneToMany(mappedBy = "module")
public Set<Page> getPages() {
return pages;
}
public void addPage(Page page) {
if (pages == null) {
pages = new HashSet<>();
}
pages.add(page);
if (page.getModule() != this) {
page.setModule(this);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Module module = (Module) o;
return Objects.equals(label, module.label) && Objects.equals(displayName, module.displayName);
}
@Override
public int hashCode() {
return Objects.hash(label, displayName);
}
}
Page.java
@Entity
@Table(name = "dt_page")
public class Page {
private Long id;
private String name;
private String action;
private String description;
private Module module;
@Id
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getAction() {
return action;
}
public String getDescription() {
return description;
}
@ManyToOne
public Module getModule() {
return module;
}
public void setModule(Module module) {
this.module = module;
this.module.addPage(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Page page = (Page) o;
return Objects.equals(name, page.name) &&
Objects.equals(action, page.action) &&
Objects.equals(description, page.description) &&
Objects.equals(module, page.module);
}
@Override
public int hashCode() {
return Objects.hash(name, action, description, module);
}
}
存储库
现在是 Spring 存储库的代码,相当简单:
ModuleRepository.java
@RepositoryRestResource(collectionResourceRel = "module", path = "module")
public interface ModuleRepository extends PagingAndSortingRepository<Module, Long> {
}
PageRepository.java
@RepositoryRestResource(collectionResourceRel = "page", path = "page")
public interface PageRepository extends PagingAndSortingRepository<Page, Long> {
}
配置
配置来自2个文件:
Application.java
@EnableJpaRepositories
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.properties
spring.jpa.database = H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.data.rest.basePath=/api
数据库
最后是数据库模式和一些测试数据:
schema.sql
drop table if exists dt_page;
drop table if exists dt_module;
create table DT_MODULE (
id IDENTITY primary key,
label varchar(30) not NULL,
display_name varchar(40) not NULL
);
create table DT_PAGE (
id IDENTITY primary key,
name varchar(50) not null,
action varchar(50) not null,
description varchar(255),
module_id bigint not null REFERENCES dt_module(id)
);
data.sql
INSERT INTO DT_MODULE (label, display_name) VALUES ('mod1', 'Module 1'), ('mod2', 'Module 2'), ('mod3', 'Module 3');
INSERT INTO DT_PAGE (name, action, description, module_id) VALUES ('page1', 'action1', 'desc1', 1);
就是这样。现在,我运行因此从命令行启动应用程序:mvn spring-boot:run
。应用程序启动后,我可以像这样查询它的主要端点:
$ curl http://localhost:8080/api
回复
{
"_links" : {
"page" : {
"href" : "http://localhost:8080/api/page{?page,size,sort}",
"templated" : true
},
"module" : {
"href" : "http://localhost:8080/api/module{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://localhost:8080/api/alps"
}
}
}
获取所有模块
curl http://localhost:8080/api/module
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module"
}
},
"_embedded" : {
"module" : [ {
"label" : "mod1",
"displayName" : "Module 1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1"
},
"pages" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
}
}, {
"label" : "mod2",
"displayName" : "Module 2",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/2"
},
"pages" : {
"href" : "http://localhost:8080/api/module/2/pages"
}
}
}, {
"label" : "mod3",
"displayName" : "Module 3",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/3"
},
"pages" : {
"href" : "http://localhost:8080/api/module/3/pages"
}
}
} ]
},
"page" : {
"size" : 20,
"totalElements" : 3,
"totalPages" : 1,
"number" : 0
}
}
获取一个模块的所有页面
curl http://localhost:8080/api/module/1/pages
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
}, {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
如您所见,我在这里两次访问相同的页面。怎么回事?
奖金问题:为什么这有效?
我正在清理提交此问题的代码,为了使其更紧凑,我将 Page
实体上的 JPA Annotations 移动到字段级别,如下所示:
Page.java
@Entity
@Table(name = "dt_page")
public class Page {
@Id
private Long id;
private String name;
private String action;
private String description;
@ManyToOne
private Module module;
...
其余class保持不变。这可以在分支 field-level.
上的同一个 github 回购协议中看到事实证明,执行相同的请求并更改 API 将呈现预期的结果(在以与之前相同的方式启动服务器之后):
获取一个模块的所有页面curl http://localhost:8080/api/module/1/pages
回复
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
这导致了您的问题(页面实体):
public void setModule(Module module) {
this.module = module;
this.module.addPage(this); //this line right here
}
Hibernate 使用您的 setter 来初始化实体,因为您将 JPA 注释放在了 getter 上。
导致问题的初始化序列:
- 已创建模块对象
- 设置模块属性(页面集已初始化)
- 已创建页面对象
- 将创建的页面添加到 Module.pages
- 设置页面属性
- setModule 在 Page 对象上调用,这会将当前页面添加 (addPage) 到 Module.pages 第二次
您可以将 JPA 注释放在字段上,它会起作用,因为在初始化期间不会调用 setter(奖励问题)。
我遇到了这个问题,我刚刚将 fetch=FetchType.EAGER
更改为 fetch=FetchType.LAZY
这解决了我的问题!