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
Main Class ─ ContactApplication
Let's check the main class of our Spring Boot application:
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:
- Name (full name or display name)
- Phone number (either personal or work number - or mobile number)
- Email address (either personal or work email)
- Address (any home or work address - address line 1, 2, and 3)
- Postal code of address
- 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:
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:
- annotated it with
@Entity
- 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.
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.
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:
- BadResourceException: to indicate if the resource (Contact) is incomplete or in the wrong format
- ResourceAlreadyExistsException: to indicate that the resource is already available (conflict)
- ResourceNotFoundException: to indicate that the resource is not available in the server (or database)
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);
}
}
package com.dariawan.contactapp.exception;
public class ResourceAlreadyExistsException extends Exception {
public ResourceAlreadyExistsException() {
}
public ResourceAlreadyExistsException(String msg) {
super(msg);
}
}
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', '[email protected]'),
('Roronoa Zoro', '09023456789', '[email protected]'),
('Nami', '09034567890', '[email protected]'),
('Usopp', '09045678901', '[email protected]'),
('Vinsmoke Sanji', '09056789012', '[email protected]'),
('Tony Tony Chopper', '09067890123', '[email protected]'),
('Nico Robin', '09078901234', '[email protected]'),
('Franky', '09089012345', '[email protected]'),
('Brook', '09090123456', '[email protected]')
We can do a simple unit test for our service, to check if nothing break from service layer until connectivity with data storage.
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("[email protected]");
contactService.save(c);
assertNotNull(c.getId());
Contact findContact = contactService.findById(c.getId());
assertEquals("Portgas D. Ace", findContact.getName());
assertEquals("[email protected]", findContact.getEmail());
// update record
c.setEmail("[email protected]");
contactService.update(c);
// test after update
findContact = contactService.findById(c.getId());
assertEquals("[email protected]", 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.
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', '[email protected]'),
('Roronoa Zoro', '09023456789', '[email protected]'),
('Nami', '09034567890', '[email protected]'),
('Usopp', '09045678901', '[email protected]'),
('Vinsmoke Sanji', '09056789012', '[email protected]'),
('Tony Tony Chopper', '09067890123', '[email protected]'),
('Nico Robin', '09078901234', '[email protected]'),
('Franky', '09089012345', '[email protected]'),
('Brook', '09090123456', '[email protected]')
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:
Function | URI | HTTP Methods | Description |
---|---|---|---|
findAll | /api/contacts | GET | Find all contacts with pagination and filter by name |
findContactById | /api/contacts/{contactId} | GET | Find single contact by id |
addContact | /api/contacts | POST | Add new contact |
updateContact | /api/contacts/{contactId} | PUT | Update contact |
updateAddress | /api/contacts/{contactId} | PATCH | Update contact's address |
deleteContactById | /api/contacts/{contactId} | DELETE | Delete 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":"[email protected]","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":7,"name":"Nico Robin","phone":"09078901234","email":"[email protected]","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":8,"name":"Franky","phone":"09089012345","email":"[email protected]","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null},{"id":9,"name":"Brook","phone":"09090123456","email":"[email protected]","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":"[email protected]","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}]Sanji","phone":"09056789012","email":"[email protected]","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\": \"[email protected]\", \"name\": \"Monkey D. Dragon\", \"note\": \"Leader Revolutionary Army \", \"phone\": null}" {"id":21,"name":"Monkey D. Dragon","phone":null,"email":"[email protected]","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":"[email protected]","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":"[email protected]","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\": \"[email protected]\", \"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":"[email protected]","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":"[email protected]","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!