How to build a Spring + Hibernate + GraphQL application
1. Overview
This tutorial will provide an introduction to Spring Data JPA having a Spring project using Java 17, Hibernate 6, lombok, and GraphQL , it will cover configuring the persistence layer. For a step-by-step introduction to setting up the Spring context using Java-based configuration and GraphQL service definition, you can checkout the projection configuration and sources from our repository
2. GraphQL
Provides a web API approach in which clients define the structure of the data to be returned by the server. This can impede web caching of query results, the main advantage of GraphQL vs traditional Rest is the focus on implementing the use case while Rest focus is on providing the data as a resource, therefore a GraphQL query allow to be very granullar and query for the specific data points that the use case needs.
3. 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
4. Entity Class
Creating the JPA's entities is one of the most essential tasks in our implementation since this is 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
5. 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
6. Service
The service layer defines any business logic that needs to be implemented as well as 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 the JPA relations on the integration layer like Rest or GraphQL, 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
7. GraphQL Schema Definition
This is our integration layer where our microservice can be called by GraphQL using queries to fetch data and mutations to manipulate data
type User {
id: ID!
fullName: String
email: String!
}
type Query {
findById(id: ID): User
findByEmail(email: String): User
findAll(page: Int, pageSize: Int): [User]!
}
type Mutation {
createUser(fullName: String!, email: String!) : User!
updateUser(id: ID, fullName: String!, email: String!) : User!
deleteUser(id: ID): Boolean!
}
8. GraphQL Controller
Per each query and mutation we need to implement it
import com.the.sample.app.model.User;
import com.the.sample.app.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
import java.util.Optional;
@Controller
@AllArgsConstructor
public class UserController {
private final UserService userService;
@QueryMapping
public Optional findById(@Argument Long id) {
return userService.findById(id);
}
@QueryMapping
public Optional findByEmail(@Argument String email) {
return userService.findByEmail(email);
}
@QueryMapping
public List findAll(@Argument int page, @Argument int pageSize) {
return userService.findAll(page, pageSize);
}
@MutationMapping
public User createUser(@Argument String fullName, @Argument String email){
User user = User.builder().fullName(fullName).email(email).build();
userService.save(user);
return user;
}
@MutationMapping
public User updateUser(@Argument Long id,@Argument String fullName, @Argument String email){
User user = User.builder().id(id).fullName(fullName).email(email).build();
userService.save(user);
return user;
}
@MutationMapping
public Boolean deleteUser(@Argument Long id){
try{
userService.deleteById(id);
}catch (Exception ex){
return false;
}
return true;
}
}
UserController.java
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.MutationMapping
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller
import java.util.*
@Controller
class UserController(val userService: UserService) {
@QueryMapping
fun findById(@Argument id: Long): Optional? {
return Optional.ofNullable(userService.findById(id))
}
@QueryMapping
fun findByEmail(@Argument email: String): Optional? {
return Optional.ofNullable(userService.findByEmail(email))
}
@QueryMapping
fun findAll(@Argument page: Int, @Argument pageSize: Int): List {
return userService!!.findAll(page, pageSize)
}
@MutationMapping
fun createUser(@Argument fullName: String, @Argument email: String): User {
val user: User = User(fullName = fullName, email = email)
userService!!.save(user)
return user
}
@MutationMapping
fun updateUser(@Argument id: Long, @Argument fullName: String, @Argument email: String): User {
val user: User = User(id, fullName, email)
userService.save(user)
return user
}
@MutationMapping
fun deleteUser(@Argument id: Long): Boolean {
try {
userService.deleteById(id)
} catch (ex: Exception) {
return false
}
return true
}
}
UserController.kt
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.graphql.data.method.annotation.{Argument, MutationMapping, QueryMapping}
import org.springframework.stereotype.Controller
import java.util
import java.util.Optional
@Controller
class UserController(val userService: UserService) {
@QueryMapping def findById(@Argument id: Long): Optional[User] = userService.findById(id)
@QueryMapping def findByEmail(@Argument email: String): Optional[User] = userService.findByEmail(email)
@QueryMapping def findAll(@Argument page: Int, @Argument pageSize: Int): util.List[User] = userService.findAll(page, pageSize)
@MutationMapping def createUser(@Argument fullName: String, @Argument email: String): User = {
val user = new User(fullName = fullName, email = email)
userService.save(user)
user
}
@MutationMapping def updateUser(@Argument id: Long, @Argument fullName: String, @Argument email: String): User = {
val user = new User(fullName = fullName, email = email)
user.id = id
userService.save(user)
user
}
@MutationMapping def deleteUser(@Argument id: Long): Boolean = {
try userService.deleteById(id)
catch {
case ex: Exception =>
return false
}
true
}
}
UserController.scala
9. 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