In this tutorial we'll see how to create a Spring Boot REST API CRUD example with Spring data JPA (Hibernate) and entities having One-To-Many, Many-To-One bidirectional association. Database used is MySQL.
Database Tables
Tables used for the example are Customer and Account. Queries for creating tables are given below. A customer can have many accounts which means a One-To-Many association between Customer and Account. That is done by having the customer_id as foreign key in Account table.
I want the account number to start from 1000 so that is given as the initial value of auto_increment in Account table.
CREATE TABLE `customer` ( `customer_id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `mobile_number` varchar(255) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, `created_at` datetime(6) DEFAULT NULL, `created_by` varchar(255) DEFAULT NULL, `updated_on` datetime(6) DEFAULT NULL, `updated_by` varchar(255) DEFAULT NULL, PRIMARY KEY (`customer_id`), UNIQUE KEY `UK5v8hijx47m783qo8i4sox2n5t` (`mobile_number`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `account` ( `account_number` bigint NOT NULL AUTO_INCREMENT, `branch_address` varchar(255) DEFAULT NULL, `account_type` enum('CHECKING','SALARY','SAVINGS') DEFAULT NULL, `customer_id` bigint NOT NULL, `created_at` datetime(6) DEFAULT NULL, `created_by` varchar(255) DEFAULT NULL, `updated_on` datetime(6) DEFAULT NULL, `updated_by` varchar(255) DEFAULT NULL, PRIMARY KEY (`account_number`), KEY `FKnnwpo0lfq4xai1rs6887sx02k` (`customer_id`), CONSTRAINT `FKnnwpo0lfq4xai1rs6887sx02k` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Spring Boot REST API Example
In the Spring Boot project we'll create entity classes having fields that will map to DB Table columns. We'll create resources (Customer and Account) exposing REST endpoints to perform the following operations.
Customer
- @PostMapping("/customer")- To create a new Customer
- @GetMapping("/customer/{id}")- To fetch customer details for a passed customerId
- @GetMapping("/customer")- To fetch customer details for a passed mobile number (mobile number is passed as query parameter)
- @DeleteMapping("/customer/{id}")- To delete customer having the passed ID.
- @PutMapping("/customer")- To update whole customer object
- @PatchMapping("/customer")- For partial update (updating only the customer contact fields)
Account
- @PostMapping("/account")- To create a new Account
- @DeleteMapping("/account/{acctNo}")- To delete account having the passed ID.
- @GetMapping("/account/{acctNo}")- Fetch account details for the passed account number.
Technologies Used
- Spring Boot 3.x.x
- Java 21
- Spring Data JPA
- Hibernate 6
- MySQL 8
- Maven
- Spring Tool Suite (STS 4.x) is used as IDE.
Creating Spring Starter Project
In the STS select File - New - Spring Starter Project and provide the details for project name and packaging type. Please use the following image as reference.
Click next and select the required starter dependencies.
- Spring Web- Needed to create a web, RESTful applications.
- Spring Data JPA- Need for Java Persistence API. With Spring Data JPA you won't have to write boiler plate code for CRUD operations, it will be generated by Spring framework. You just need to create an interface.
- Spring Boot Dev Tools- Provides fast application restarts, LiveReload, and configurations for enhanced development experience.
Click next and finish to create a Spring boot project.
In the generated pom.xml also add the dependency for MySQL driver.
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
Refer the generated pom.xml for better understanding.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.knpcode</groupId> <artifactId>SpringBootProj</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBootProj</name> <description>Customer Service</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Configuring DB properties
Under src/main/resources create a file named application.yml. Note that application.properties file already exists once the project structure is created, this .properties file can also be used for providing configuration but these days YAML is preferred as it is more convenient to provide configuration for deployment. Note that YAML file uses indentation so always give space(s) when nesting.
spring: datasource: url: jdbc:mysql://localhost:3306/knpcode?createDatabaseIfNotExist=true driver-class-name: com.mysql.cj.jdbc.Driver username: DB_USER password: DB_PASSWORD application: name: customer-service jpa: database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: update show-sql: true
Under spring.datasource DB related configurations like DB_URL, DB_user, DB_password are provided. Please change the URL, user and password as per your DB configuration.
Application name is given as customer-service.
Under jpa, dialect is given as org.hibernate.dialect.MySQLDialect so that generated SQL is optimized for MySQL DB.
ddl-auto: update is used for auto-generation of DB schema.
show-sql: true is used to show the generated SQL in the console.
Create Packages
In the generated project structure create the following packages to keep the source files in a structured way. As per my package names the packages which I have to create are-
com.knpcode.customer.controller com.knpcode.customer.entity com.knpcode.customer.repository com.knpcode.customer.service com.knpcode.customer.audit com.knpcode.customer.dto com.knpcode.customer.exception
Please put the classes in the appropriate package.
Entity classes
Under the com.knpcode.customer.entity package create classes Customer
, Account
and Base Entity
. Generally in a project you do have tables with these columns created_by, created_at, updated_on, updated_by to capture user data for the same. When creating Entity classes general design is to create a BaseEntity class with these columns which can then be extended
by other entity classes.
BaseEntity.java
import java.time.LocalDateTime; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public class BaseEntity { @CreatedDate @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @CreatedBy @Column(name = "created_by", updatable = false) private String createdBy; @LastModifiedDate @Column(name = "updated_on", insertable = false) private LocalDateTime updatedOn; @LastModifiedBy @Column(name = "updated_by", insertable = false) private String updatedBy; public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } public LocalDateTime getUpdatedOn() { return updatedOn; } public void setUpdatedOn(LocalDateTime updatedOn) { this.updatedOn = updatedOn; } public String getUpdatedBy() { return updatedBy; } public void setUpdatedBy(String updatedBy) { this.updatedBy = updatedBy; } }
In the above class some of the important points are-
- Use of @MappedSuperclass annotation to indicate that this class would be inherited and it is not an entity class which maps to a DB table.
- Use of @LastModifiedDate, @LastModifiedBy, @CreatedDate, @CreatedBy annotations to configure JPA to persist values for these columns automatically. Note that with the @CreatedDate and @CreatedBy annotations updatable = false attribute is used because you don’t want to change the values of these columns at the time of update. Same way, with @LastModifiedDate and @LastModifiedBy annotations insertable = false attribute is used because these columns should have value only when row is updated not when the row is inserted.
- To ensure that JPA can automatically put values for these four columns you need some extra configuration. First thing is using the @EntityListeners(AuditingEntityListener.class) annotation to configure an entity listener to capture auditing information on persisting and updating entities.
- To provide information about the user (value that will be inserted in createdBy and updatedBy fields), you need to provide an implementation of AuditorAware interface and override getCurrentAuditor() method.
AuditorAware interface implementation
import java.util.Optional; import org.springframework.data.domain.AuditorAware; import org.springframework.stereotype.Component; @Component public class CustomAuditAware implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { return Optional.of("admin"); } }
Here the user name is hardcoded. In actual project you can get it from Security context.
You also need to use @EnableJpaAuditing annotation to enable auditing in JPA. That annotation can be used in the application class.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing(auditorAwareRef = "customAuditAware") public class SpringBootProjApplication { public static void main(String[] args) { SpringApplication.run(SpringBootProjApplication.class, args); } }
Customer.java
import java.util.ArrayList; import java.util.List; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @Table(name = "customer") public class Customer extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "customer_id") private Long customerId; @Column(name = "name") private String name; private String email; @Column(name = "mobile_number", unique = true) private String mobileNumber; @Column(name = "address") private String address; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Account> accounts = new ArrayList<>(); public List<Account> getAccounts() { return accounts; } public void setAccounts(List<Account> accounts) { this.accounts = accounts; for(Account account : accounts) { account.setCustomer(this); } } public Long getCustomerId() { return customerId; } public void setCustomerId(Long customerId) { this.customerId = customerId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getMobileNumber() { return mobileNumber; } public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
In the above class some of the important points are-
- Since a customer can have many accounts, in Java class that means using a field of type List or Set. That is why List<Account> accounts field is there.
- Field accounts is annotated with @OneToMany annotation indicating the association that one customer may associate with many accounts. Cascade type is used as CascadeType.ALL which means all operations like merge, delete will be cascaded to associated entity. Though Fetch type is lazy by default for OnetoMany, still keeping it for readability purpose.
- In the setAccounts(List<Account> accounts) method customer is explicitly set for each account.
for(Account account : accounts) { account.setCustomer(this); }
Account.java
import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import com.fasterxml.jackson.annotation.JsonProperty; @Entity @Table(name = "account") public class Account extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "account_number") private Long accountNumber; @Enumerated(EnumType.STRING) @Column(name = "account_type") private AccountType accountType; @Column(name = "branch_address") private String branchAddress; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "customer_id") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private Customer customer; public Long getAccountNumber() { return accountNumber; } public void setAccountNumber(Long accountNumber) { this.accountNumber = accountNumber; } public AccountType getAccountType() { return accountType; } public void setAccountType(AccountType accountType) { this.accountType = accountType; } public String getBranchAddress() { return branchAddress; } public void setBranchAddress(String branchAddress) { this.branchAddress = branchAddress; } public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } @Override public String toString() { return "Account [accountNumber=" + accountNumber + ", accountType=" + accountType + ", branchAddress=" + branchAddress + ", customer=" + customer + "]"; } }
In the above class some of the important points are-
- To make the association bi-directional @ManytoOne annotation is used here with the customer field. Use of
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) ensures that customer object is not created from JSON
(deserialization process) otherwise the process may go into an infinite loop.
@ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "customer_id") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private Customer customer;
- accountType field is annotated with @Enumerated(EnumType.STRING) which means this field needs an enum value and persists that value as String.
AccountType.java
This is the enum with 3 values for account types.
public enum AccountType { CHECKING, SAVINGS, SALARY }
Controller Classes
Controller classes with the REST endpoints.
CustomerController.java
import java.net.URI; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.knpcode.customer.dto.ResponseDto; import com.knpcode.customer.entity.Customer; import com.knpcode.customer.service.CustomerService; @RestController @RequestMapping("/api") public class CustomerController { private final CustomerService customerService; public CustomerController(CustomerService customerService) { this.customerService = customerService; } @PostMapping("/customer") public ResponseEntity<Customer> createCustomer(@RequestBody Customer customer) { try { Customer savedCustomer = customerService.createCustomer(customer); final URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").build() .expand(savedCustomer.getCustomerId()).toUri(); return ResponseEntity.created(location).body(savedCustomer); }catch(Exception e) { throw new RuntimeException("Error while creating customer " + e.getMessage()); } } @GetMapping("/customer/{id}") public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) { Customer customer = customerService.getCustomerById(id); return new ResponseEntity<>(customer, HttpStatus.OK); } @GetMapping("/customer") public ResponseEntity<Customer> getCustomerByMobileNumber(@RequestParam String mobileNumber) { Customer customer = customerService.getCustomerByMobileNumber(mobileNumber); return new ResponseEntity<>(customer, HttpStatus.OK); } @DeleteMapping("/customer/{id}") public ResponseEntity<ResponseDto> deleteCustomerById(@PathVariable Long id) { Customer customer = customerService.getCustomerById(id); customerService.deleteCustomerById(id); return ResponseEntity.ok(new ResponseDto(HttpStatus.OK, "Customer deleted successfully")); } @PutMapping("/customer") public ResponseEntity<Customer> updateCustomer(@RequestBody Customer customer) { Customer updatedCustomer = customerService.updateCustomer(customer); return new ResponseEntity<>(updatedCustomer, HttpStatus.OK); } @PatchMapping("/customer") public ResponseEntity<Customer> UpdateCustomerContactDeatails(@RequestBody Customer customer) { Customer updatedCustomer = customerService.updateCustomerContactDeatails(customer); return new ResponseEntity<>(updatedCustomer, HttpStatus.OK); } }
In the above class some of the important points are-
- For mapping, nouns are used as per REST specification so for creating a resource, mapping is /api/customer whereas for getting or deleting a specific resource mapping is /api/customer/id where id is the customerId.
- When creating a resource, response which is sent back should have the location of the newly created resource sent as header and body may include the created object, along with the status code (which is 201 Created here).
- When deleting or fetching Customer data by passing a specific ID, you should also ensure that the ID which is sent by user actually exists. That validation is done in the Service class.
- There is a method getCustomerByMobileNumber() which needs mobile number to be sent as a query parameter. That means URL to access this endpoint should be something like this- localhost:8080/api/customer?mobileNumber=9888777650
- There is a method updateCustomer() which is annotated with PutMapping which means it will replace the whole object. For that validation is done initially to ensure that such a customer exists.
- In the method updateCustomerContactDeatails() which is annotated with @PatchMapping, partial update of email, address, mobileNumber fields is done.
- In all the methods appropriate response is sent back which contains status code and body. Body can be an object or a message of type ResponseDto.
AccountController.java
import java.net.URI; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.knpcode.customer.dto.ResponseDto; import com.knpcode.customer.entity.Account; import com.knpcode.customer.entity.Customer; import com.knpcode.customer.service.AccountService; import com.knpcode.customer.service.CustomerService; @RestController @RequestMapping("/api") public class AccountController { private final AccountService accountService; private final CustomerService customerService; AccountController(AccountService accountService, CustomerService customerService) { this.accountService = accountService; this.customerService = customerService; } @PostMapping("/account") public ResponseEntity<?> createAccount(@RequestBody Account account) { try { System.out.println(account.getCustomer().getCustomerId()); Customer customer = customerService.getCustomerById(account.getCustomer().getCustomerId()); // set the customer account.setCustomer(customer); Account savedAccount = accountService.createAccount(account); final URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}").build() .expand(savedAccount.getAccountNumber()).toUri(); return ResponseEntity.created(location).body(savedAccount); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Error while creating Account " + e.getMessage()); } } @DeleteMapping("/account/{acctNo}") public ResponseEntity<ResponseDto> deleteAccountByAccountNumber(@PathVariable Long acctNo) { accountService.deleteAccountByAccountNumber(acctNo); return ResponseEntity.ok(new ResponseDto(HttpStatus.OK, "Account deleted successfully")); } @GetMapping("/account/{acctNo}") public Account getAccountByAccountNumber(@PathVariable Long acctNo) { Account account= accountService.getAccountByAccountNumber(acctNo); System.out.println(account.getCustomer().getName()); return account; } }
Service classes
Since Service layer should not be tightly coupled with Presentation layer so interfaces are created for the abstraction.
CustomerService.java
import com.knpcode.customer.entity.Customer; public interface CustomerService { public Customer createCustomer(Customer customer); public Customer getCustomerById(Long customerId); public Customer getCustomerByMobileNumber(String mobileNumber); public void deleteCustomerById(Long customerId); public Customer updateCustomer(Customer customer); public Customer updateCustomerContactDeatails(Customer customer); }
CustomerServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired; import com.knpcode.customer.entity.Customer; import com.knpcode.customer.exception.ResourceNotFoundException; import com.knpcode.customer.repository.CustomerRepository; import org.springframework.stereotype.Service; @Service public class CustomerServiceImpl implements CustomerService { @Autowired CustomerRepository customerRepository; @Override public Customer createCustomer(Customer customer) { return customerRepository.save(customer); } @Override public Customer getCustomerById(Long customerId) { Customer customer = customerRepository.findById(customerId) .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customerId)); return customer; } @Override public Customer getCustomerByMobileNumber(String mobileNumber) { Customer customer = customerRepository.findByMobileNumber(mobileNumber) .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given mobileNumber: " + mobileNumber)); return customer; } @Override public void deleteCustomerById(Long customerId) { customerRepository.deleteById(customerId); } @Override public Customer updateCustomer(Customer customer) { Customer dbCustomer = customerRepository.findById(customer.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customer.getCustomerId())); mapToCustomer(customer, dbCustomer); return customerRepository.save(dbCustomer); } private Customer mapToCustomer(Customer customer, Customer dbCustomer) { dbCustomer.setName(customer.getName()); dbCustomer.setEmail(customer.getEmail()); dbCustomer.setMobileNumber(customer.getMobileNumber()); dbCustomer.setAddress(customer.getAddress()); return dbCustomer; } /** * Method for partial update * Update only Contact Details - email, address, mobileNumber */ @Override public Customer updateCustomerContactDeatails(Customer customer) { Customer dbCustomer = customerRepository.findById(customer.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customer.getCustomerId())); if(customer.getMobileNumber() != null) { dbCustomer.setMobileNumber(customer.getMobileNumber()); } if(customer.getAddress() != null) { dbCustomer.setAddress(customer.getAddress()); } if(customer.getEmail() != null) { dbCustomer.setEmail(customer.getEmail()); } return customerRepository.save(dbCustomer); } }
AccountService.java
import com.knpcode.customer.entity.Account; public interface AccountService { public Account createAccount(Account account); public Account getAccountByAccountNumber(Long acctNo); public void deleteAccountByAccountNumber(Long acctNo); }
AccountServiceImpl.java
import org.springframework.stereotype.Service; import com.knpcode.customer.entity.Account; import com.knpcode.customer.exception.ResourceNotFoundException; import com.knpcode.customer.repository.AccountRepository; @Service public class AccountServiceImpl implements AccountService { private final AccountRepository accountRepository; AccountServiceImpl(AccountRepository accountRepository){ this.accountRepository = accountRepository; } @Override public Account createAccount(Account account) { return accountRepository.save(account); } @Override public void deleteAccountByAccountNumber(Long acctNo) { accountRepository.findById(acctNo).orElseThrow(() -> new ResourceNotFoundException("No account associated with the given account number " + acctNo)); accountRepository.deleteById(acctNo); } @Override public Account getAccountByAccountNumber(Long acctNo) { return accountRepository.findById(acctNo) .orElseThrow(() -> new ResourceNotFoundException("No account associated with the given account number " + acctNo)); } }
DTO classes
Data transfer object are POJOs that are used to transfer data among layers.
ResponseDto.java
import org.springframework.http.HttpStatus; public class ResponseDto { private HttpStatus statusCode; private String message; public ResponseDto(HttpStatus statusCode, String message) { this.statusCode = statusCode; this.message = message; } public HttpStatus getStatusCode() { return statusCode; } public void setStatusCode(HttpStatus statusCode) { this.statusCode = statusCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
Custom Exception classes
A custom exception class is also used in the example which is used to throw an exception when resource is not found.
public class ResourceNotFoundException extends RuntimeException { /** * */ private static final long serialVersionUID = 1L; public ResourceNotFoundException(String message) { super(message); } }
Repository classes
Since we are using Spring Data JPA so there is no need to create classes with data access code. We just need to create interfaces by extending JpaRepository interface and passing entity for which data access code is needed and the type of the ID.
AccountRepository.java
import org.springframework.data.jpa.repository.JpaRepository; import com.knpcode.customer.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountRepository extends JpaRepository<Account, Long> { }
CustomerRepository.java
import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.knpcode.customer.entity.Customer; import org.springframework.stereotype.Repository; @Repository public interface CustomerRepository extends JpaRepository<Customer, Long> { public Optional<Customer> findByMobileNumber(String mobileNumber); }
In the CustomerRepository interface one method findByMobileNumber()
is added, for this method also framework will create the data
access code (including SQL) by parsing the method.
With that we have all the classes for the example and we can run the application class by right clicking it and choosing Run As - Spring Boot App. If there is no error then Tomcat server should start listening on the port 8080 (default port).
Creating Customer resource
Creating a customer object by passing customer and account information. Using Postman you can test the creation of Customer resource.
In the Postman, change the mapping to POST in the dropdown and select tab body - raw -JSON. URL is localhost:8080/api/customer
Paste the following as body and click Send.
{ "name": "Ram", "email": "ram@ram.com", "mobileNumber": "9888777648", "address": "112, MG Road, Mumbai", "accounts":[{ "accountType": "SAVINGS", "branchAddress": "MG Road, Mumbai" }, { "accountType": "CHECKING", "branchAddress": "MG Road, Mumbai" }] }
If you check in the STS console you should also be able to see the generated queries because of setting jpa.show-sql: true in the application.yml file.
Hibernate: insert into customer (address,created_at,created_by,email,mobile_number,name) values (?,?,?,?,?,?) Hibernate: insert into account (account_type,branch_address,created_at,created_by,customer_id) values (?,?,?,?,?) Hibernate: insert into account (account_type,branch_address,created_at,created_by,customer_id) values (?,?,?,?,?)
In the DB, you should have one entry in the Customer table and two entries with the same customer_id in Account table. Also verify the values in the created_by and created_at table which should be "admin" and current date and time. Columns updated_on and updated_by shouldn't have any value.
Creating Account resource
You can add account to the existing customer by sending Account info to the URL- localhost:8080/api/account
{ "accountType": "SALARY", "branchAddress": "JP Nagar, Mumbai", "customer": { "customerId": 1 } }
That should add one more account to the existing Customer with ID as 1.
Get and Delete Customer
Same way you can check get and delete mapping by passing URL- localhost:8080/api/customer/1 where 1 is the ID part. Deleting customer resource should also delete the associated accounts because of the cascade property used in the entity mapping.
Getting account
You can get specific account by passing the account number with the URL- localhost:8080/api/account/1003
One thing to note here is initially Hibernate gets only the account data (not the associated customer) because of the FetchType = lazy setting. This is the generated SQL.
select a1_0.account_number,a1_0.account_type,a1_0.branch_address,a1_0.created_at,a1_0.created_by,a1_0.customer_id,a1_0.updated_by,a1_0.updated_on from account a1_0 where a1_0.account_number=?
In the AccountController I have also tried to access the customer name, that triggers another query to get associated customer information.
@GetMapping("/account/{acctNo}") public Account getAccountByAccountNumber(@PathVariable Long acctNo) { Account account= accountService.getAccountByAccountNumber(acctNo); System.out.println(account.getCustomer().getName()); return account; }
You should see another Select query-
select c1_0.customer_id,c1_0.address,c1_0.created_at,c1_0.created_by,c1_0.email,c1_0.mobile_number,c1_0.name,c1_0.updated_by,c1_0.updated_on from customer c1_0 where c1_0.customer_id=?
Updating customer
You can update customer by using PUT mapping and the URL as- localhost:8080/api/customer
In the case of update using PUT, you need to send whole body even if there is change in mobileNumber and address fields only. That is required because PUT replaces the entire resource. If you don't send the whole customer object whatever is not sent is replaced by null.
{ "customerId": 1, "name": "Ram", "email": "ram@ram.com", "mobileNumber": "9888777649", "address": "113, MG Road, Mumbai" }
This is the generated query-
update customer set address=?,email=?,mobile_number=?,name=?,updated_by=?,updated_on=? where customer_id=?
If you verify in the DB table now you should see values for updated_on and updated_by columns apart from the updated values.
Partial update (Using PATCH)
Patch mapping has been used to update the contact details of the customer (email, address, mobile number). With Patch you can send only the fields that need to be updated not the whole object as required with PUT.
That's all for the topic Spring Boot REST API, Data JPA, One-to-Many/Many-To-One Bidirectional Example. If something is missing or you have something to share about the topic please write a comment.
You may also like
- Spring Boot + Spring Data JPA + MySQL + Spring RESTful
- Spring Data JPA @Query Annotation Example
- Spring Boot MVC Form Validation Example
- Spring @Autowired Annotation
- Pass Data From Child to Parent Component in React
- Main Thread in Java
- JDBC Transaction Management and Savepoint Example
- Java BigDecimal Class With Examples
- Express File Download Using res.download()
- Java Program to Find Maximum And Minimum Number in a Matrix