0. Intro
It's getting more and more easier to create rest services with spring boot : now It's possible to create them with just several annotations! Let's see :)1. Project structure
I created simple gradle project with some tests:2. Dependencies (build.gradle)
We need:- spring-data-jpa - to use auto-generated by Spring repositories
- sparing-data-rest - to expose these repositories by REST services
- lombok - to reduce code for entities classes
- h2 - as database
plugins { id 'org.springframework.boot' version '2.1.3.RELEASE' id 'java'} apply plugin: 'io.spring.dependency-management' group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-rest' runtimeOnly 'com.h2database:h2' compileOnly('org.projectlombok:lombok') testImplementation 'org.springframework.boot:spring-boot-starter-test'}
3. Main runner class
Nothing interesting is here - just spring boot runner:package com.demien.sprdata; import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplicationpublic class SprdataApplication { public static void main(String[] args) { final SpringApplication app = new SpringApplication(SprdataApplication.class); app.run(args); } }
4. Domain entities
I create only 2 entities: "parent" : UserGroup and "child": User.4.1. UserGroup entity
Thanks to lombok, it's very simple, we just have to list properties. Also we have to annotate it as "@Entity" and also I'm defining named query "UserGroup.namedQueryByName"| :package com.demien.sprdata.domain; import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.NamedQuery; import lombok.Getter;import lombok.Setter; @Entity@Getter@Setterpublic class UserGroup { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private String description; public UserGroup() { } }
4.2. User entity
It's little bit more complicated: we have to define relationship with parent: we will be joining by field groupId. And I'm also defining named query.package com.demien.sprdata.domain; import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;import javax.persistence.NamedQuery; import lombok.Getter;import lombok.Setter; @Getter@Setter@Entity@NamedQuery(name = "User.namedQueryByName", query = "SELECT u FROM User u WHERE u.name = :name ") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "groupId") private UserGroup group; private String name; private Integer salary; public User() { } }
5. Repositories
The magic is happening here. We just have to define interface ..... and that's it! We don't have to create the implementation - it will be created by Spring!!!5.1. UserGroup repository
I made it very simple: we're just extending CrudRepository and adding several methods."Crud" means Create, Update and Delete - so all these methods will be available in implementation which will be generated by Spring. And also 2 more methods added: findAll and count;
package com.demien.sprdata.repository; import org.springframework.data.repository.CrudRepository; import com.demien.sprdata.domain.UserGroup; public interface UserGroupRepository extends CrudRepository<UserGroup, Long> { Iterable<UserGroup> findAll(); long count(); }
5.2. UserRepository
It's more complicated. First of all, we're annotation it with @RepositoryRestResource to expose methods as rest services. Next - I'm using PagingAndSorting repository, so paging and sorting features will be available. Also I'm adding a lot of methods which will be generated by Spring:- we can use patter [find | count] By [fieldName].
- we can even use fields of parent entity (UserGroup which is accessed by "group" field in User entity)
- we can define our own queries
package com.demien.sprdata.repository; import java.util.List; import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.PagingAndSortingRepository;import org.springframework.data.repository.query.Param;import org.springframework.data.rest.core.annotation.RepositoryRestResource; import com.demien.sprdata.domain.User; //http://localhost:8080/users/@RepositoryRestResource(collectionResourceRel = "users", path = "users") public interface UserRepository extends PagingAndSortingRepository<User, Long> { Iterable<User> findAll(); long count(); List<User> findByName(String name); Long deleteByName(String name); Long countByGroupName(String groupName); // find by parent entity : Group List<User> findByGroupName(String name); // defining custom query @Query("SELECT u FROM User u WHERE u.name LIKE CONCAT('%', :name, '%') ") List<User> queryByName(@Param("name") String name); // using named query defined in entity class List<User> namedQueryByName(@Param("name") String name); @Query(value = "SELECT * FROM User WHERE name = :name ", nativeQuery = true) List<User> nativeQueryByName(@Param("name") String name); }
6. Testing
And now let's test how this magic works6.1 UserGroupRepository - test
Here I'm testing just CRUD operations:package com.demien.sprdata; import static org.junit.Assert.assertEquals;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue; import java.util.ArrayList;import java.util.List;import java.util.Optional; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.test.context.junit4.SpringRunner; import com.demien.sprdata.domain.UserGroup;import com.demien.sprdata.repository.UserGroupRepository; @DataJpaTest@RunWith(SpringRunner.class) public class UserGroupRepositoryTest { @Autowired private UserGroupRepository groupRepository; @Autowired private TestEntityManager em; @Test public void findAllTest() { final List<UserGroup> groups = new ArrayList<>(); groupRepository.findAll().forEach(group -> groups.add(group)); assertEquals(4, groups.size()); } @Test public void checkUserGroupCount() { assertEquals(4, groupRepository.count()); } @Test public void findOne() { Optional<UserGroup> opGroup = groupRepository.findById(1001L); assertTrue(opGroup.isPresent()); assertEquals("ADM", opGroup.get().getName()); groupRepository.deleteById(1001L); opGroup = groupRepository.findById(1001L); assertFalse(opGroup.isPresent()); } @Test public void createNewTest() { final UserGroup newGroup = new UserGroup(); newGroup.setDescription("Created"); groupRepository.save(newGroup); assertTrue(newGroup.getId() != null); em.flush(); final Optional<UserGroup> loaded = groupRepository.findById(newGroup.getId()); assertTrue(loaded.isPresent()); assertTrue(loaded.get().getDescription().equals("Created")); groupRepository.deleteById(newGroup.getId()); } }
6.2. UserRepository - test
And most interesting is happening here: I'm testing paging, sorting, named queries, native queries....package com.demien.sprdata; import static org.junit.Assert.assertTrue; import java.util.List; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Sort;import org.springframework.test.context.junit4.SpringRunner; import com.demien.sprdata.domain.User;import com.demien.sprdata.repository.UserRepository; @DataJpaTest@RunWith(SpringRunner.class) public class UserRepositoryTest { @Autowired UserRepository userRepository; @Autowired TestEntityManager em; @Test public void sortTest() { Sort sort = new Sort(Sort.Direction.ASC, "group_id").and(new Sort(Sort.Direction.DESC, "name")); Iterable<User> users = userRepository.findAll(sort); User first = users.iterator().next(); assertTrue(first.getGroup().getId() == 1001L); assertTrue(first.getName().equals("Victor")); } @Test public void pagingTest() { final PageRequest pageRequest = PageRequest.of(0, 2); final Page<User> userPage = userRepository.findAll(pageRequest); assertTrue(userPage.getNumberOfElements() == 2); assertTrue(userPage.getTotalPages() == 4); } @Test public void findTest() { List<User> users = userRepository.findByName("Joe"); assertTrue(users.size() == 1); assertTrue(users.get(0).getName().equals("Joe")); Long countByGroupName = userRepository.countByGroupName("ADM"); assertTrue(countByGroupName == 3L); users = userRepository.findByGroupName("ADM"); assertTrue(users.size() == 3); users = userRepository.queryByName("a"); assertTrue(users.size() == 5); assertTrue(users.get(0).getName().contains("a")); users = userRepository.namedQueryByName("Charles"); assertTrue(users.size() == 1); assertTrue(users.get(0).getId() == 104L); users = userRepository.nativeQueryByName("Mario"); assertTrue(users.size() == 1); assertTrue(users.get(0).getId() == 105L); } }
7. Rest services
And final thing: let's now run our application runner and open in browser: http://localhost:8080/users/It should be something like :
{ "_embedded" : { "users" : [ { "name" : "Joe", "salary" : 100, "_links" : { "self" : { "href" : "http://localhost:8080/users/101" }, "user" : { "href" : "http://localhost:8080/users/101" }, "group" : { "href" : "http://localhost:8080/users/101/group" } } }, { "name" : "Huan", "salary" : 200, "_links" : { "self" : { "href" : "http://localhost:8080/users/102" }, "user" : { "href" : "http://localhost:8080/users/102" }, "group" : { "href" : "http://localhost:8080/users/102/group" } } }, { "name" : "Michael", "salary" : 300, "_links" : { "self" : { "href" : "http://localhost:8080/users/103" }, "user" : { "href" : "http://localhost:8080/users/103" }, "group" : { "href" : "http://localhost:8080/users/103/group" } } }, { "name" : "Charles", "salary" : 100, "_links" : { "self" : { "href" : "http://localhost:8080/users/104" }, "user" : { "href" : "http://localhost:8080/users/104" }, "group" : { "href" : "http://localhost:8080/users/104/group" } } }, { "name" : "Mario", "salary" : 200, "_links" : { "self" : { "href" : "http://localhost:8080/users/105" }, "user" : { "href" : "http://localhost:8080/users/105" }, "group" : { "href" : "http://localhost:8080/users/105/group" } } }, { "name" : "Jan", "salary" : 300, "_links" : { "self" : { "href" : "http://localhost:8080/users/106" }, "user" : { "href" : "http://localhost:8080/users/106" }, "group" : { "href" : "http://localhost:8080/users/106/group" } } }, { "name" : "Victor", "salary" : 500, "_links" : { "self" : { "href" : "http://localhost:8080/users/107" }, "user" : { "href" : "http://localhost:8080/users/107" }, "group" : { "href" : "http://localhost:8080/users/107/group" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/users{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/profile/users" }, "search" : { "href" : "http://localhost:8080/users/search" } }, "page" : { "size" : 20, "totalElements" : 7, "totalPages" : 1, "number" : 0 } }