The Magic of Spring Data
Let's take a look at how we can get the REST API to access our data without writing a single line of code by using Spring Data technology.
Join the DZone community and get the full member experience.
Join For FreeEnterprise applications deal with data processing and enterprise technology providers strive to make such a processing as easy as possible. Probably, one of the most advanced approaches was taken by Spring Data project. This project is actually comprised of several subprojects, most of which are focused on data persistence with various databases. The most interesting feature of Spring Data is the ability to automatically create repositories, based on a repository specification interface. We need just to define our functional interface and Spring Data engine generates all the data access stuff for us. Even more, with Spring Data REST, we can also get the REST API to access our data, without writing a line of code! It sounds like a magic, so let's check what kind of magic we can get with Spring Data technology.
1. Out-Of-Box CRUD
Now we are going to discuss Spring Data JPA project, which provides persistence support for relational databases. Let's use the collection domain model from this article:
Listing 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
. . .
}
Listing 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
. . .
}
We create a new Spring Boot Starter project using Maven as the build tool. Our project pom.xml file can look like the following:
<?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.3.1.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</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>
Then we add the Spring Data JPA dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
This dependency not only adds Spring Data JPA packages but also transitively includes Hibernate as the JPA implementation. Then we declare the following interface to manage collection items:
Listing 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> {
}
And now we can see the magic. We can populate our database with test data by providing the following SQL scripts with predefined names, schema.sql for the database structure and data.sql for the data to be inserted into the tables:
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');
Also, to make Spring Data engine using our schema script, we need to switch off default Hibernate DDL settings in application.properties: spring.jpa.hibernate.ddl-auto=none.
Now let's run this simple test:
Listing 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());
}
}
Annotation @SpringBootTest creates Spring Boot environment for running our test. In the test method, we call findAll() method, which was inherited by our repository from its parent interface, CrudRepository. We didn't provide any implementation for this method, but it returns the data we expect! Actually, Spring Data engine provides implementations for each method declared in CrudRepository interface. It looks great but we can get even more! Let's see how we can develop REST API for our repository with the usage of Spring Data REST.
2. Out-Of-Box REST API
First of all, we need to add the corresponding dependency to our pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
Believe or not, it is the only change we need to enable CollectionItem REST endpoint in our application! It sounds too good to be true, so let's check the stuff now. The simplest way to test Spring Boot Web application is probably to use Spring test support classes. So, we can write the following test:
Listing 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"));
}
}
New stuff here is @AutoConfigureMockMvc annotation, which initializes and autowires an instance of MockMvc test utility class with default settings. Actually, MockMvc class is a REST client, which enables sending REST requests from inside a unit test. If everything is OK, the test should complete successfully. Now we can see that our endpoint works without any line of specific code have been written by us!
It is really great to get basic CRUD endpoint out-of-box, but usually, we need more functionality to be exposed for our clients. Spring Data addresses this problem by providing various possibilities for data access customization and enhancement.
3. Ways of Customization
3.1. Query Methods
The simplest, but quite powerful option is to add customized search methods to our repository. It is about just adding method signatures to the repository interface. Based on the method signature and name, Spring Data will generate the implementation. There is a comprehensive set of method naming rules, following which we can define filtering and sorting parameters for retrieving domain objects. For example, if we want Spring Data to filter collection items by country, we add the following method to CollectionItemRepository interface:
@CrossOrigin(origins="*") public interface CollectionItemRepository extends PagingAndSortingRepository<CollectionItem, Long>, JpaSpecificationExecutor<CollectionItem> { public List<CollectionItem> findByCountry(@Param("cntry") String country); }
Note that here we use @CrossOrigin annotation for permitting cross-origin requests. For every query method declared in the repository, Spring Data REST exposes a query method resource and we need @Param annotation to define the GET query parameter.
Provided having REST API enabled, we can retrieve collection items filtered by country with the following URL:
http://localhost:8080/collectionItems/search/findByCountry?cntry=us
Note the search part of the URL. By default, Spring Data REST puts all query methods under the search path. That is, the path will be like "/search/<your_query_method_name>". We can customize the endpoint with the usage of @RestResource annotation. So, if we modify our query method as follows:
@RestResource(path = "byCountry", rel = "byCountry")
public List<CollectionItem> findByCountry(@Param("cntry") String country);
Then we will be able to access the endpoint method with the following URL:
http://localhost:8080/collectionItems/search/byCountry?cntry=uk
NOTE:
Resource path ".../search" displays all exposed custom query methods. So, we can check our query method this way, for example:
curl localhost:8080/collectionItems/search
{
"_links": {
"byCountry": {
"href": "http://localhost:8080/collectionItems/search/byCountry{?cntry}",
"templated": true
},
"self": {
"href": "http://localhost:8080/collectionItems/search"
}
}
}
If we need more sophisticated filtering, which is not possible to achieve with combination of filtering parameters in the method name, we can use all the power of JPA query language with @Query annotation:
@Query("SELECT ci FROM CollectionItem ci WHERE ci.country = :cntry")
public List<CollectionItem> findByCountryQuery(@Param("cntry") String country);
Ability to declare custom queries for repository methods is a powerful feature, but, sometimes, we need to create queries dynamically, based on the filtering parameters provided by the client. Criteria API can be a good choice for this case and Spring Data supports the usage of Criteria API in repositories by means of specifications (see the reference documentation for details).
3.2. Specifications and Custom Endpoints
To use specifications in our repository, we need to extend your repository interface with the JpaSpecificationExecutor
interface. The additional interface carries methods that allow you to execute Specification
s in a variety of ways. For example, the findAll
method will return all entities that match the specification:
List<T> findAll(Specification<T> spec);
The Specification
interface is:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
Specifications can easily be used to build an extensible set of predicates on top of an entity that then can be combined and used with JpaRepository without the need to declare a query (method) for every needed combination. For example, if we want to provide a possibility for clients to filter collection items by country, year, and topics, in any combination of these parameters, we can define the following set of specifications:
Listing 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;
};
}
}
Here CollectionItem_ is a metamodel object, which is generated while building the project. To achieve this functionality, we can enhance the Maven plugin with the corresponding annotation processor:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> . . . <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <annotationProcessorPath> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>${hibernate.version}</version> </annotationProcessorPath> <path> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </path> <path> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.1</version> </path> </annotationProcessorPaths> </configuration> </plugin>
If we work with a Java version greater than 1.8, we need to add dependency for jaxb-api, as it is used by the annotation processor under the hood and is not a part of the standard Java library any more.
Now we can test the specifications and their combinations with the following test:
Listing 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());
}
}
Provided having our specifications working as expected, we need to make them accessible for customers. Spring Data REST doesn't automatically expose JpaSpecificationExecutor methods as REST endpoints, so we need to create a custom controller for exposing the methods:
Listing 3.3. SearchController.java - custom endpoint
package com.collections.control; import static com.collections.repository.CollectionItemSpecs.*; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.CrossOrigin; 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.entity.CollectionItem; import com.collections.repository.CollectionItemRepository; @CrossOrigin(origins="*") @RepositoryRestController @RequestMapping("/collectionItems") public class SearchController { @Autowired CollectionItemRepository collectionItemRepository; @Transactional @RequestMapping(method = RequestMethod.GET, value = "/search/byParams") @ResponseBody List<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); return filteredItems; } }
Here @RepositoryRestController annotation makes Spring Data REST to support our controller with Spring Data REST’s settings, message converters, exception handling and more. The endpoint method base mapping corresponds to that provided by Spring Data REST for the auto-generated stuff, that is "/collectionItems/search".
Pointing curl to the endpoint method, we get the following result:
curl "http://localhost:8080/collectionItems/search/byParams?cntry=us"
[ { "id" : 2, "price" : 1000000, "smallImage" : { "id" : 1, "content" : "MTIzNDU=" }, "image" : { "id" : 2, "content" : "MTIzNDU=" }, "name" : "Juke", "summary" : "The Juke stature", "description" : "Porcelain stature of Juke", "year" : 1996, "country" : "us", "topics" : [ "Programming", "Arts" ] }, { "id" : 3, "price" : 2000000, "smallImage" : { "id" : 5, "content" : "TEST5" }, "image" : { "id" : 6, "content" : "TEST6" }, "name" : "Juggling Juke", "summary" : "The Juggling Juke painting", "description" : "Post modernist oil painting of the juggling Juke", "year" : 2000, "country" : "us", "topics" : [ "Programming", "Arts" ] } ]
The response contains just the filtered data. While it is appropriate and sufficient in many cases, we can enhance the response with HATEOAS (Hypermedia As The Engine Of Application State) links and make our REST API self-describing and more friendly for customers. With the HATEOAS approach, resources returned from an API include links to related resources. To enable hypermedia in our application, we need to add the following dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
Spring Data REST enables HATEOAS links for all auto-generated endpoints, but we need to add this functionality to our custom endpoint manually, like it is shown in listing 3.4.
Listing 3.4. Custom search endpoint enhanced with hypermedia
package com.collections.control; import static com.collections.repository.CollectionItemSpecs.*; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.CrossOrigin; 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.entity.CollectionItem; import com.collections.repository.CollectionItemRepository; @CrossOrigin(origins="*") @RepositoryRestController @RequestMapping("/collectionItems") public class SearchHATEOASController { @Autowired CollectionItemRepository collectionItemRepository; @Transactional @RequestMapping(method = RequestMethod.GET, value = "/hateoassearch/byParams") @ResponseBody ResponseEntity<CollectionModel<EntityModel<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); List<EntityModel<CollectionItem>> itemResources = StreamSupport.stream(filteredItems.spliterator(), false) .map(item -> EntityModel.of(item, linkTo(SearchHATEOASController.class).slash(item.getId()).withSelfRel())) .collect(Collectors.toList()); return ResponseEntity.ok(CollectionModel.of( itemResources, linkTo(methodOn(SearchHATEOASController.class).searchByParams(country, year, topics)).withSelfRel())); } }
This time, we got HATEOAS-enabled response:
curl "http://localhost:8080/collectionItems/hateoassearch/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 modernist 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/hateoassearch/byParams?cntry=us{&year,topics}", "templated" : true } } }
NOTE: In real projects, you probably might want to create the central point for enhancing domain resources with hypermedia links. You could extend RepresentationModelAssemblerSupport class in this case.
Spring Data greatly simplifies retrieving domain objects from the underlying database, but often we don't use all the properties of a domain object, but only some of them. In this case, it would be more efficient to not retrieve whole object, but only the properties of interest, that is, a domain object projection (see also this article). Spring Data can help with it as well.
3.3. Exposing Projections
To use projections with Spring Data, we need to define an interface containing getters for the domain object properties we want to retrieve. For example, we want to display a list of collection items in a table on a web page. Each row should contain only basic data, that is, collection item name, summary, country, and the small image. For this case, we define the following interface:
Listing 3.5. CollectionListItem.java. Domain object projection
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();
}
When encountering an interface with @Projection annotation, Spring Data REST automatically generates the implementation and also adds projection reference to the endpoint link description, like this:
{
"_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"
}
}
}
We can get a list of collection item projections filtered by country, for example:
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"
}
}
}
Projections can also generate virtual data. For example, we can customize the summary data in this way:
@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();
}
Then the response data will contain the following fragments:
"name": "Juke",
. . .
"summary" : "The Juke stature. Created in 1996",
. . .
"name": "Juggling Juke",
. . .
"summary" : "The Juggling Juke painting. Created in 2000",
. . .
Spring Data and Spring Data REST provide a way to quickly add basic, commonly used data access functionality to Spring applications with a possibility to enhance and customize the data access according to the requirements specified for each particular project. This article is just a brief description of some features of these technologies. The source code for this article is available at https://github.com/spylypets/spring-data-magic.git. You can find more information on the Spring documentation pages, for example:
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/
Opinions expressed by DZone contributors are their own.
Comments