Simple application that displays the Service and the Persistence layers of a typical Hibernate project. The app is a eGradebook which should display the grades of the students in different classes.
This project only displays the backend so there is no MVC here. The functionality is tested using JUnit.
Technologies Used
- Spring Core
- Hibernate
- MySQL
- Maven
- Junit
Database
The students are stored in the Student table and their information in the Student_info table. The Class table contains the courses and is connected to the Student table through the the Student_class table. The Results table contains information about the students grades.
CREATE TABLE student ( student_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT , username VARCHAR(400) NOT NULL, password VARCHAR(400) NOT NULL, creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE student_info ( student_id BIGINT NOT NULL PRIMARY KEY, first_name VARCHAR(400), last_name VARCHAR(400), semester VARCHAR(50), address VARCHAR(1000), city VARCHAR(400), country VARCHAR(400), phone_1 VARCHAR(100), phone_2 VARCHAR(100), FOREIGN KEY (student_id) REFERENCES student(student_id) ); create table class ( class_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description varchar(500), semester VARCHAR(50), UNIQUE (name) ); create table student_class ( student_id BIGINT, class_id BIGINT, FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (class_id) REFERENCES class(class_id) ); CREATE TABLE results ( result_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, grade INT, student_id BIGINT, class_id BIGINT, creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (class_id) REFERENCES class(class_id) );
Implementation
Entities
The entities represent the database tables, in an object oriented way. Using the hibernate annotations we are able to describe the relationships between them.
First of all we declare our class as @Entity and we use the @Table annotation to map it with a table.
We use the @Id annotation to declare that the current property is the primary key of the table. Also the @GeneratedValue annotation is used to the auto generated columns. Using the @Column annotation we map the variable with the table column.
Student.java
@Entity @Table(name = "student") public class Student { private long studentId; private String username; private String password; private Date creationDate; private StudentInfo studentInfo; private Set<Result> resultSet = new HashSet<>(); private Set<Course> courseSet = new HashSet<>(); public Student() { } public Student(String username, String password, Date creationDate) { this.username = username; this.password = password; this.creationDate = creationDate; } @Id @GeneratedValue @Column(name="student_id") public long getStudentId() { return studentId; } public void setStudentId(long studentId) { this.studentId = studentId; } @Column(name="username") public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Column(name="password") public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Column(name="creation_date") public Date getCreationDate() { return creationDate; } public void setCreationDate(Date creationDate) { this.creationDate = creationDate; } @OneToOne(fetch = FetchType.EAGER, mappedBy = "student", cascade = CascadeType.ALL) public StudentInfo getStudentInfo() { return studentInfo; } public void setStudentInfo(StudentInfo studentInfo) { this.studentInfo = studentInfo; } @OneToMany(fetch = FetchType.EAGER, mappedBy = "student") public Set<Result> getResultSet() { return resultSet; } public void setResultSet(Set<Result> resultSet) { this.resultSet = resultSet; } @ManyToMany(fetch = FetchType.LAZY, mappedBy = "students") public Set<Course> getCourseSet() { return courseSet; } public void setCourseSet(Set<Course> courseSet) { this.courseSet = courseSet; } }
The trickiest task we have to perform is to map the entities according to the database relationships. Tables student and student_info are connected with an one-to-one relationship, which needs configuration on both sides. On the Student entity, we place the @OneToOne annotation on the studentInfo getter method. We use the FetchType.EAGER as fetch type, because we need the student information everytime we load a student. On the StudentInfo entity we will declare the studentId variable and use the @GenericGenerator annotation on its getter to configure it as a foreign key. Also we need a student variable with an @OneToOne annotation as well (and fetch type also eager).
Student.java
@Entity @Table(name = "student") public class Student { ... private StudentInfo studentInfo; ... @OneToOne(fetch = FetchType.EAGER, mappedBy = "student", cascade = CascadeType.ALL) public StudentInfo getStudentInfo() { return studentInfo; } public void setStudentInfo(StudentInfo studentInfo) { this.studentInfo = studentInfo; } ... }
StudentInfo.java
@Entity @Table(name="student_info") public class StudentInfo { private long studentId; private Student student; ... @GenericGenerator(name = "gen", strategy = "foreign", parameters = @Parameter(name = "property", value = "student")) @Id @GeneratedValue(generator = "gen") @Column(name = "student_id") public long getStudentId() { return studentId; } public void setStudentId(long studentId) { this.studentId = studentId; } @OneToOne(fetch = FetchType.EAGER) public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } ... }
The next relationship we are going to configure is the one-to-many relationship between the Student and the Result entities. On the Student class we declare the results as a HashSet and use the @OneToMany annotation on its getter. We need the results everytime we load the user, so we will set the fetch type as eager. The mappedBy property must have the name of the student variable which we will create on the Result entity. In that class we only need to use the @ManyToOne annotation and the @JoinColumn which will be set as the name of the student foreign key column.
Student.java
@Entity @Table(name = "student") public class Student { ... private Set<Result> resultSet = new HashSet<>(); ... @OneToMany(fetch = FetchType.EAGER, mappedBy = "student") public Set<Result> getResultSet() { return resultSet; } public void setResultSet(Set<Result> resultSet) { this.resultSet = resultSet; } ... }
Result.java
@Entity @Table(name = "results") public class Result { private Student student; ... @ManyToOne @JoinColumn(name="student_id") public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } ... }
The Student table has also a many-to-many relationship with the Courses which we need to configure. In this case we will use a HashSet for both the courses and the students. On the Student class we use the @ManyToMany annotation with a lazy fetch type, as we don’t need the student’s courses everytime we load one. On the Courses entity we again use the same annotation but this time we also need to describe the table we used to join those two tables. We will use the @JoinTable annotation for that, and configure the two columns that contain the ids.
Student.java
@Entity @Table(name = "student") public class Student { private Set<Course> courseSet = new HashSet<>(); ... @ManyToMany(fetch = FetchType.LAZY, mappedBy = "students") public Set<Course> getCourseSet() { return courseSet; } public void setCourseSet(Set<Course> courseSet) { this.courseSet = courseSet; } }
Course.java
@Entity @Table(name = "class") public class Course { ... private Set<Student> students = new HashSet<>(); ... @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinTable(name="student_class", joinColumns = {@JoinColumn(name="class_id")}, inverseJoinColumns = {@JoinColumn(name="student_id")}) public Set<Student> getStudents() { return students; } public void setStudents(Set<Student> students) { this.students = students; } ... }
Service Layer
The Service layer will be used to implement our business logic. Since this application is very simple we don’t need any additional functions, and we will only use this layer to pass the requests to the Persistence classes. We create an abstract service which will contain all the common methods and be used as a blueprint for the other services.
By using the @EnableTransactionManagement on our service classes and the @Transactional annotation on the methods that are going to perform database actions, we let the Spring handle the transaction management for us.
AbstractService.java
package com.antogeo.service; import java.util.List; public abstract class AbstractService<T> { public abstract T insert(Object o); public abstract T update(Object o); public abstract List<T> getObjectsByStudentId(long studentId); public abstract T getObjectById(long objectId); public abstract boolean deleteObjectById(long objectId); }
StudentService.java
@Service("studentService") @EnableTransactionManagement public class StudentService extends AbstractService { @Autowired private StudentDao studentDao; @Autowired private CourseService courseService; @Override @Transactional public Student insert(Object o) { return studentDao.insert(o); } @Override @Transactional public Student update(Object o) { return studentDao.update(o); } @Override @Transactional(readOnly = true) public List<Student> getObjectsByStudentId(long studentId) { return studentDao.getObjectsByStudentId(studentId); } @Override @Transactional(readOnly = true) public Student getObjectById(long objectId) { return studentDao.getObjectById(objectId); } @Override @Transactional public boolean deleteObjectById(long objectId) { return studentDao.deleteObjectById(objectId); } }
Persistence Layer
The Persistence layer is consisted by the Data Access Objects (DAO). We create an abstract class to hold the common functionality like the basic insert, update and get functions.
AbstractDao.java
package com.antogeo.dao; import java.util.List; public abstract class AbstractDao<T> { public abstract T insert(Object o); public abstract T update(Object o); public abstract List<T> getObjectsByStudentId(long studentId); public abstract T getObjectById(long objectId); public abstract boolean deleteObjectById(long objectId); }
Also to avoid code repetition we have created a class which contain the implementation of the basic hibernate functions that we need. Those are some basic methods for insert, update, select and delete, which will be called by the object daos.
HibernateUtil.java
package com.antogeo.dao; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.List; @Repository public class HibernateUtil { @Autowired private SessionFactory sessionFactory; protected Object insert(Object o) throws RuntimeException{ Session session = getSessionFactory().getCurrentSession(); session.save(o); return o; } protected Object update(Object o) throws RuntimeException{ Session session = getSessionFactory().getCurrentSession(); session.update(o); return o; } protected Object selectListById(String objectType, String column, long value){ try{ String query = "FROM " + objectType + " A WHERE A." + column + " = :value "; Session session = getSessionFactory().getCurrentSession(); List<Object> objects = session.createQuery(query).setLong("value", value).list(); return objects; }catch(RuntimeException e){ getSessionFactory().getCurrentSession().getTransaction().rollback(); System.out.println("Transaction rollback"); throw e; } } protected boolean deleteById(Class<?> type, long id) throws RuntimeException{ Session session = getSessionFactory().getCurrentSession(); Object persistentInstance = session.load(type, id); if(persistentInstance != null){ session.delete(persistentInstance); return true; } return false; } protected Object selectById(String objectType, String column, long value){ try{ String query = "FROM " + objectType + " A WHERE A." + column + " = :value "; Session session = getSessionFactory().getCurrentSession(); List<Object> objects = session.createQuery(query).setLong("value", value).list(); return objects.get(0); }catch(RuntimeException e){ getSessionFactory().getCurrentSession().getTransaction().rollback(); System.out.println("Transaction rollback"); throw e; } } protected SessionFactory getSessionFactory() { return sessionFactory; } }
Finally we create the entity daos, which will extent the AbstractDao and use the HibernateUtil methods.
StudentDao.java
package com.antogeo.dao; import com.antogeo.entity.Student; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.List; @Repository public class StudentDao extends AbstractDao { @Autowired private HibernateUtil hu; @Override public Student insert(Object o) { return (Student) hu.insert(o); } @Override public Student update(Object o) { return (Student) hu.update(o); } @Override public List<Student> getObjectsByStudentId(long studentId) { return null; } @Override public Student getObjectById(long objectId) { return (Student) hu.selectById("Student", "studentId", objectId); } @Override public boolean deleteObjectById(long objectId) { return hu.deleteById(Student.class, objectId); } }
Installation and Run
- Build the project and run the tests using mvn clean install