Spring Hibernate Rest (WebFlux)

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

Database connection parameters are defined in the src/main/resources/application.properties
  • 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

8. Source Code

Checkout the whole source code for the following programming languages: