Spring Boot CRUD application example
This article expounds on the ERD design discussion by implementing a Spring Boot application for the design.We shall also expose REST APIs to interact with our application to perform the CRUD operations.
Creating a Sping Boot starter project
1.Head to https://start.spring.io/
2.Fill in project details as show in below screen shot. Choose selected dependencies as well i.e Spring Web,Spring Data JPA,MySQL Driver,Lombok
3.Click GENERATE below to download the starter project to a location of your choice.
NB:The starter projects downloads as a .zip
4.Extract the .zip package and open the project with your IDE.For this lesson,we will be using Netbeans.
5.Build the project so that dependencies can be downloaded.
6.The project now looks as below.
Let’s create some packages
- Entities(package com.school.relationships.entities)-for storing database classes
- Repositories(package com.school.relationships.repositories)-for the repository classes for interacting with the database
- Services(package package com.school.relationships.services)-for services classes for exposing business logic/functionality
- Controllers(package com.school.relationships.controllers)-for storage of controller classes for the project
Creating the entities
Entities are Java classes that model the database and provide an easy way of interacting with the database.However,writing entities manually is time consuming and error prone.We can pull/reverse engineer the entities from the database .This is opposite of forward engineering as we saw in Mysql Workbench when creating our database from the design.
Lets create a database connection
On Services tab,Right click Databases and choose New Connection.
Key in the database connection parameters and click test the connection with the ‘Test Connection’ option.
If Connection succeeds,click Next then Finish.
Activate the Projects tab and right click on the entities package,choose New and then Entity Classes from Database.
On the database connection option ,choose the connections we created and click Next.
On the screen that appears click Next.On the last screen ,choose lazy for Association Fetch and java.util.list for Collection Type.
NB:lazy association fetches related tables/entities on demand and not when the initial object is loaded from query.This increases the performance of the query and the application.e.g when loading a student,the related subjects the student takes will not be loaded at that point since we might be loading a student to check other details that do not require the subject he/she takes.The opposite is true for eager fetch.lazy fetch is the default fetch type.
The below classes are added
Lets add the below database connection parameters on the application.properties file
spring.datasource.url=jdbc:mysql://localhost:3306/school
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=none
Then build the project.
Crud repositories
Lets create the crud repositories for interacting with the database as shown below
@Repository
public interface AdmissionfileRepository extends CrudRepository<Admissionfile, Integer> {
}
@Repository
public interface DormitoryRepository extends CrudRepository<Dormitory, Integer>{
}
@Repository
public interface StudentRepository extends CrudRepository<Student, Integer>{
}
@Repository
public interface SubjectRepository extends CrudRepository<Subject, Integer>{
}
@Repository
public interface TeacherRepository extends CrudRepository<Teacher, Integer>{
}
Composite primary keys
Now,inorder to perform Crud operations on our join tables ‘student_has_subject’ and ‘teacher_has_subject’ we need to add some entities not generated by Netbeans during our reverse engineering step and repositories for creating records of the entities in the database.
Composite primary keys are formed by combining the primary keys/unique keys of 2 or more tables in order to form a unique identifier.They are majorly used in Many to Many relationships.
-The below class models our composite primary keys for the student_has_subject table and teacher_has_subject tables .
package com.school.relationships.entities;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/**
*
* @author hp
*/
@Embeddable
public class StudentHasSubjectPK implements Serializable {
@Basic(optional = false)
@Column(name = "student_idstudent")
private int studentID;
@Basic(optional = false)
@Column(name = "subject_idsubject")
private int subjectID;
public StudentHasSubjectPK() {
}
public StudentHasSubjectPK(int studentID, int subjectID) {
this.studentID = studentID;
this.subjectID = subjectID;
}
@Override
public int hashCode() {
int hash = 7;
hash = 37 * hash + this.studentID;
hash = 37 * hash + this.subjectID;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final StudentHasSubjectPK other = (StudentHasSubjectPK) obj;
if (this.studentID != other.studentID) {
return false;
}
if (this.subjectID != other.subjectID) {
return false;
}
return true;
}
/**
* @return the studentID
*/
public int getStudentID() {
return studentID;
}
/**
* @param studentID the studentID to set
*/
public void setStudentID(int studentID) {
this.studentID = studentID;
}
/**
* @return the subjectID
*/
public int getSubjectID() {
return subjectID;
}
/**
* @param subjectID the subjectID to set
*/
public void setSubjectID(int subjectID) {
this.subjectID = subjectID;
}
}
package com.school.relationships.entities;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/**
*
* @author hp
*/
@Embeddable
public class TeacherHasSubjectPK implements Serializable {
@Basic(optional = false)
@Column(name = "teacher_idteacher")
private int teacherID;
@Basic(optional = false)
@Column(name = "subject_idsubject")
private int subjectID;
public TeacherHasSubjectPK() {
}
public TeacherHasSubjectPK(int teacherID, int subjectID) {
this.teacherID = teacherID;
this.subjectID = subjectID;
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + this.teacherID;
hash = 97 * hash + this.subjectID;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final TeacherHasSubjectPK other = (TeacherHasSubjectPK) obj;
if (this.teacherID != other.teacherID) {
return false;
}
if (this.subjectID != other.subjectID) {
return false;
}
return true;
}
/**
* @return the teacherID
*/
public int getTeacherID() {
return teacherID;
}
/**
* @param teacherID the teacherID to set
*/
public void setTeacherID(int teacherID) {
this.teacherID = teacherID;
}
/**
* @return the subjectID
*/
public int getSubjectID() {
return subjectID;
}
/**
* @param subjectID the subjectID to set
*/
public void setSubjectID(int subjectID) {
this.subjectID = subjectID;
}
}
Lets also create below models represent our student_has_subject table and teacher_has_subject tables and which also use the above composite primary keys.
NB:We started with the composite primary keys first since they are needed as primary keys in the student_has_subject and teacher_has_subject tables.
package com.school.relationships.entities;
import java.io.Serializable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author hp
*/
@Entity
@Table(name = "student_has_subject")
@XmlRootElement
public class StudentHasSubject implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private StudentHasSubjectPK studentHasSubjectPK;
public StudentHasSubject() {
}
/**
* @return the studentHasSubjectPK
*/
public StudentHasSubjectPK getStudentHasSubjectPK() {
return studentHasSubjectPK;
}
/**
* @param studentHasSubjectPK the studentHasSubjectPK to set
*/
public void setStudentHasSubjectPK(StudentHasSubjectPK studentHasSubjectPK) {
this.studentHasSubjectPK = studentHasSubjectPK;
}
}
package com.school.relationships.entities;
import java.io.Serializable;
import java.util.Objects;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author hp
*/
@Entity
@Table(name = "teacher_has_subject")
@XmlRootElement
public class TeacherHasSubject implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private TeacherHasSubjectPK teacherHasSubjectPK;
@Override
public int hashCode() {
int hash = 5;
hash = 89 * hash + Objects.hashCode(this.teacherHasSubjectPK);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final TeacherHasSubject other = (TeacherHasSubject) obj;
if (!Objects.equals(this.teacherHasSubjectPK, other.teacherHasSubjectPK)) {
return false;
}
return true;
}
/**
* @return the teacherHasSubjectPK
*/
public TeacherHasSubjectPK getTeacherHasSubjectPK() {
return teacherHasSubjectPK;
}
/**
* @param teacherHasSubjectPK the teacherHasSubjectPK to set
*/
public void setTeacherHasSubjectPK(TeacherHasSubjectPK teacherHasSubjectPK) {
this.teacherHasSubjectPK = teacherHasSubjectPK;
}
}
Lets add below repositories for the extra 2 entities we created.
@Repository
public interface StudentHasSubjectRepository extends CrudRepository<StudentHasSubject, StudentHasSubjectPK> {
}
@Repository
public interface TeacherHasSubjectRepository extends CrudRepository<TeacherHasSubject, TeacherHasSubjectPK> {
}
Next we proceed to create models to hold payload data of the entities we are inserting into the database as well as create service classes and controllers to expose our APIs.
The models to hold data
Request/response models are POJOs that hold payload data in API requests. They are lightweight in that they may not contain all the data of the underlying database entities they represent and also may be constructed in a way to leave out sensitive data from the underlying entities e.g. a StudentModel may omit the primary key of the underlying Student entity when serving API requests against a Student data processing system
-In our project,they are in the com.school.relationships.models package.
-Below is sample code for our StudentModel
package com.school.relationships.models;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author hp
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class StudentModel {
@JsonProperty("Surname")
private String surname;
@JsonProperty("Firstname")
private String firstname;
@JsonProperty("Lastname")
private String lasttname;
@JsonProperty("Dob")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date dateOfBirth;
@JsonProperty("FileNumber")
private Integer admissionFileNo;
@JsonProperty("DormitoryNumber")
private Integer dormitoryNo;
}
Implementing service classes
Service classes handle business logic of an application.In our case,we have several service classes namely AdmissionFileService, DormitoryService, StudentService, StudentSubjectService, SubjectService, TeacherService and TeacherSubjectService service.These classes are in the com.school.relationships.services packages.
Sample code for our StudentService is as below.
package com.school.relationships.services;
import com.school.relationships.entities.Admissionfile;
import com.school.relationships.entities.Student;
import com.school.relationships.models.StudentModel;
import com.school.relationships.repositories.StudentRepository;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* @author hp
*/
@Service
@Slf4j
public class StudentService {
@Autowired
private StudentRepository studentRepository;
@Autowired
private DormitoryService dormitoryService;
@Autowired
private AdmissionFileService admissionFileService;
public void createStudent(StudentModel model) {
try {
Student student = new Student();
Admissionfile file = admissionFileService.findAdmissionFileByFileNumber(model.getAdmissionFileNo()).get();
student.setFileNo(file);
student.setDob(model.getDateOfBirth());
student.setFirstName(model.getFirstname());
student.setSurname(model.getSurname());
student.setLastName(model.getLasttname());
student.setDormitoryIddormitory(dormitoryService.findDormitoryById(model.getDormitoryNo()).get());
studentRepository.save(student);
} catch (Exception e) {
log.error("Error creating student > {}", e);
throw e;
}
}
public Optional<Student> findStudentWithTheirSubjects(Integer idstudent) {
return studentRepository.findStudentAndSubjectsTaken(idstudent);
}
public Optional<Student> findStudentById(Integer idstudent) {
return studentRepository.findById(idstudent);
}
}
Implementing controllers to expose REST API
We implement controller classes to expose REST endpoints which will process our HTTP requests for creating students, fetching students, linking students and subjects they take, creating subjects, creating teachers etc.
Our controllers are found in the com.school.relationships.controllers package.
Sample controller is show below,the StudentController class.It exposes HTTP methods for creating a student
- createStudent(/Student)-a POST method that takes in StudentModel and returns success on creating a student.
- subjectList(/Student/Subjects/{id})-a GET method that returns the subjects a student takes in.A path variable named id representing a student is passed get specific subjects for a specific student.
- findStudent (/Student/{id})-a GET method for getting specific student data.A path variable named id which is a student identifier is passed in.
package com.school.relationships.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.school.relationships.entities.Student;
import com.school.relationships.models.StudentModel;
import com.school.relationships.services.StudentService;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
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.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;
/**
*
* @author hp
*/
@RestController
@RequestMapping("/Student")
@Slf4j
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> createStudent(@RequestBody StudentModel model) throws JsonProcessingException {
Map<String, Object> responseMap = new HashMap<>();
try {
log.info("POST | Student Creation Operation = {}", model);
responseMap.put("Status", "Success");
studentService.createStudent(model);
return ResponseEntity.ok(new ObjectMapper().writeValueAsString(responseMap));
} catch (Exception e) {
responseMap.put("Status", "Fail");
responseMap.put("Message", e.getMessage());
return new ResponseEntity<>(new ObjectMapper().writeValueAsString(responseMap), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping(value = "/Subjects/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> subjectList(@PathVariable Integer id) throws JsonProcessingException {
Map<String, Object> responseMap = new HashMap<>();
try {
log.info("GET |Student Subjects Operation");
Optional<Student> result = studentService.findStudentWithTheirSubjects(id);
responseMap.put("Status", "Success");
responseMap.put("Student's Subjects", result.get().getSubjectList());
return ResponseEntity.ok(new ObjectMapper().writeValueAsString(responseMap));
} catch (Exception e) {
responseMap.put("Status", "Fail");
responseMap.put("Message", e.getMessage());
return new ResponseEntity<>(new ObjectMapper().writeValueAsString(responseMap), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> findStudent(@PathVariable Integer id) throws JsonProcessingException {
Map<String, Object> responseMap = new HashMap<>();
try {
log.info("GET |Student Operation");
Optional<Student> result = studentService.findStudentById(id);
responseMap.put("Status", "Success");
responseMap.put("Student", result.get());
return ResponseEntity.ok(new ObjectMapper().writeValueAsString(responseMap));
} catch (Exception e) {
responseMap.put("Status", "Fail");
responseMap.put("Message", e.getMessage());
return new ResponseEntity<>(new ObjectMapper().writeValueAsString(responseMap), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
The code in the above illustrations can be found at Github