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 works
6.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
}
}
- HATEOAS is in place !
8. The end
As I mentioned at the beginning, with spring-boot stack we can create rest services with DB repositories by just adding few annotations! Full source code can be downloaded from
here.