How to build a Spring + Hibernate + Rest application
1. Overview
This tutorial will provide an introduction to Spring Data JPA having a Spring project using Java 17, Hibernate 6, lombok, and Rest using Spring WebFlux, it will cover configuring the persistence layer. For a step-by-step introduction to setting up the Spring context using Java-based configuration and you can checkout the projection configuration and sources from our repository
2. JPA (Jakarta Persistence API)
Jakarta Persistence API (JPA; formerly Java Persistence API) is a Jakarta EE application programming interface specification that describes the management of relational data in enterprise Java applications, is the most well know Java object/relational mapping API, that uses Java Annotations and has been used in the industry for many years
3. Entity Class
Creating the JPA's entities, is one of the most important tasks on our implementation since this where the ORM (object/relational mapping) is happening, the class will map the tables and fields that exists in our database, lately since the transition from Java EE to Jakarta EE we have to use the new package "jakarta.persistence" instead of "java.persistence" or else entities will not be found by the normal spring entity scan (@EntityScan)
in the below example we use @Entity to define that en class will be an entity and by default this will be the name of the table and it can be overwritten by @Table, in order the map the fields of the table we use @Column, in this example (Only for Java) we use Lombok to avoid Java's boilerplate code of getter/setters which are a requirement for JPA
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "UserData")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="identifier")
private Long id;
@Column(name="fullName", length = 40, nullable = false)
private String fullName;
@Column(name="email", length = 50, unique = true, nullable = false)
private String email;
}
User.java
import jakarta.persistence.*
@Entity
@Table(name = "UserData")
class User (
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "identifier")
var id: Long? = null,
@Column(name="fullName", length = 40, nullable = false)
var fullName: String,
@Column(name="email", length = 50, unique = true, nullable = false)
var email: String
)
User.kt
import jakarta.persistence.{Column, Entity, GeneratedValue, Id, Table}
import scala.beans.BeanProperty
@Entity
@Table(name = "UserData")
class User (@BeanProperty
@Column(name = "fullName", length = 40, nullable = false)
var fullName: String,
@Column(name = "email", length = 50, nullable = false, unique = true)
@BeanProperty var email: String){
@Id
@GeneratedValue
@Column(name = "identifier")
var id: Long = _
def this() = this(null,null)
}
User.scala
4. Repository
The main advantage of Spring Data is avoiding the boilerplate code from JPA, specially the generating JPQL queries
by implementing one of the Repository interfaces, the repository will already have some basic CRUD methods (and queries) defined and implemented. To define more specific access methods, Spring Data JPA supports quite the following options:
- Simply define a new method in the interface
- Provide the actual JPQL query by using the @Query annotation
- Use the more advanced Specification and Querydsl support in Spring Data
- Define custom queries via JPA Named Queries
import com.the.sample.app.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository {
Optional findByEmail(String email);
}
UserRepository.java
import com.the.sample.app.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.*
@Repository
interface UserRepository : JpaRepository{
fun findByEmail(email: String?): Optional
}
UserRepository.kt
import com.the.sample.app.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
trait UserRepository extends JpaRepository[User,Long]{
def findByEmail(email: String): User
}
UserRepository.scala
5. Service
The service layer defines any business logic that needs to be implemented as well the transactional layer by using the @Transactional, any data access outside this layer is not permitted, ex a common issue is trying to access a collection that depends on a JPA releations on the integration layer like Rest or gRPC, this will trigger a JPA session error
import com.the.sample.app.model.User;
import com.the.sample.app.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
@AllArgsConstructor
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
@Override
public List findAll(int page, int pageSize) {
return userRepository.findAll(PageRequest.of(page,pageSize)).toList();
}
@Override
public Optional findById(Long id) {
return userRepository.findById(id);
}
@Override
public Optional findByEmail(String email) {
return userRepository.findByEmail(email);
}
@Override
public void save(User user) {
userRepository.save(user);
}
@Override
public void deleteById(Long id) {
userRepository.deleteById(id);
}
}
UserServiceImpl.java
import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
interface UserService {
fun findAll(page: Int, pageSize: Int): List
fun findById(id: Long): User?
fun findByEmail(email: String): User?
fun save(user: User)
fun deleteById(id: Long)
}
@Service
@Transactional
class UserServiceImpl(val userRepository: UserRepository) : UserService{
override fun findAll(page: Int, pageSize: Int): List {
return userRepository.findAll(PageRequest.of(page,pageSize)).toList()
}
override fun findById(id: Long): User? {
return userRepository.findById(id).orElse(null)
}
override fun findByEmail(email: String): User? {
return userRepository.findByEmail(email).orElse(null)
}
override fun save(user: User) {
userRepository.save(user)
}
override fun deleteById(id: Long) {
userRepository.deleteById(id)
}
}
UserService.kt
import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.Optional
trait UserService {
def findAll(page: Int, pageSize: Int): java.util.List[User]
def findById(id: Long): Optional[User]
def findByEmail(email: String): Optional[User]
def save(user: User): Unit
def deleteById(id: Long): Unit
}
@Service
@Transactional
class UserServiceImpl(userRepository: UserRepository) extends UserService {
override def findAll(page: Int, pageSize: Int): java.util.List[User] =
userRepository.findAll(PageRequest.of(page, pageSize)).toList
override def findById(id: Long): Optional[User] = userRepository.findById(id)
override def findByEmail(email: String): Optional[User] = userRepository.findByEmail(email)
override def save(user: User): Unit = userRepository.save(user)
override def deleteById(id: Long): Unit = userRepository.deleteById(id)
}
UserService.scala
6. Rest Controller
This is our integration layer where our microservice can be called by Rest JSON using Spring WebFlux using the new Spring Reactive Stack
import com.the.sample.app.model.User;
import com.the.sample.app.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
@GetMapping("/{id}")
public Mono getUserById(@PathVariable("id") Long id) {
return Mono.justOrEmpty(userService.findById(id));
}
@GetMapping("/{page}/{pageSize}")
public Flux getAllUsers(@PathVariable("page") Integer page,
@PathVariable("pageSize") Integer pageSize) {
return Flux.fromIterable(userService.findAll(page,pageSize));
}
@PostMapping("/")
public Mono saverUser(@RequestBody User user) {
userService.save(user);
return Mono.just(user);
}
@PutMapping("/{id}")
public Mono updateUser(@PathVariable("id") Long id,@RequestBody User user) {
user.setId(id);
userService.save(user);
return Mono.just(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable("id") Long id) {
userService.deleteById(id);
}
}
UserRestController.java
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/users")
class UserRestController(val userService: UserService) {
@GetMapping("/{id}")
fun getUserById(@PathVariable("id") id: Long?): Mono? {
return Mono.justOrEmpty(userService.findById(id!!))
}
@GetMapping("/{page}/{pageSize}")
fun getAllUsers(
@PathVariable("page") page: Int,
@PathVariable("pageSize") pageSize: Int
): Flux? {
return Flux.fromIterable(userService.findAll(page, pageSize))
}
@PostMapping("/")
fun saverUser(@RequestBody user: User): Mono? {
userService.save(user)
return Mono.just(user)
}
@PutMapping("/{id}")
fun updateUser(@PathVariable("id") id: Long, @RequestBody user: User): Mono? {
user.id = id
userService.save(user)
return Mono.just(user)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable("id") id: Long): Unit {
userService.deleteById(id)
}
}
UserRestController.kt
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.web.bind.annotation.{
DeleteMapping,
GetMapping,
PathVariable,
PostMapping,
PutMapping,
RequestBody,
RequestMapping,
RestController
}
import reactor.core.publisher.{ Flux, Mono }
@RestController
@RequestMapping(Array("/users"))
class UserRestController(userService: UserService) {
@GetMapping(Array("/{id}")) def getUserById(@PathVariable("id") id: Long): Mono[User] =
Mono.justOrEmpty(userService.findById(id))
@GetMapping(Array("/{page}/{pageSize}")) def getAllUsers(@PathVariable("page") page: Integer,
@PathVariable("pageSize") pageSize: Integer): Flux[User] =
Flux.fromIterable(userService.findAll(page, pageSize))
@PostMapping(Array("/")) def saverUser(@RequestBody user: User): Mono[User] = {
userService.save(user)
Mono.just(user)
}
@PutMapping(Array("/{id}")) def updateUser(@PathVariable("id") id: Long, @RequestBody user: User): Mono[User] = {
user.id = id
userService.save(user)
Mono.just(user)
}
@DeleteMapping(Array("/{id}")) def deleteUser(@PathVariable("id") id: Long): Unit =
userService.deleteById(id)
}
UserRestController.scala
7. Spring Boot Context Configuration
This is the main component that starts our Spring Context and our JPA session manager
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EntityScan(basePackages = "com.the.sample.app")
@EnableJpaRepositories(basePackages = {"com.the.sample.app"})
@EnableTransactionManagement
public class SpringHibernateCrudApplication {
public static void main(String[] args) {
SpringApplication.run(SpringHibernateCrudApplication.class, args);
}
}
SpringHibernateCrudApplication.java
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement
@SpringBootApplication
@EntityScan(basePackages = ["com.the.sample.app"])
@EnableJpaRepositories(basePackages = ["com.the.sample.app"])
@EnableTransactionManagement
class SpringHibernateCrudApplication
fun main(args: Array) {
SpringApplication.run(SpringHibernateCrudApplication::class.java, *args)
}
SpringHibernateCrudApplication.kt
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement
@SpringBootApplication
@EntityScan(basePackages = Array("com.the.sample.app"))
@EnableJpaRepositories(basePackages = {
Array("com.the.sample.app")
})
@EnableTransactionManagement
class SpringHibernateCrudApplication
object SpringHibernateCrudApplication extends App{
SpringApplication.run(classOf[SpringHibernateCrudApplication])
}
SpringHibernateCrudApplication.scala
- spring.datasource.driver-class-name - Defines the JDBC driver class name to use (JPA support the most common RDMS databases such as MySql, Postgres, Oracle, DB2)
- spring.datasource.url - defines the JDBC connection url
- spring.jpa.properties.hibernate.dialect - defines the database dialect to use for Queries or DDL statements