使用Spring Data Rest查询集合时获取重复项

时间:2015-07-02 05:45:16

标签: spring-boot spring-data-jpa spring-data-rest

我在使用这个简单模型的集合上有重复的结果:实体Module和实体PageModule有一组页面,Page属于该模块。

这是使用Spring Boot Spring Data JPASpring Data Rest设置的。

可以在GitHub

上访问完整代码

实体

这是实体的代码。为简洁起见,删除了大多数setter:

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

数据库

最后是db模式和一些测试数据:

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注释移动到字段级别,如下所示:

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

班上所有其他人都保持不变。这可以在分支field-level上的同一个github repo上看到。

事实证明,对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"
        }
      }
    } ]
  }
}

2 个答案:

答案 0 :(得分:3)

这导致您的问题(Page Entity):

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

Hibernate使用你的setter初始化实体,因为你把JPA注释放在getter上。

导致问题的初始化序列:

  1. 创建模块对象
  2. 设置模块属性(已初始化页面设置)
  3. 已创建网页对象
  4. 将创建的页面添加到Module.pages
  5. 设置页面属性
  6. 在Page对象上调用setModule,这会在第二次将当前页面添加(addPage)到Module.pages
  7. 你可以在字段上放置JPA注释,它会起作用,因为在初始化期间不会调用setter(奖金问题)。

答案 1 :(得分:0)

我遇到了这个问题,我刚刚将fetch=FetchType.EAGER更改为fetch=FetchType.LAZY

这解决了我的问题!