Spring Boot + JPA/Hibernate + PostgreSQL RESTful CRUD API Example

This tutorial will walk you through the steps of building a RESTful CRUD APIs web services with Spring Boot using JPA/Hibernate. Spring Boot makes it extremely convenient for programmers to quickly develop Spring applications using common RDBMS databases, or embedded databases. In this example, we will use PostgreSQL database.

The project that we will create in this example is a simple contact application.

Creating a Spring Boot Project

First, we can start by creating a new Spring Boot project either using Spring Intializr or Spring CLI tool. Please refer to Spring Boot Quick Start on how to scaffolding your application. In this example, I will use Spring CLI tool. Type following command in the terminal:

D:\Projects>spring init --name=contact-app --dependencies=web,data-jpa,postgresql,lombok --package-name=com.dariawan.contactapp spring-boot-jpa-hibernate-pgsql
Using service at https://start.spring.io
Project extracted to 'D:\Projects\spring-boot-jpa-hibernate-pgsql'

And here the project structure created:

spring-boot-jpa-hibernate-pgsql │ .gitignore │ HELP.md │ mvnw │ mvnw.cmd │ pom.xml │ ├───.mvn │ └───wrapper │ maven-wrapper.jar │ maven-wrapper.properties │ MavenWrapperDownloader.java │ └───src ├───main │ ├───java │ │ └───com │ │ └───dariawan │ │ └───contactapp │ │ ContactApplication.java │ │ │ └───resources │ │ application.properties │ │ │ ├───static │ └───templates │ └───test └───java └───com └───dariawan └───contactapp ContactApplicationTests.java

Let's check the main class of our Spring Boot application:

ContactApplication.java
package com.dariawan.contactapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ContactApplication {

    public static void main(String[] args) {
        SpringApplication.run(ContactApplication.class, args);
    }
}
                    

ContactApplication is annotated with @SpringBootApplication. Refer to @SpringBootApplication section in Spring Boot Web Application Example. The main() method will call Spring Boot’s SpringApplication.run() method to launch the application.

src/resources folder contains:

  • static: default folder for static resources such as css/stylesheet, js, fonts and images
  • templates: default folder for server-side templates (example: Thymeleaf)
  • application.properties: file that contains various properties (settings) for our application

PostgreSQL Configuration

Now, let’s create a database for our Spring Boot project. We can do this by using the createdb command line tool which is located in the bin folder of your PostgreSQL installation (you should add this to your PATH). To create a database named contactdb.

createdb -U postgres contactdb
password **************

Our sample application only manage one entity: Contact. Here the structure of Contact class:

  1. Name (full name or display name)
  2. Phone number (either personal or work number - or mobile number)
  3. Email address (either personal or work email)
  4. Address (any home or work address - address line 1, 2, and 3)
  5. Postal code of address
  6. Note

With that structure, here the DDL for table contact:

CREATE TABLE contact ( id bigserial NOT NULL, name character varying(255), phone character varying(255), email character varying(255), address1 character varying(255), address2 character varying(255), address3 character varying(255), postal_code character varying(255), note character varying(4000), CONSTRAINT contact_pkey PRIMARY KEY (id) ); ALTER TABLE contact OWNER TO barista;

Now, let’s configure Spring Boot to use PostgreSQL as our data source by adding PostgreSQL database url, username, and password in the src/main/resources/application.properties file:

spring.datasource.url = jdbc:postgresql://localhost/contactdb # Username and password spring.datasource.username = barista spring.datasource.password = espresso # Allows Hibernate to generate SQL optimized for a particular DBMS spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL82Dialect spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation = true

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation = true is added as workaround for error

java.sql.SQLFeatureNotSupportedException: Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented.

Please refer to this thread for more details.

Create Domain Models

Domain models refer to classes that are mapped to the corresponding tables in the database. It's useful for persistence and used by a higher-level layer to gain access to the data. For table contact, we will create a Contact class:

Contact.java
package com.dariawan.contactapp.domain;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "contact")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
public class Contact implements Serializable {

    private static final long serialVersionUID = 4048798961366546485L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank
    private String name;
    
    private String phone;
    private String email;
    private String address1;
    private String address2;
    private String address3;
    private String postalCode;
    
    @Column(length = 4000)
    private String note;    
}
                    

@Entity annotation defines that a class can be mapped to a table. It's a mandatory, when you create a new entity you have to do at least two things:

  1. annotated it with @Entity
  2. create an id field and annotate it with @Id

@Table annotation allows you to specify the details of the table that will be used to persist the entity in the database.

@Cache annotation used to provide caching configuration. The READ_WRITE strategy is an asynchronous cache concurrency mechanism and to prevent data integrity issues (e.g. stale cache entries), it uses a locking mechanism that provides unit-of-work isolation guarantees.

@Getter and @Setter is to let lombok generate the default getter/setter automatically.

@Id annotation mark a field as a primary key.

@GeneratedValue annotation specifies that a value will be automatically generated for that field. It's usually used for primary key generation strategy, as example we are using IDENTITY strategy which means "auto-increment".

@NotBlank part of Hibernate Validator; a constrained String is valid if it's not null and the trimmed length is greater than zero

@Column is used to specify the mapped column for a persistent property or field.

Create Repository

The repository layer (or sometimes DAO layer) is responsible for communication with the used data storage. ContactRepository will be used to access contact data from the database.

ContactRepository.java
package com.dariawan.contactapp.repository;

import com.dariawan.contactapp.domain.Contact;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ContactRepository extends PagingAndSortingRepository<Contact, Long>, 
        JpaSpecificationExecutor<Contact> {
}
                    

ContactRepository extends PagingAndSortingRepository, an interface that provides generic CRUD operations and add methods to retrieve entities using the pagination and sorting abstraction.

ContactRepository also extend JpaSpecificationExecutor<T> interface. This interface provides methods that can be used to invoke database queries using JPA Criteria API. T describes the type of the queried entity, in our case is Contact. To specify the conditions of the invoked database query, we need to create a new implementation of Specification<T>: ContactSpecification class.

package com.dariawan.contactapp.specification;

import com.dariawan.contactapp.domain.Contact;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;

public class ContactSpecification implements Specification<Contact> {

    private Contact filter;

    public ContactSpecification(Contact filter) {
        super();
        this.filter = filter;
    }

    @Override
    public Predicate toPredicate(Root<Contact> root, CriteriaQuery<?> cq,
            CriteriaBuilder cb) {

        Predicate p = cb.disjunction();

        if (filter.getName() != null) {
            p.getExpressions().add(cb.like(root.get("name"), "%" + filter.getName() + "%"));
        }

        if (filter.getPhone()!= null) {
            p.getExpressions().add(cb.like(root.get("phone"), "%" + filter.getPhone() + "%"));
        }

        return p;
    }
}
                    

Create Business Service

The (business) service layer usually acts as a transaction boundary, contains the business logic and sometimes responsible for security/authorization of the application. The service communicates with other services and call methods in the repository layer.

ContactService.java
package com.dariawan.contactapp.service;

import com.dariawan.contactapp.domain.Address;
import com.dariawan.contactapp.domain.Contact;
import com.dariawan.contactapp.exception.BadResourceException;
import com.dariawan.contactapp.exception.ResourceAlreadyExistsException;
import com.dariawan.contactapp.exception.ResourceNotFoundException;
import com.dariawan.contactapp.repository.ContactRepository;
import com.dariawan.contactapp.specification.ContactSpecification;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
public class ContactService {
    
    @Autowired
    private ContactRepository contactRepository;
    
    private boolean existsById(Long id) {
        return contactRepository.existsById(id);
    }
    
    public Contact findById(Long id) throws ResourceNotFoundException {
        Contact contact = contactRepository.findById(id).orElse(null);
        if (contact==null) {
            throw new ResourceNotFoundException("Cannot find Contact with id: " + id);
        }
        else return contact;
    }
    
    public List<Contact> findAll(int pageNumber, int rowPerPage) {
        List<Contact> contacts = new ArrayList<>();
        contactRepository.findAll(PageRequest.of(pageNumber - 1, rowPerPage)).forEach(contacts::add);
        return contacts;
    }
    
    public List<Contact> findAllByName(String name, int pageNumber, int rowPerPage) {
        Contact filter = new Contact();
        filter.setName(name);
        Specification<Contact> spec = new ContactSpecification(filter);
        
        List<Contact> contacts = new ArrayList<>();
        contactRepository.findAll(spec, PageRequest.of(pageNumber - 1, rowPerPage)).forEach(contacts::add);
        return contacts;
    }
    
    public Contact save(Contact contact) throws BadResourceException, ResourceAlreadyExistsException {
        if (!StringUtils.isEmpty(contact.getName())) {
            if (contact.getId() != null && existsById(contact.getId())) { 
                throw new ResourceAlreadyExistsException("Contact with id: " + contact.getId() +
                        " already exists");
            }
            return contactRepository.save(contact);
        }
        else {
            BadResourceException exc = new BadResourceException("Failed to save contact");
            exc.addErrorMessage("Contact is null or empty");
            throw exc;
        }
    }
    
    public void update(Contact contact) 
            throws BadResourceException, ResourceNotFoundException {
        if (!StringUtils.isEmpty(contact.getName())) {
            if (!existsById(contact.getId())) {
                throw new ResourceNotFoundException("Cannot find Contact with id: " + contact.getId());
            }
            contactRepository.save(contact);
        }
        else {
            BadResourceException exc = new BadResourceException("Failed to save contact");
            exc.addErrorMessage("Contact is null or empty");
            throw exc;
        }
    }
    
    public void updateAddress(Long id, Address address) 
            throws ResourceNotFoundException {
        Contact contact = findById(id);
        contact.setAddress1(address.getAddress1());
        contact.setAddress2(address.getAddress2());
        contact.setAddress3(address.getAddress3());
        contact.setPostalCode(address.getPostalCode());
        contactRepository.save(contact);        
    }
    
    public void deleteById(Long id) throws ResourceNotFoundException {
        if (!existsById(id)) { 
            throw new ResourceNotFoundException("Cannot find contact with id: " + id);
        }
        else {
            contactRepository.deleteById(id);
        }
    }
    
    public Long count() {
        return contactRepository.count();
    }
}
                    

@Service is stereotype for service layer.

@Autowired annotation allows Spring to resolve and inject collaborating beans into your bean.

As you can see from ContactService, there are three custom exception classes:

  1. BadResourceException: to indicate if the resource (Contact) is incomplete or in the wrong format
  2. ResourceAlreadyExistsException: to indicate that the resource is already available (conflict)
  3. ResourceNotFoundException: to indicate that the resource is not available in the server (or database)
BadResourceException.java
package com.dariawan.contactapp.exception;

import java.util.ArrayList;
import java.util.List;

public class BadResourceException extends Exception {

    private List<String> errorMessages = new ArrayList<>();
            
    public BadResourceException() {
    }

    public BadResourceException(String msg) {
        super(msg);
    }
    
    /**
     * @return the errorMessages
     */
    public List<String> getErrorMessages() {
        return errorMessages;
    }

    /**
     * @param errorMessages the errorMessages to set
     */
    public void setErrorMessages(List<String> errorMessages) {
        this.errorMessages = errorMessages;
    }

    public void addErrorMessage(String message) {
        this.errorMessages.add(message);
    }
}
                    

ResourceAlreadyExistsException
package com.dariawan.contactapp.exception;

public class ResourceAlreadyExistsException extends Exception {

    public ResourceAlreadyExistsException() {
    }

    public ResourceAlreadyExistsException(String msg) {
        super(msg);
    }
}
                    

ResourceNotFoundException.java
package com.dariawan.contactapp.exception;

public class ResourceNotFoundException extends Exception {

    public ResourceNotFoundException() {
    }

    public ResourceNotFoundException(String msg) {
        super(msg);
    }    
}
                    

insert into contact (name, phone, email)
values 
('Monkey D. Luffy', '09012345678', 'luffy@strawhatpirat.es'),
('Roronoa Zoro', '09023456789', 'zoro@strawhatpirat.es'),
('Nami', '09034567890', 'nami@strawhatpirat.es'),
('Usopp', '09045678901', 'usopp@strawhatpirat.es'),
('Vinsmoke Sanji', '09056789012', 'sanji@strawhatpirat.es'),
('Tony Tony Chopper', '09067890123', 'chopper@strawhatpirat.es'),
('Nico Robin', '09078901234', 'robin@strawhatpirat.es'),
('Franky', '09089012345', 'franky@strawhatpirat.es'),
('Brook', '09090123456', 'brook@strawhatpirat.es')
                    

We can do a simple unit test for our service, to check if nothing break from service layer until connectivity with data storage.

ContactServiceJPATest.java
package com.dariawan.contactapp.service;

import com.dariawan.contactapp.domain.Contact;
import com.dariawan.contactapp.exception.ResourceNotFoundException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;
import javax.sql.DataSource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ContactServiceJPATest {

    @Autowired 
    private DataSource dataSource;
    
    @Autowired 
    private ContactService contactService;
    
    ...
    
    @Rule
    public ExpectedException exceptionRule = ExpectedException.none();
    
    @Test
    public void testSaveUpdateDeleteContact() throws Exception{
        Contact c = new Contact();
        c.setName("Portgas D. Ace");
        c.setPhone("09012345678");
        c.setEmail("ace@whitebeard.com");
        
        contactService.save(c);
        assertNotNull(c.getId());
        
        Contact findContact = contactService.findById(c.getId());
        assertEquals("Portgas D. Ace", findContact.getName());
        assertEquals("ace@whitebeard.com", findContact.getEmail());
        
        // update record
        c.setEmail("ace@whitebeardpirat.es");
        contactService.update(c);
        
        // test after update
        findContact = contactService.findById(c.getId());
        assertEquals("ace@whitebeardpirat.es", findContact.getEmail());
        
        // test delete
        contactService.deleteById(c.getId());
        
        // query after delete
        exceptionRule.expect(ResourceNotFoundException.class);
        contactService.findById(c.getId());
    }    
}
                    

Some people will argue that service layer is unnecessary, and controllers just need to communicate to repositories directly. You can agree to disagree, but creating a service layer is a good practice as we can keep our controller class clean and we can add any required business logic to the service instead.

Creating RestController

And last but not least, our RestController. ContactController is the entry point of our REST APIs that performing CRUD operations on contacts.

ContactController.java
package com.dariawan.contactapp.controller;

import com.dariawan.contactapp.domain.Address;
import com.dariawan.contactapp.domain.Contact;
import com.dariawan.contactapp.exception.BadResourceException;
import com.dariawan.contactapp.exception.ResourceAlreadyExistsException;
import com.dariawan.contactapp.exception.ResourceNotFoundException;
import com.dariawan.contactapp.service.ContactService;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ContactController {
    
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    private final int ROW_PER_PAGE = 5;
    
    @Autowired
    private ContactService contactService;
    
    @GetMapping(value = "/contacts", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<Contact>> findAll(
            @RequestParam(value="page", defaultValue="1") int pageNumber,
            @RequestParam(required=false) String name) {
        if (StringUtils.isEmpty(name)) {
            return ResponseEntity.ok(contactService.findAll(pageNumber, ROW_PER_PAGE));
        }
        else {
            return ResponseEntity.ok(contactService.findAllByName(name, pageNumber, ROW_PER_PAGE));
        }
    }

    @GetMapping(value = "/contacts/{contactId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Contact> findContactById(@PathVariable long contactId) {
        try {
            Contact book = contactService.findById(contactId);
            return ResponseEntity.ok(book);  // return 200, with json body
        } catch (ResourceNotFoundException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // return 404, with null body
        }
    }
    
    @PostMapping(value = "/contacts")
    public ResponseEntity<Contact> addContact(@Valid @RequestBody Contact contact) 
            throws URISyntaxException {
        try {
            Contact newContact = contactService.save(contact);
            return ResponseEntity.created(new URI("/api/contacts/" + newContact.getId()))
                    .body(contact);
        } catch (ResourceAlreadyExistsException ex) {
            // log exception first, then return Conflict (409)
            logger.error(ex.getMessage());
            return ResponseEntity.status(HttpStatus.CONFLICT).build();
        } catch (BadResourceException ex) {
            // log exception first, then return Bad Request (400)
            logger.error(ex.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
    }
    
    @PutMapping(value = "/contacts/{contactId}")
    public ResponseEntity<Contact> updateContact(@Valid @RequestBody Contact contact, 
            @PathVariable long contactId) {
        try {
            contact.setId(contactId);
            contactService.update(contact);
            return ResponseEntity.ok().build();
        } catch (ResourceNotFoundException ex) {
            // log exception first, then return Not Found (404)
            logger.error(ex.getMessage());
            return ResponseEntity.notFound().build();
        } catch (BadResourceException ex) {
            // log exception first, then return Bad Request (400)
            logger.error(ex.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
    }
    
    @PatchMapping("/contacts/{contactId}")
    public ResponseEntity<Void> updateAddress(@PathVariable long contactId,
            @RequestBody Address address) {
        try {
            contactService.updateAddress(contactId, address);
            return ResponseEntity.ok().build();
        } catch (ResourceNotFoundException ex) {
            // log exception first, then return Not Found (404)
            logger.error(ex.getMessage());
            return ResponseEntity.notFound().build();
        }
    }
    
    @DeleteMapping(path="/contacts/{contactId}")
    public ResponseEntity<Void> deleteContactById(@PathVariable long contactId) {
        try {
            contactService.deleteById(contactId);
            return ResponseEntity.ok().build();
        } catch (ResourceNotFoundException ex) {
            logger.error(ex.getMessage());
            return ResponseEntity.notFound().build();
        }
    }
}
                    

@RestController is a convenience annotation that is itself annotated with @Controller and @ResponseBody. This annotation is applied to a class to mark it as a request handler, and used for RESTful web services using Spring MVC.

@RequestMapping annotation maps HTTP requests to handler methods of controllers. This is one of the most common annotation used in Spring Web applications. There are @GetMapping , @PostMapping , @PutMapping,@PatchMapping, and @DeleteMapping for handling different HTTP request types.

@Valid annotation is to ensure that the request body is valid. It will fail if the @NotBlank is fail for Contact's name?

To understand about HTTP Verbs (GET/POST/PUT/PATCH/DELETE), please check HTTP Methods in Spring RESTful Services.

Running and Test the Application

Before we running the application, let's insert some data into contact table:

insert into contact (name, phone, email)
values 
('Monkey D. Luffy', '09012345678', 'luffy@strawhatpirat.es'),
('Roronoa Zoro', '09023456789', 'zoro@strawhatpirat.es'),
('Nami', '09034567890', 'nami@strawhatpirat.es'),
('Usopp', '09045678901', 'usopp@strawhatpirat.es'),
('Vinsmoke Sanji', '09056789012', 'sanji@strawhatpirat.es'),
('Tony Tony Chopper', '09067890123', 'chopper@strawhatpirat.es'),
('Nico Robin', '09078901234', 'robin@strawhatpirat.es'),
('Franky', '09089012345', 'franky@strawhatpirat.es'),
('Brook', '09090123456', 'brook@strawhatpirat.es')
                    

And now, we are ready to run our application and test the APIs. Type the below command at the project root directory: mvn clean spring-boot:run

D:\Projects\spring-boot-jpa-hibernate-pgsql>mvn clean spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------< com.example:spring-boot-jpa-hibernate-pgsql >-------------
[INFO] Building contact-app 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ spring-boot-jpa-hibernate-pgsql ---
[INFO] Deleting D:\Projects\spring-boot-jpa-hibernate-pgsql\target
[INFO]
[INFO] >>> spring-boot-maven-plugin:2.1.8.RELEASE:run (default-cli) > test-compile @ spring-boot-jpa-hibernate-pgsql >>>
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ spring-boot-jpa-hibernate-pgsql ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ spring-boot-jpa-hibernate-pgsql ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 9 source files to D:\Projects\spring-boot-jpa-hibernate-pgsql\target\classes
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ spring-boot-jpa-hibernate-pgsql ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory D:\Projects\spring-boot-jpa-hibernate-pgsql\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ spring-boot-jpa-hibernate-pgsql ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to D:\Projects\spring-boot-jpa-hibernate-pgsql\target\test-classes
[INFO]
[INFO] <<< spring-boot-maven-plugin:2.1.8.RELEASE:run (default-cli) < test-compile @ spring-boot-jpa-hibernate-pgsql <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:2.1.8.RELEASE:run (default-cli) @ spring-boot-jpa-hibernate-pgsql ---

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.8.RELEASE)

2019-10-17 01:50:17.098  INFO 16556 --- [           main] c.d.contactapp.ContactApplication     : Starting ContactApplication on DessonAr with PID 16556 (D:\Projects\spring-boot-jpa-hibernate-pgsql\target\classes started by Desson in D:\Projects\spring-boot-jpa-hibernate-pgsql)
2019-10-17 01:50:17.103  INFO 16556 --- [           main] c.d.contactapp.ContactApplication     : No active profile set, falling back to default profiles: default
2019-10-17 01:50:18.086  INFO 16556 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-10-17 01:50:18.194  INFO 16556 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 93ms. Found 1 repository interfaces.
2019-10-17 01:50:18.791  INFO 16556 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$418a0ada] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-10-17 01:50:19.680  INFO 16556 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2019-10-17 01:50:19.732  INFO 16556 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-10-17 01:50:19.732  INFO 16556 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.24]
2019-10-17 01:50:19.956  INFO 16556 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-10-17 01:50:19.957  INFO 16556 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2782 ms
2019-10-17 01:50:20.256  INFO 16556 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-10-17 01:50:20.455  INFO 16556 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-10-17 01:50:20.550  INFO 16556 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
        name: default
        ...]
2019-10-17 01:50:20.657  INFO 16556 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.11.Final}
2019-10-17 01:50:20.660  INFO 16556 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2019-10-17 01:50:21.214  INFO 16556 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-10-17 01:50:21.451  INFO 16556 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL82Dialect
2019-10-17 01:50:21.574  INFO 16556 --- [           main] o.h.e.j.e.i.LobCreatorBuilderImpl        : HHH000421: Disabling contextual LOB creation as hibernate.jdbc.lob.non_contextual_creation is true
2019-10-17 01:50:21.581  INFO 16556 --- [           main] org.hibernate.type.BasicTypeRegistry     : HHH000270: Type registration [java.util.UUID] overrides previous : org.hibernate.type.UUIDBinaryType@5bafec18
2019-10-17 01:50:22.400  INFO 16556 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-10-17 01:50:23.121  INFO 16556 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-10-17 01:50:23.218  WARN 16556 --- [           main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-10-17 01:50:23.637  INFO 16556 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-10-17 01:50:23.641  INFO 16556 --- [           main] c.d.contactapp.ContactApplication     : Started ContactApplication in 7.386 seconds (JVM running for 20.115)

Everything good. Now, It’s time to test our APIs. In this article, I will just use simple curl command. Let's revisit again functions/APIs that available from ContactController:

FunctionURIHTTP MethodsDescription
findAll/api/contactsGETFind all contacts with pagination and filter by name
findContactById/api/contacts/{contactId}GETFind single contact by id
addContact/api/contactsPOSTAdd new contact
updateContact/api/contacts/{contactId}PUTUpdate contact
updateAddress/api/contacts/{contactId}PATCHUpdate contact's address
deleteContactById/api/contacts/{contactId}DELETEDelete contact

First, to test if findAll(...) with pagination and search by name working:

$ curl http://localhost:8080/api/contacts?page=2
[{"id":6,"name":"Tony Tony Chopper","phone":"09067890123","email":"chopper@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":7,"name":"Nico Robin","phone":"09078901234","email":"robin@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":8,"name":"Franky","phone":"09089012345","email":"franky@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":9,"name":"Brook","phone":"09090123456","email":"brook@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}]
$ curl http://localhost:8080/api/contacts?name=Luffy
[{"id":1,"name":"Monkey D. Luffy","phone":"09012345678","email":"luffy@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}]Sanji","phone":"09056789012","email":"sanji@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}]

Next, is to test addContact(...), deleteContactById(...), with findContactById(...) in between

$ curl -X POST "http://localhost:8080/api/contacts" -H "Content-Type: application/json" -d "{ \"email\": \"dragon@revolutionary.net\", \"name\": \"Monkey D. Dragon\", \"note\": \"Leader Revolutionary Army \", \"phone\": null}"
{"id":21,"name":"Monkey D. Dragon","phone":null,"email":"dragon@revolutionary.net","address1":null,"address2":null,"address3":null,"postalCode":null,"note":"Leader Revolutionary Army "}
$ curl -X GET "http://localhost:8080/api/contacts/21"
{"id":21,"name":"Monkey D. Dragon","phone":null,"email":"dragon@revolutionary.net","address1":null,"address2":null,"address3":null,"postalCode":null,"note":"Leader Revolutionary Army "}
$ curl -X DELETE "http://localhost:8080/api/contacts/21"

$ curl -X GET "http://localhost:8080/api/contacts/21"

And finally is to test updateContact (...) and updateAddress(...)

$ curl -X GET "http://localhost:8080/api/contacts/3"
{"id":3,"name":"Nami","phone":"09034567890","email":"nami@strawhatpirat.es","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}
$ curl -X PUT "http://localhost:8080/api/contacts/3" -H "Content-Type: application/json" -d "{ \"address1\": \"Cocoyashi Village\", \"email\": \"nami@strawhatpirat.es\", \"name\": \"Nami\", \"note\": \"Navigator who dreams of drawing a map of the entire world.\"}"

$ curl -X GET "http://localhost:8080/api/contacts/3"
{"id":3,"name":"Nami","phone":null,"email":"nami@strawhatpirat.es","address1":"Cocoyashi Village","address2":null,"address3":null,"postalCode":null,"note":"Navigator who dreams of drawing a map of the entire world."}
$ curl -X PATCH "http://localhost:8080/api/contacts/3" -H "Content-Type: application/json" -d "{ \"address1\": \"Thousand Sunny\", \"address2\": \"Straw Hat Grand Fleet\", \"address3\": null, \"postalCode\": null}"

$ curl -X GET "http://localhost:8080/api/contacts/3"
{"id":3,"name":"Nami","phone":null,"email":"nami@strawhatpirat.es","address1":"Thousand Sunny","address2":"Straw Hat Grand Fleet","address3":null,"postalCode":null,"note":"Navigator who dreams of drawing a map of the entire world."}

Final Word

We successfully built a RESTful CRUD API using Spring Boot, JPA/Hibernate and PostgreSQL. This is the final project structure:

spring-boot-jpa-hibernate-pgsql │ .gitignore │ HELP.md │ mvnw │ mvnw.cmd │ pom.xml │ ├───.mvn │ └───wrapper │ maven-wrapper.jar │ maven-wrapper.properties │ MavenWrapperDownloader.java │ └───src ├───main │ ├───java │ │ └───com │ │ └───dariawan │ │ └───contactapp │ │ │ ContactApplication.java │ │ │ │ │ ├───controller │ │ │ ContactController.java │ │ │ │ │ ├───domain │ │ │ Address.java │ │ │ Contact.java │ │ │ │ │ ├───exception │ │ │ BadResourceException.java │ │ │ ResourceAlreadyExistsException.java │ │ │ ResourceNotFoundException.java │ │ │ │ │ ├───repository │ │ │ ContactRepository.java │ │ │ │ │ ├───service │ │ │ ContactService.java │ │ │ │ │ └───specification │ │ ContactSpecification.java │ │ │ └───resources │ │ application.properties │ │ │ ├───static │ └───templates ├───sql │ contact.sql │ └───test └───java └───com └───dariawan └───contactapp │ ContactApplicationTests.java │ └───service ContactServiceJPATest.java

Thank you, and happy coding!