企业应用程序处理数据处理,企业技术提供商努力使这样的处理尽可能容易。Spring Data项目可能是处理这类问题最先进的方法之一。该项目实际上由几个子项目组成,其中大多数子项目侧重于各种数据库的数据持久性。Spring Data最有趣的特性是能够基于存储库规范界面自动创建存储库。我们只需要定义我们的功能接口,Spring Data引擎就可以为我们生成所有的数据访问内容。更重要的是,使用Spring Data REST,我们还可以获得REST API来访问我们的数据,而无需编写一行代码!这听起来很神奇,所以让我们来看看Spring Data技术可以带来什么样的魔力。

1.开箱即用的CRUD

现在我们将讨论Spring Data JPA项目,该项目为关系数据库提供持久性支持。让我们用这篇文章收集的域模型:

清单1.1。CollectionItem.java

package com.collections.entity;
import java.math.BigDecimal;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name="COLLECTION_ITEMS")
public class CollectionItem {
  @Id
  @SequenceGenerator(name = "CollectionItems_Generator", sequenceName = "CollectionItems_seq", allocationSize = 1)
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CollectionItems_Generator")
  long id;
  BigDecimal price;
  @OneToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval=true)
  @JoinColumn(name="small_image", unique=true)
  Image smallImage;
  @OneToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval=true)
  @JoinColumn(name="image", unique=true)
  Image image;
  String name;
  String summary;
  @Lob
  String description;
  Short year;
  String country;
  @ElementCollection
  @CollectionTable(name="ITEMS_TOPICS",
  joinColumns=@JoinColumn(name="ITEM_ID"))
  @Column(name="TOPIC")
  Set<String> topics;
Getters and setters
. . .
}

清单1.2。Image.java

package com.collections.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name="IMAGES")
public class Image implements java.io.Serializable {
private static final long serialVersionUID = 1L;
  @Id
  @SequenceGenerator(name = "Images_Generator", sequenceName = "Images_seq", allocationSize = 1)
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Images_Generator")
  private long id;
  @Lob
  private String content;
Getters and setters
. . .
}

我们使用Maven作为构建工具创建一个新的Spring Boot Starter项目。我们的项目pom.xml文件可能如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.collections</groupId>
  <artifactId>SpringDataMagic</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>SpringDataMagic</name>
  <description>Magic of Spring Data article example code</description>
  <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.3.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>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

然后我们添加Spring Data JPA依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

这种依赖不仅添加了Spring Data JPA包,而且还传递了Hibernate作为JPA实现。然后我们声明以下界面来管理集合项:

清单1.3。CollectionItemRepository.java

package com.collections.repository;
import org.springframework.data.repository.CrudRepository;
import com.collections.entity.CollectionItem;

public interface CollectionItemRepository extends CrudRepository<CollectionItem, Long>  {
}

而现在我们可以看到它的魔力。我们可以通过提供预定义的名称,下面的SQL脚本,来填充我们的测试数据数据库,其中schema.sql文件代表数据库结构,data.sql的数据会被插入到表:

schema.sql

create sequence CollectionItems_seq
increment by 1
start with 1
 nomaxvalue
nocycle
 nocache;
create sequence Images_seq
increment by 1
start with 1
 nomaxvalue
nocycle
 nocache;
create table COLLECTION_ITEMS (
        id bigint not null,
        name varchar2(255),
summary varchar2(1000),
description CLOB,
 country varchar2(2),
year smallint,
price decimal,
small_image bigint,
image bigint,
        primary key (id)
);
create table IMAGES (
        id bigint not null,
content CLOB,
        primary key (id)
);
create table ITEMS_TOPICS (
item_id bigint,
topic varchar2(100),
 primary key (item_id, topic)
);

data.sql

delete from IMAGES;

insert into IMAGES(id, content) values(1, 'MTIzNDU=');
insert into IMAGES(id, content) values(2, 'MTIzNDU=');
insert into IMAGES(id, content) values(3, 'TEST');
insert into IMAGES(id, content) values(4, 'TEST4');
insert into IMAGES(id, content) values(5, 'TEST5');
insert into IMAGES(id, content) values(6, 'TEST6');

delete from COLLECTION_ITEMS;

insert into COLLECTION_ITEMS (id, name, summary, description, year, country, price, small_image, image) values(1, 'The Penny Black', 'The very first stamp', 'The very first post stamp but suprisely not the most expensive one', 1840, 'uk', 1000, 3, 4);

insert into COLLECTION_ITEMS (id, name, summary, description, year, country, price, small_image, image) values(2, 'Juke', 'The Juke stature', 'Porcelain stature of Juke', 1996, 'us', 1000000, 1, 2);

insert into COLLECTION_ITEMS (id, name, summary, description, year, country, price, small_image, image) values(3, 'Juggling Juke', 'The Juggling Juke painting', 'Post modernist oil painting of the juggling Juke', 2000, 'us', 2000000, 5, 6);

delete from ITEMS_TOPICS;

insert into ITEMS_TOPICS (item_id, topic) values(1, 'History');
insert into ITEMS_TOPICS (item_id, topic) values(2, 'Arts');
insert into ITEMS_TOPICS (item_id, topic) values(2, 'Programming');
insert into ITEMS_TOPICS (item_id, topic) values(3, 'Arts');
insert into ITEMS_TOPICS (item_id, topic) values(3, 'Programming');

另外,要使用我们的schema脚本制作Spring Data引擎,我们需要在application.properties中关闭默认的Hibernate DDL设置:spring.jpa.hibernate.ddl-auto = none

现在让我们运行这个简单的测试:

清单1.4。CollectionItemRepositoryTest.java

package com.collections.repository;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import com.collections.entity.CollectionItem;
import com.collections.repository.CollectionItemRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CollectionItemRepositoryTest {

  @Autowired
  CollectionItemRepository collectionItemRepository;
  
  @Test
  @Transactional
  public void testFindAll() {
    Iterable<CollectionItem> itemData = collectionItemRepository.findAll();
    List<CollectionItem> itemList = new ArrayList<>();
    itemData.forEach(itemList::add);
    Assert.assertEquals(3, itemList.size());
    CollectionItem item = itemList.get(0);
    Assert.assertEquals(1, item.getId());
    Assert.assertEquals("The Penny Black", item.getName());
    Assert.assertEquals("The very first stamp", item.getSummary());
    Assert.assertEquals(BigDecimal.valueOf(1000), item.getPrice());
    Assert.assertEquals("uk", item.getCountry());
    Assert.assertEquals(Short.valueOf((short) 1840), item.getYear());
  }
}

注释@SpringBootTest创建Spring Boot环境以运行我们的测试。在测试方法中,我们调用findAll()方法,该方法由我们的存储库从其父接口CrudRepository继承。我们没有为此方法提供任何实现,但它返回我们期望的数据!实际上,Spring Data引擎为CrudRepository接口中声明的每个方法提供了实现。它看起来很棒,但我们可以得到更多!让我们看看如何使用Spring Data REST为我们的存储库开发REST API。

2.开箱即用的REST API

首先,我们需要在pom.xml中添加相应的依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

信不信由你,这是我们在应用程序中启用CollectionItem REST所需的唯一更改!这听起来好得令人难以置信,所以让我们现在检查一下。测试Spring Boot Web应用程序的最简单方法可能是使用Spring测试支持类。所以,我们可以编写以下测试:

清单2.1。CollectionItemsEndpointTest.java

package com.collections;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import com.collections.repository.CollectionItemRepository;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CollectionItemsEndpointTest {
  @Autowired
  CollectionItemRepository collectionItemRepository;
  
  @Autowired
  private MockMvc mockMvc;
  
  @Test
  public void testFindAll() throws Exception {
    mockMvc.perform(get("/collectionItems"))
     .andExpect(status().isOk())
     .andExpect(jsonPath("$._embedded.collectionItems[0]").exists())
     .andExpect(jsonPath("$._embedded.collectionItems[0].name").value("The Penny Black"))
     .andExpect(jsonPath("$._embedded.collectionItems[1]").exists())
     .andExpect(jsonPath("$._embedded.collectionItems[1].name").value("Juke"))
     .andExpect(jsonPath("$._embedded.collectionItems[2]").exists())
     .andExpect(jsonPath("$._embedded.collectionItems[2].name").value("Juggling Juke"));
  }
}

这里的新东西是@AutoConfigureMockMvc注释,它使用默认设置初始化并自动装配MockMvc测试实用程序类的实例。实际上,MockMvc类是一个REST客户端,它允许从单元测试内部发送REST请求。如果一切正常,测试应该成功完成。现在我们可以看到我们的端点在没有任何特定代码行的情况下工作了!

获得基本的CRUD端点非常棒,但通常我们需要为客户公开更多功能。Spring Data通过为数据访问定制和增强提供各种可能性来解决此问题。

3.定制方式

3.1。查询方法

最简单但功能最强大的选项是向我们的存储库添加自定义搜索方法。它只是将方法签名添加到存储库接口。基于方法签名和名称,Spring Data将生成实现。有一个全面的方法的命名规则,以下我们可以定义过滤和排序参数用于检索域对象。例如,如果我们希望Spring Data按国家/地区过滤集合项,我们将以下方法添加到CollectionItemRepository接口:

List < CollectionItem > findByCountry(@Param(“cntry”)String country);

对于在存储库中声明的每个查询方法,Spring Data REST公开了一个查询方法资源,我们需要@Param注释来定义GET查询参数。

如果启用了REST API,我们可以使用以下URL检索按国家/地区筛选的集合项:

http:// localhost:8080 / collectionItems / search / findByCountry?cntry = us

请注意URL的搜索部分。默认情况下,Spring Data REST将所有查询方法放在搜索路径下。也就是说,路径将类似于“ / search / <your_query_method_name> ”。我们可以使用@RestResource注释自定义端点。所以,如果我们修改我们的查询方法如下:

@RestResource(path =“byCountry”,rel =“byCountry”)
public List < CollectionItem > findByCountry(@Param(“cntry”)String country);

然后我们将能够使用以下URL访问端点方法:

http://localhost:8080/collectionItems/search/byCountry?cntry=uk

注意:

资源路径“... / search”显示所有公开的自定义查询方法。所以,我们可以通过这种方式检查我们的查询方法,例如:

curl localhost:8080 / collectionItems / search

{
  "_links": {
    "byCountry": {
      "href": "http://localhost:8080/collectionItems/search/byCountry{?cntry}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/collectionItems/search"
    }
  }
}

如果我们需要更多的软化过滤,在方法名称中使用过滤参数的组合是不可能实现的,我们可以使用JPA查询语言的所有功能和@Query注释:

@Query("SELECT ci FROM CollectionItem ci WHERE ci.country = :cntry")
public List<CollectionItem> findByCountryQuery(@Param("cntry") String country);

声明存储库方法的自定义查询的能力是一个强大的功能,但有时,我们需要根据客户端提供的过滤参数动态创建查询。Criteria API对于这种情况可能是一个不错的选择,Spring Data支持通过规范在存储库中使用Criteria API(有关详细信息,请参阅参考文档)。

3.2。规格和自定义端点

要在我们的存储库中使用规范,我们需要使用该JpaSpecificationExecutor接口扩展存储库接口。附加接口带有允许您以Specification各种方式执行s的方法。例如,该findAll方法将返回与规范匹配的所有实体:

List<T> findAll(Specification<T> spec);

Specification接口是:

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}

可以轻松地使用规范在实体之上构建可扩展的谓词集,然后可以将其与JpaRepository结合使用,而无需为每个所需组合声明查询(方法)。例如,如果我们希望客户端可以按国家,年份和主题过滤收集项目,则可以在这些参数的任意组合中定义以下规范集:

清单3.1。CollectionItemSpecs.java

package com.collections.repository;

import javax.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;
import com.collections.entity.CollectionItem;
import com.collections.entity.CollectionItem_;

public class CollectionItemSpecs {
  public static Specification<CollectionItem> filterByCountry(String country) {
    return (root, query, cb) -> {
      return cb.equal(root.get(CollectionItem_.country), country);
    };
  }
  
  public static Specification<CollectionItem> filterByYear(Short year) {
    return (root, query, cb) -> {
      return cb.equal(root.get(CollectionItem_.year), year);
    };
  }
  
  public static Specification<CollectionItem> filterByTopics(String ... topics) {
    return (root, query, cb) -> {
      Predicate topicFilter = cb.conjunction();
      for(String topic : topics) {
        topicFilter = cb.and(topicFilter, cb.isMember(topic, root.get(CollectionItem_.topics)));
      }
      return topicFilter;
    };
  }
}

这里CollectionItem_是一个元模型对象,它是在构建项目时生成的。例如,您可以使用以下Maven插件:

<plugin>
  <groupId>org.bsc.maven</groupId>
  <artifactId>maven-processor-plugin</artifactId>
  <version>3.3.2</version>
  <executions>
    <execution>
      <id>process</id>
      <goals>
        <goal>process</goal>
      </goals>
      <phase>generate-sources</phase>
      <configuration>
        <outputDirectory>${project.build.directory}/generated-sources/metamodel</outputDirectory>
        <processors>
          <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
        </processors>
      </configuration>
    </execution>
  </executions>
  <dependencies>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-jpamodelgen</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
  </dependencies>
</plugin>

我们可以通过以下测试来测试规格及其组合:

清单3.2。CollectionItemSpecsTest.java

package com.collections.repository;

import static com.collections.repository.CollectionItemSpecs.*;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.collections.Topics;
import com.collections.entity.CollectionItem;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CollectionItemSpecsTest {

  @Autowired
  CollectionItemRepository collectionItemRepository;
  
  @Test
  public void testFilterByCountrySpec() {
    List<CollectionItem> resList = collectionItemRepository.findAll(filterByCountry("uk"));
    Assert.assertEquals(1, resList.size());
    Assert.assertEquals("The Penny Black", resList.get(0).getName());
  }
  
  @Test
  public void testFilterByYearSpec() {
    List<CollectionItem> resList = collectionItemRepository.findAll(filterByYear((short) 1840));
    Assert.assertEquals(1, resList.size());
    Assert.assertEquals("The Penny Black", resList.get(0).getName());
  }
  
  @Test
  public void testFilterByTopicsSpec() {
    List<CollectionItem> resList = collectionItemRepository.findAll(filterByTopics(Topics.ARTS.getName(), Topics.PROGRAMMING.getName()));
    Assert.assertEquals(2, resList.size());
    Assert.assertEquals("Juke", resList.get(0).getName());
  }
  
  @Test
  public void testFilterByCountryYearTopics() {
    List<CollectionItem> resList = collectionItemRepository.findAll(filterByCountry("us").and(filterByYear((short) 2000)).and(filterByTopics(Topics.ARTS.getName(), Topics.PROGRAMMING.getName())));
    Assert.assertEquals(1, resList.size());
    Assert.assertEquals("Juggling Juke", resList.get(0).getName());
  }
}

如果我们的规格按预期工作,我们需要让客户可以访问它们。Spring Data REST不会自动将JpaSpecificationExecutor方法公开为REST端点,因此我们需要创建一个自定义控制器来公开这些方法:

清单3.3。SearchController.java - 自定义端点

package com.collections.control;

import static com.collections.repository.CollectionItemSpecs.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.collections.BasePathAwareLinks;
import com.collections.entity.CollectionItem;
import com.collections.repository.CollectionItemRepository;

@RepositoryRestController
@RequestMapping("/collectionItems")
public class SearchController {

  @Autowired
  CollectionItemRepository collectionItemRepository;
  
  @Transactional
  @RequestMapping(method = RequestMethod.GET, value = "/search/byParams") 
  @ResponseBody ResponseEntity<Resources<Resource<CollectionItem>>> searchByParams(@RequestParam(name="cntry", required=false) String country, @RequestParam(required=false) Short year, @RequestParam(required=false) String ... topics) {
    Specification<CollectionItem> byParamsSpec = null;
    if (country != null) {
      byParamsSpec = filterByCountry(country);
    }
    if (year != null) {
      byParamsSpec = byParamsSpec != null ? byParamsSpec.and(filterByYear(year))  : filterByYear(year);
    }
    if (topics != null) {
      byParamsSpec = byParamsSpec != null ? byParamsSpec.and(filterByTopics(topics))  : filterByTopics(topics);
    }
    List<CollectionItem> filteredItems = collectionItemRepository.findAll(byParamsSpec);
    Resources<Resource<CollectionItem>> itemResources = convertToResources(filteredItems);
    return new ResponseEntity<>(itemResources, HttpStatus.OK);
  }
  
  private Resources<Resource<CollectionItem>> convertToResources(List<CollectionItem> items) {
    Resources<Resource<CollectionItem>> itemResources = Resources.wrap(items);
    return itemResources; 
  }
}

这里@RepositoryRestController注释使Spring Data REST能够通过Spring Data REST的设置,消息转换器,异常处理等支持我们的控制器。端点方法基本映射对应于Spring Data REST为自动生成的东西提供的映射,即“/ collectionItems / search”。

将curl指向端点方法,我们得到以下结果:

curl“http:// localhost:8080 / collectionItems / search / byParams?cntry = us”

{
  "_embedded" : {
    "collectionItems" : [ {
      "price" : 1000000,
      "smallImage" : {
        "content" : "MTIzNDU="
      },
      "image" : {
        "content" : "MTIzNDU="
      },
      "name" : "Juke",
      "summary" : "The Juke stature",
      "description" : "Porcelain stature of Juke",
      "year" : 1996,
      "country" : "us",
      "topics" : [ "Programming", "Arts" ]
    }, {
      "price" : 2000000,
      "smallImage" : {
        "content" : "TEST5"
      },
      "image" : {
        "content" : "TEST6"
      },
      "name" : "Juggling Juke",
      "summary" : "The Juggling Juke painting",
      "description" : "Post modernistic oil painting of the juggling Juke",
      "year" : 2000,
      "country" : "us",
      "topics" : [ "Programming", "Arts" ]
    } ]
  }
}

响应仅包含过滤后的数据。虽然在许多情况下它是合适且充分的,但我们可以使用HATEOAS(超媒体作为应用程序状态引擎)链接来增强响应,并使我们的REST API自我描述并且对客户更友好。使用HATEOAS方法,从API返回的资源包括指向相关资源的链接。要在我们的应用程序中启用超媒体,我们需要添加以下依赖项:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

Spring Data REST为所有自动生成的端点启用HATEOAS链接,但我们需要手动将此功能添加到我们的自定义端点,如清单3.4所示。

清单3.4。使用超媒体增强自定义搜索端点

package com.collections.control;
import static com.collections.repository.CollectionItemSpecs.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.collections.BasePathAwareLinks;
import com.collections.entity.CollectionItem;
import com.collections.repository.CollectionItemRepository;

@RepositoryRestController
@RequestMapping("/collectionItems")
public class SearchController {

  @Autowired
  CollectionItemRepository collectionItemRepository;
  
  @Transactional
  @RequestMapping(method = RequestMethod.GET, value = "/search/byParams") 
  @ResponseBody ResponseEntity<Resources<Resource<CollectionItem>>> searchByParams(@RequestParam(name="cntry", required=false) String country, @RequestParam(required=false) Short year, @RequestParam(required=false) String ... topics) {
    Specification<CollectionItem> byParamsSpec = null;
    if (country != null) {
      byParamsSpec = filterByCountry(country);
    }
    if (year != null) {
      byParamsSpec = byParamsSpec != null ? byParamsSpec.and(filterByYear(year))  : filterByYear(year);
    }
    if (topics != null) {
      byParamsSpec = byParamsSpec != null ? byParamsSpec.and(filterByTopics(topics))  : filterByTopics(topics);
    }
    List<CollectionItem> filteredItems = collectionItemRepository.findAll(byParamsSpec);
    Resources<Resource<CollectionItem>> itemResources = convertToResources(filteredItems);
    Link templatedLink = linkTo(methodOn(SearchController.class).searchByParams(country, year, topics)).withSelfRel();
    itemResources.add(templatedLink);
    return new ResponseEntity<>(itemResources, HttpStatus.OK);
  }
  
  private Resources<Resource<CollectionItem>> convertToResources(List<CollectionItem> items) {
    Resources<Resource<CollectionItem>> itemResources = Resources.wrap(items);
    itemResources.forEach(r -> {r.add(linkTo(SearchController.class).slash(r.getContent().getId()).withSelfRel());});
    return itemResources; 
  }
}

这次,我们获得了启用HATEOAS的响应:

curl“http:// localhost:8080 / collectionItems / search / byParams?cntry = us”

{
  "_embedded" : {
    "collectionItems" : [ {
      "price" : 1000000,
      "smallImage" : {
        "content" : "MTIzNDU="
      },
      "image" : {
        "content" : "MTIzNDU="
      },
      "name" : "Juke",
      "summary" : "The Juke stature",
      "description" : "Porcelain stature of Juke",
      "year" : 1996,
      "country" : "us",
      "topics" : [ "Programming", "Arts" ],
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/collectionItems/2"
        }
      }
    }, {
      "price" : 2000000,
      "smallImage" : {
        "content" : "TEST5"
      },
      "image" : {
        "content" : "TEST6"
      },
      "name" : "Juggling Juke",
      "summary" : "The Juggling Juke painting",
      "description" : "Post modernistic oil painting of the juggling Juke",
      "year" : 2000,
      "country" : "us",
      "topics" : [ "Programming", "Arts" ],
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/collectionItems/3"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/collectionItems/search/byParams?cntry=us{&year,topics}",
      "templated" : true
    }
  }
}

注意: 在实际项目中,您可能希望创建使用超媒体链接增强域资源的中心点。在这种情况下,您可以扩展ResourceAssemblerSupport类。

Spring Data极大地简化了从底层数据库中检索域对象的过程,但通常我们不使用域对象的所有属性,而只使用其中的一些属性。在这种情况下,不检索整个对象更有效,而只检索感兴趣的属性,即域对象投影(另请参阅本文。Spring Data也可以提供帮助。

3.3。公开预测

要在Spring Data中使用投影,我们需要定义一个包含我们想要检索的域对象属性的getter的接口。例如,我们希望在网页上的表格中显示集合项列表。每行应仅包含基本数据,即集合项名称,摘要,国家/地区和小图像。对于这种情况,我们定义以下接口:

清单3.5。CollectionListItem.java。域对象投影

package com.collections.entity;
import org.springframework.data.rest.core.config.Projection;

@Projection(name = "collectionListItem", types = { CollectionItem.class })
public interface CollectionListItem {
  String getName();
  String getSummary();
  String getCountry();
  Short getYear();
  Image getSmallImage();
}

当遇到带有@Projection批注的接口时,Spring Data REST会自动生成实现,并且还会添加对端点链接描述的投影引用,如下所示:

{
  "_links" : {
    "findByCountryQuery" : {
      "href" : "http://localhost:8080/collectionItems/search/findByCountryQuery{?cntry,projection}",
      "templated" : true
    },
    "findByCountry" : {
      "href" : "http://localhost:8080/collectionItems/search/findByCountry{?cntry,projection}",
      "templated" : true
    },
    "self" : {
      "href" : "http://localhost:8080/collectionItems/search"
    }
  }
}

我们可以获得按国家/地区筛选的收集项目预测列表,例如:

curl“localhost:8080 / collectionItems / search / findByCountry?cntry = us&projection = collectionListItem”

{
  "_embedded": {
    "collectionItems": [
      {
        "name": "Juke",
        "country": "us",
        "year": 1996,
        "smallImage": {
          "content": "MTIzNDU="
        },
        "summary": "The Juke stature",
        "_links": {
          "self": {
            "href": "http://localhost:8080/collectionItems/2"
          },
          "collectionItem": {
            "href": "http://localhost:8080/collectionItems/2{?projection}",
            "templated": true
          }
        }
      },
      {
        "name": "Juggling Juke",
        "country": "us",
        "year": 2000,
        "smallImage": {
          "content": "TEST5"
        },
        "summary": "The Juggling Juke painting",
        "_links": {
          "self": {
            "href": "http://localhost:8080/collectionItems/3"
          },
          "collectionItem": {
            "href": "http://localhost:8080/collectionItems/3{?projection}",
            "templated": true
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/collectionItems/search/findByCountry?cntry=us&projection=collectionListItem"
    }
  }
}

预测也可以生成虚拟数据。例如,我们可以通过以下方式自定义摘要数据:

@Projection(name = "collectionListItem", types = { CollectionItem.class })
public interface CollectionListItem {
  String getName();
  @Value("#{target.summary}. Created in #{target.year}")
  String getSummary();
  String getCountry();
  Short getYear();
  Image getSmallImage();
}

然后响应数据将包含以下片段:

"name": "Juke",

. . .

"summary" : "The Juke stature. Created in 1996",

. . .

"name": "Juggling Juke",

. . .

"summary" : "The Juggling Juke painting. Created in 2000",

. . .

Spring Data和Spring Data REST提供了一种向Spring应用程序快速添加基本的常用数据访问功能的方法,可以根据为每个特定项目指定的要求来增强和自定义数据访问。本文只是对这些技术的一些功能的简要描述。您可以在Spring文档页面上找到更多信息,例如:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

https://docs.spring.io/spring-data/rest/docs/current/reference/html/

https://docs.spring.io/spring-hateoas/docs/current/reference/html/

https://spring.io/guides/gs/accessing-data-rest/

https://spring.io/guides/gs/rest-hateoas/

英文原文:https://dzone.com/articles/magic-of-spring-data