Getting duplicate items when querying a collection using Spring Data Rest

I have duplicate results in a collection with this simple model: Module entity and Page entity. A Module has a set of pages, and Page belongs to the module.

This is configured using Spring Boot with Spring JPA Data and Spring Saving Data .

Full code is available on GitHub

The objects

Here is the code for the objects. Most setters are removed for brevity:

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

Storage facilities

Now the code for the Spring repositories, which is pretty simple:

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> { } 

Config

The configuration comes from 2 files:

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 

Database

Finally, the db schema and some test data:

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

What about that. Now I run from the command line to start the application: mvn spring-boot:run . After starting the application, I can request its main endpoint as follows:

Get API
 $ curl http://localhost:8080/api 
response
 { "_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" } } } 
Get all modules
 curl http://localhost:8080/api/module 
response
 { "_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 } } 
Get all pages for one module
 curl http://localhost:8080/api/module/1/pages 
response
 { "_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" } } } ] } } 

So, as you can see, I get the same page twice. What's happening?

Bonus question: why does it work?

I cleaned up the code to post this question, and to make it more compact, I moved the JPA annotations to the Page object at the field level, for example:

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; ... 

The rest of the class remains the same. This can be seen on the same github registry on the field-level branch.

As it turned out, executing the same request with this API change will lead to the expected result (after starting the server the same as before):

Get all pages for one module
 curl http://localhost:8080/api/module/1/pages 
response
 { "_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" } } } ] } } 
+9
spring-boot spring-data-jpa spring-data-rest
source share
2 answers

This causes your problem (Page Entity):

  public void setModule(Module module) { this.module = module; this.module.addPage(this); //this line right here } 

Hibernate uses your setters to initialize the object because you put JPA annotations on getters.

The initialization sequence that causes the problem:

  • Module object created
  • Set module properties (page set initialized)
  • Page object created
  • Add Created Page to Module.pages
  • Set Page Properties
  • setModule is called on the page object, and this adds (addPage) the current page to Module.pages a second time

You can put JPA annotations in fields and they will work because setters will not be called during initialization (bonus question).

+3
source share

I had this problem and I just changed fetch=FetchType.EAGER to fetch=FetchType.LAZY

This solved my problem!

0
source share

All Articles