Hibernate maps entity classes to tables so that persistence code is annotations and a session or entity manager call rather than hand-written SQL. This session persists the student admission data and performs a batch update.
The student-admission project gets a proper schema: five entities, a fresh student_admission database, full CRUD on students and a batch approval that assigns enrolment numbers in one transaction.
Objectives
Do not copy. Read for understanding and the viva- Complete questions 25 to 28 of the manual: hibernate and jpa
- Prepare the deliverable before the lab and finish it during the session
- Be ready to explain every step in the viva
Questions Covered
Do not copy. Read for understanding and the viva| Question | Requirement | Status |
|---|---|---|
| Q25 | Create a database for the student admission lifecycle and configure Hibernate and JPA… | Complete |
| Q26 | Retrieve Student information using Hibernate and printing the value in the console | Complete |
| Q27 | Create CRUD (Create/Save, Read/Fetch, Edit/Update, Delete) using Spring MVC and… | Complete |
| Q28 | Apply batch update for student’s admission approval using Spring MVC and Hibernate | Complete |
Preparation
Do not copy. Read for understanding and the viva- Draw the admission life-cycle tables first: Student, Programme, Course, StudentCourse, AdmissionStatus. Map each with
@Entity,@Id,@OneToMany,@ManyToMany. - Configure
hibernate.cfg.xmlor Spring’sLocalSessionFactoryBeanwith the MySQL dialect andhbm2ddl.auto=updatefor the lab. - Batch update: loop with
session.updateand flush every 20 records, inside one transaction.
Question 25
Problem Statement
Write in lab recordCreate a database for the student admission lifecycle and configure Hibernate and JPA with the Spring MVC Project along with all table entities.
Solution
Write in lab recordLife cycle: a student applies to one programme and picks courses (status PENDING); the office approves or rejects; every change is recorded. Tables and relations:
| Table | Key columns | Relation |
|---|---|---|
| programme | id, code, name | one programme has many courses |
| course | id, code, title, programme_id | many courses belong to one programme |
| student | id, enrolment_no, name, email, mobile, dob, address, hostel_required, programme_id, status, applied_on | many students apply to one programme |
| student_course | id, student_id, course_id, enrolled_on | join entity between student and course (many-to-many with a date) |
| admission_status | id, student_id, status, changed_on, remark | one student has many status changes, oldest first |
Steps
- Run
student_admission.sqlin MySQL (Workbench ormysql -u root -p < student_admission.sql). It creates the database, five tables and seeds two programmes and six courses. - Point
db.propertiesatstudent_admission. - Replace
JpaConfig.java(batch size and validation mode are new) and confirmpom.xmlmatches the final listing below. - In
com.ignou.lab.admission.entityreplaceStudent.javaand addProgramme.java,Course.java,StudentCourse.java,AdmissionStatus.java. - Delete Session 5’s
AdmissionController.java,admission-form.jspandadmission-result.jsp; Q27 replaces them. Rebuild only after Q27’s files are in place, becauseAdmissionFormchanges shape.
Program
Lab record: every tab is one file of the answer. Write all of them.
-- Student admission life cycle: one programme has many courses; a student
-- applies to one programme, picks courses, and moves PENDING -> APPROVED/REJECTED.
CREATE DATABASE IF NOT EXISTS student_admission;
USE student_admission;
CREATE TABLE programme (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(10) NOT NULL UNIQUE,
name VARCHAR(80) NOT NULL
);
CREATE TABLE course (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(10) NOT NULL UNIQUE,
title VARCHAR(120) NOT NULL,
programme_id BIGINT NOT NULL,
FOREIGN KEY (programme_id) REFERENCES programme(id)
);
CREATE TABLE student (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
enrolment_no VARCHAR(12) UNIQUE,
name VARCHAR(80) NOT NULL,
email VARCHAR(120) NOT NULL,
mobile VARCHAR(10) NOT NULL,
dob DATE NOT NULL,
address VARCHAR(255) NOT NULL,
hostel_required BIT(1) NOT NULL,
programme_id BIGINT NOT NULL,
status ENUM('PENDING','APPROVED','REJECTED') NOT NULL DEFAULT 'PENDING',
applied_on DATE NOT NULL,
FOREIGN KEY (programme_id) REFERENCES programme(id)
);
-- join table with its own data (when the course was taken), so it is an entity
CREATE TABLE student_course (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
enrolled_on DATE NOT NULL,
UNIQUE (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES course(id)
);
-- every status change of every application, oldest first
CREATE TABLE admission_status (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id BIGINT NOT NULL,
status ENUM('PENDING','APPROVED','REJECTED') NOT NULL,
changed_on DATETIME(6) NOT NULL,
remark VARCHAR(255),
FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE
);
INSERT INTO programme (code, name) VALUES
('MCA', 'Master of Computer Applications'),
('BCA', 'Bachelor of Computer Applications');
INSERT INTO course (code, title, programme_id) VALUES
('MCS-218', 'Data Communication and Computer Networks', 1),
('MCS-219', 'Object Oriented Analysis and Design', 1),
('MCS-220', 'Web Technologies', 1),
('MCS-221', 'Data Warehousing and Data Mining', 1),
('BCS-011', 'Computer Basics and PC Software', 2),
('BCS-012', 'Basic Mathematics', 2);# src/main/resources/db.properties
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/student_admission
db.username=root
db.password=Ignou@1234package com.ignou.lab.admission;
import java.util.Properties;
import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@PropertySource("classpath:db.properties")
public class JpaConfig {
@Bean
public DataSource dataSource(Environment env) {
// ponytail: no connection pool; switch to HikariDataSource when more than one user hits the app
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(env.getRequiredProperty("db.driver"));
ds.setUrl(env.getRequiredProperty("db.url"));
ds.setUsername(env.getRequiredProperty("db.username"));
ds.setPassword(env.getRequiredProperty("db.password"));
return ds;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("com.ignou.lab.admission.entity"); // all five entities
emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); // Hibernate 6 detects the MySQL dialect itself
Properties props = new Properties();
props.put("hibernate.hbm2ddl.auto", "update"); // "validate" once the tables exist and you want a strict check
props.put("hibernate.show_sql", "true");
props.put("hibernate.format_sql", "true");
props.put("hibernate.jdbc.batch_size", "20"); // Q28: send UPDATEs to MySQL 20 at a time
props.put("hibernate.order_updates", "true");
props.put("jakarta.persistence.validation.mode", "none"); // the form is validated in the controller already
emf.setJpaProperties(props);
return emf;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}package com.ignou.lab.admission.entity;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "programme")
public class Programme {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 10)
private String code;
@Column(nullable = false, length = 80)
private String name;
@OneToMany(mappedBy = "programme")
private List<Course> courses = new ArrayList<>();
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<Course> getCourses() { return courses; }
public void setCourses(List<Course> courses) { this.courses = courses; }
}package com.ignou.lab.admission.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 10)
private String code;
@Column(nullable = false, length = 120)
private String title;
@ManyToOne(optional = false)
@JoinColumn(name = "programme_id")
private Programme programme;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Programme getProgramme() { return programme; }
public void setProgramme(Programme programme) { this.programme = programme; }
}package com.ignou.lab.admission.entity;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import com.ignou.lab.admission.entity.AdmissionStatus.Status;
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** assigned on approval (Q28); null while the application is pending */
@Column(name = "enrolment_no", length = 12, unique = true)
private String enrolmentNo;
@Column(nullable = false, length = 80)
private String name;
@Column(nullable = false, length = 120)
private String email;
@Column(nullable = false, length = 10)
private String mobile;
@Column(nullable = false)
private LocalDate dob;
@Column(nullable = false, length = 255)
private String address;
@Column(name = "hostel_required", nullable = false)
private boolean hostelRequired;
@ManyToOne(optional = false)
@JoinColumn(name = "programme_id")
private Programme programme;
/** current stage; the full history is in AdmissionStatus rows */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private Status status = Status.PENDING;
@Column(name = "applied_on", nullable = false)
private LocalDate appliedOn = LocalDate.now();
/** cascade + orphanRemoval: saving/deleting the student saves/deletes its rows */
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private List<StudentCourse> courses = new ArrayList<>();
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("changedOn")
private List<AdmissionStatus> history = new ArrayList<>();
/** the only way to change the status: keeps the current column and the history in step */
public void changeStatus(Status newStatus, String remark) {
this.status = newStatus;
history.add(new AdmissionStatus(this, newStatus, remark));
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEnrolmentNo() { return enrolmentNo; }
public void setEnrolmentNo(String enrolmentNo) { this.enrolmentNo = enrolmentNo; }
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 getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public LocalDate getDob() { return dob; }
public void setDob(LocalDate dob) { this.dob = dob; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public boolean isHostelRequired() { return hostelRequired; }
public void setHostelRequired(boolean hostelRequired) { this.hostelRequired = hostelRequired; }
public Programme getProgramme() { return programme; }
public void setProgramme(Programme programme) { this.programme = programme; }
public Status getStatus() { return status; }
public LocalDate getAppliedOn() { return appliedOn; }
public List<StudentCourse> getCourses() { return courses; }
public List<AdmissionStatus> getHistory() { return history; }
}package com.ignou.lab.admission.entity;
import java.time.LocalDate;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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 jakarta.persistence.UniqueConstraint;
/** Join entity between Student and Course; carries the enrolment date. */
@Entity
@Table(name = "student_course", uniqueConstraints = @UniqueConstraint(columnNames = { "student_id", "course_id" }))
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne(optional = false)
@JoinColumn(name = "course_id")
private Course course;
@Column(name = "enrolled_on", nullable = false)
private LocalDate enrolledOn = LocalDate.now();
protected StudentCourse() { }
public StudentCourse(Student student, Course course) {
this.student = student;
this.course = course;
}
public Long getId() { return id; }
public Student getStudent() { return student; }
public Course getCourse() { return course; }
public LocalDate getEnrolledOn() { return enrolledOn; }
}package com.ignou.lab.admission.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
/** One row per status change: the life cycle of an application. */
@Entity
@Table(name = "admission_status")
public class AdmissionStatus {
public enum Status { PENDING, APPROVED, REJECTED }
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "student_id")
private Student student;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private Status status;
@Column(name = "changed_on", nullable = false)
private LocalDateTime changedOn = LocalDateTime.now();
@Column(length = 255)
private String remark;
protected AdmissionStatus() { } // JPA needs a no-arg constructor
public AdmissionStatus(Student student, Status status, String remark) {
this.student = student;
this.status = status;
this.remark = remark;
}
public Long getId() { return id; }
public Student getStudent() { return student; }
public Status getStatus() { return status; }
public LocalDateTime getChangedOn() { return changedOn; }
public String getRemark() { return remark; }
}<?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>
<groupId>com.ignou.lab</groupId>
<artifactId>student-admission</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>student-admission</name>
<description>Student Admission: Spring MVC 6 + Hibernate 6 (JPA 3.1) on Tomcat 10.1</description>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>6.1.14</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.3.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
<build>
<finalName>student-admission</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>Output
Expected, checked by reading, not executed. SHOW TABLES in MySQL lists admission_status, course, programme, student, student_course. On deployment Tomcat’s log shows Hibernate starting with the five entities and, because hbm2ddl.auto=update finds every table already there, no create table statements:
HHH000412: Hibernate ORM core version 6.6.3.Final
HHH10001005: Database info: Database JDBC URL [jdbc:mysql://localhost:3306/student_admission] ... Database version: 8.0Explanation
@Entityplus@Tablename the table;@IdwithIDENTITYuses MySQL auto-increment;@Columnsets name, length and nullability so that generated DDL and the script agree.@ManyToOnewith@JoinColumnowns the foreign key (Course.programme,Student.programme, both sides ofStudentCourse).@OneToMany(mappedBy = ...)is the inverse, read-only view of the same key.- Student to Course is many-to-many, but the join row has its own data (
enrolled_on), so it is modelled as the entityStudentCourserather than@ManyToMany.cascade = ALL, orphanRemoval = trueonStudent.coursesmeans saving or deleting a student saves or deletes its rows. AdmissionStatusis the life cycle: one row per change, ordered by@OrderBy("changedOn").Student.statuskeeps the current stage as a column for cheap filtering;changeStatus()is the only method that writes it, so column and history never disagree.@Enumerated(STRING)storesPENDING, not0.JpaConfigis the Hibernate configuration: data source, entity package, provider, and thehibernate.*properties (hbm2ddl.auto,show_sql,jdbc.batch_sizefor Q28). Hibernate 6 detects the MySQL dialect from the connection, so nodialectproperty is needed.validation.mode=nonestops Hibernate re-running the Bean Validation the controller already ran.
Question 26
Problem Statement
Write in lab recordRetrieve Student information using Hibernate and printing the value in the console.
Solution
Write in lab recordSteps
- Add
ConsoleApp.javaincom.ignou.lab.admission. It boots onlyJpaConfig, so no Tomcat is involved. - Insert at least two students first (Q27’s form, or two
INSERTstatements in MySQL). - Run:
mvn -q compile exec:java -Dexec.mainClass=com.ignou.lab.admission.ConsoleApp, or Run As → Java Application.
Program
package com.ignou.lab.admission;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.ignou.lab.admission.entity.Student;
/**
* Q26: read students with the Hibernate Session API and print them on the console.
* Run: mvn -q compile exec:java -Dexec.mainClass=com.ignou.lab.admission.ConsoleApp
*/
public class ConsoleApp {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(JpaConfig.class)) {
SessionFactory sessionFactory = ctx.getBean(EntityManagerFactory.class).unwrap(SessionFactory.class);
try (Session session = sessionFactory.openSession()) {
List<Student> students = session.createQuery(
"select distinct s from Student s join fetch s.programme"
+ " left join fetch s.courses sc left join fetch sc.course order by s.id",
Student.class).getResultList();
System.out.printf("%-4s %-11s %-18s %-5s %-9s %s%n",
"ID", "ENROLMENT", "NAME", "PROG", "STATUS", "COURSES");
for (Student s : students) {
String courses = s.getCourses().stream()
.map(sc -> sc.getCourse().getCode())
.collect(Collectors.joining(","));
System.out.printf("%-4d %-11s %-18s %-5s %-9s %s%n",
s.getId(), s.getEnrolmentNo() == null ? "-" : s.getEnrolmentNo(),
s.getName(), s.getProgramme().getCode(), s.getStatus(), courses);
}
System.out.println(students.size() + " student(s)");
}
}
}
}Output
Expected console output after two applications and one approval, checked by reading, not executed. The Hibernate: block with the SELECT (one query with three joins) prints first because show_sql is on, then:
ID ENROLMENT NAME PROG STATUS COURSES
1 2026000001 Asha Verma MCA APPROVED MCS-218,MCS-220
2 - Rahul Singh MCA PENDING MCS-219
2 student(s)Explanation
- The Spring-managed
EntityManagerFactoryis Hibernate underneath;unwrap(SessionFactory.class)exposes the native API, andopenSession()gives a HibernateSession. - The query is HQL.
join fetch s.programmeandleft join fetch s.courses sc left join fetch sc.courseload the related rows in the same SELECT; without them eachgetProgramme()orgetCourses()would fire another query (the N+1 problem) or fail withLazyInitializationExceptiononce the session is closed. select distinctcollapses the duplicateStudentrows the join produces (one per course).printfwith fixed widths makes the console a table; enrolment shows-while it is null, which is every pending application.
Question 27
Problem Statement
Write in lab recordCreate CRUD (Create/Save, Read/Fetch, Edit/Update, Delete) using Spring MVC and Hibernation.
Solution
Write in lab recordSteps
- Replace
StudentRepository.java(all persistence),AdmissionForm.java(now carriesid,programmeId,courseIds) andStudentController.java(all URLs under/students). - Replace
students.jspand addstudent-form.jspinWEB-INF/views.head.jspffrom Session 5 stays. - In
home.jspchange the “Apply for admission” link to/students/new. - Rebuild, redeploy and walk the cycle:
/students(empty) → New application → Save → row appears → Edit → change the mobile, tick another course → Save → Delete → confirm → row gone. Watch Tomcat’s console for the SQL at each step.
| Operation | URL and method | Repository call | SQL Hibernate sends |
|---|---|---|---|
| Create | GET /students/new, POST /students/save | save(form) with id null | INSERT student, INSERT student_course per course, INSERT admission_status |
| Read | GET /students | findAll() | one SELECT with joins |
| Update | GET /students/7/edit, POST /students/save | save(form) with id 7 | UPDATE student, DELETE and INSERT student_course as needed |
| Delete | POST /students/7/delete | delete(7) | DELETE admission_status, DELETE student_course, DELETE student |
Program
Lab record: every tab is one file of the answer. Write all of them.
package com.ignou.lab.admission.repo;
import java.time.Year;
import java.util.List;
import java.util.NoSuchElementException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.ignou.lab.admission.entity.AdmissionStatus.Status;
import com.ignou.lab.admission.entity.Course;
import com.ignou.lab.admission.entity.Programme;
import com.ignou.lab.admission.entity.Student;
import com.ignou.lab.admission.entity.StudentCourse;
import com.ignou.lab.admission.web.AdmissionForm;
/** Every public method runs in one transaction; commit happens when it returns. */
@Repository
@Transactional
public class StudentRepository {
@PersistenceContext
private EntityManager em;
// ---- lookups for the form ----
public List<Programme> programmes() {
return em.createQuery("from Programme p order by p.code", Programme.class).getResultList();
}
public List<Course> courses() {
return em.createQuery("from Course c order by c.code", Course.class).getResultList();
}
// ---- Read ----
@Transactional(readOnly = true)
public List<Student> findAll() {
// join fetch: programme and courses are loaded now, so the JSP never touches a closed session
return em.createQuery(
"select distinct s from Student s join fetch s.programme"
+ " left join fetch s.courses sc left join fetch sc.course order by s.id",
Student.class).getResultList();
}
public Student find(Long id) {
Student s = em.find(Student.class, id);
if (s == null) {
throw new NoSuchElementException("No student with id " + id); // ponytail: 500 page is fine for the lab
}
return s;
}
/** the edit page needs the form filled from a managed entity (courses are lazy) */
@Transactional(readOnly = true)
public AdmissionForm formFor(Long id) {
return AdmissionForm.from(find(id));
}
// ---- Create / Update ----
public Student save(AdmissionForm f) {
Student s = f.getId() == null ? new Student() : find(f.getId());
s.setName(f.getName());
s.setEmail(f.getEmail());
s.setMobile(f.getMobile());
s.setDob(f.getDob());
s.setAddress(f.getAddress());
s.setHostelRequired(f.getHostelRequired());
s.setProgramme(em.getReference(Programme.class, f.getProgrammeId())); // no SELECT, just the FK
s.getCourses().clear(); // orphanRemoval deletes the old rows
for (Long courseId : f.getCourseIds()) {
s.getCourses().add(new StudentCourse(s, em.getReference(Course.class, courseId)));
}
if (s.getId() == null) {
s.changeStatus(Status.PENDING, "Application received");
em.persist(s); // INSERT student + student_course + admission_status
} // managed entity: UPDATE happens on commit
return s;
}
// ---- Delete ----
public void delete(Long id) {
em.remove(find(id)); // cascades to student_course and admission_status
}
// ---- Q28: batch approval ----
/** Approves every pending id in ONE transaction; UPDATEs go to MySQL in batches of 20. */
public int approve(List<Long> ids) {
int approved = 0;
for (Long id : ids) {
Student s = find(id);
if (s.getStatus() != Status.PENDING) {
continue;
}
s.setEnrolmentNo(String.format("%d%06d", Year.now().getValue(), id)); // e.g. 2026000007
s.changeStatus(Status.APPROVED, "Approved in batch");
if (++approved % 20 == 0) {
em.flush(); // push this batch of UPDATE/INSERT statements
em.clear(); // drop them from memory before loading the next 20
}
}
return approved; // commit flushes whatever is left
}
}package com.ignou.lab.admission.web;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.springframework.format.annotation.DateTimeFormat;
import com.ignou.lab.admission.entity.Student;
import com.ignou.lab.admission.entity.StudentCourse;
/** Session 6 version: programme and courses are database ids; id is set when editing. */
public class AdmissionForm {
private Long id;
@NotBlank
@Size(min = 3, max = 80)
private String name;
@NotBlank
@Email
private String email;
@NotBlank
@Pattern(regexp = "[6-9][0-9]{9}", message = "must be a 10-digit Indian mobile number")
private String mobile;
@NotNull
@Past
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate dob;
@NotBlank
@Size(max = 255)
private String address;
@NotNull(message = "choose a programme")
private Long programmeId;
@NotNull(message = "choose Yes or No")
private Boolean hostelRequired;
@NotEmpty(message = "pick at least one course")
private List<Long> courseIds = new ArrayList<>();
/** entity -> form, for the edit page */
public static AdmissionForm from(Student s) {
AdmissionForm f = new AdmissionForm();
f.id = s.getId();
f.name = s.getName();
f.email = s.getEmail();
f.mobile = s.getMobile();
f.dob = s.getDob();
f.address = s.getAddress();
f.programmeId = s.getProgramme().getId();
f.hostelRequired = s.isHostelRequired();
for (StudentCourse sc : s.getCourses()) {
f.courseIds.add(sc.getCourse().getId());
}
return f;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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 getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public LocalDate getDob() { return dob; }
public void setDob(LocalDate dob) { this.dob = dob; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public Long getProgrammeId() { return programmeId; }
public void setProgrammeId(Long programmeId) { this.programmeId = programmeId; }
public Boolean getHostelRequired() { return hostelRequired; }
public void setHostelRequired(Boolean hostelRequired) { this.hostelRequired = hostelRequired; }
public List<Long> getCourseIds() { return courseIds; }
public void setCourseIds(List<Long> courseIds) { this.courseIds = courseIds; }
}package com.ignou.lab.admission.web;
import java.time.LocalDate;
import java.util.List;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.ignou.lab.admission.entity.Course;
import com.ignou.lab.admission.entity.Programme;
import com.ignou.lab.admission.entity.Student;
import com.ignou.lab.admission.repo.StudentRepository;
/** Replaces Session 5's AdmissionController: create, read, update, delete and batch approve. */
@Controller
@RequestMapping("/students")
public class StudentController {
private final StudentRepository repo;
public StudentController(StudentRepository repo) {
this.repo = repo;
}
@ModelAttribute("programmes")
public List<Programme> programmes() {
return repo.programmes();
}
@ModelAttribute("courseList")
public List<Course> courseList() {
return repo.courses();
}
@ModelAttribute("today")
public LocalDate today() {
return LocalDate.now();
}
// Read
@GetMapping
public String list(Model model) {
model.addAttribute("students", repo.findAll());
return "students";
}
// Create (form)
@GetMapping("/new")
public String createForm(Model model) {
model.addAttribute("admission", new AdmissionForm());
return "student-form";
}
// Update (form)
@GetMapping("/{id}/edit")
public String editForm(@PathVariable Long id, Model model) {
model.addAttribute("admission", repo.formFor(id));
return "student-form";
}
// Create or Update (submit)
@PostMapping("/save")
public String save(@Valid @ModelAttribute("admission") AdmissionForm form,
BindingResult result, RedirectAttributes redirect) {
if (result.hasErrors()) {
return "student-form";
}
Student saved = repo.save(form);
redirect.addFlashAttribute("message", "Saved " + saved.getName() + " (id " + saved.getId() + ")");
return "redirect:/students"; // POST-redirect-GET: refresh does not resubmit
}
// Delete
@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id, RedirectAttributes redirect) {
repo.delete(id);
redirect.addFlashAttribute("message", "Deleted student " + id);
return "redirect:/students";
}
// Q28: batch approval of the ticked rows
@PostMapping("/approve")
public String approve(@RequestParam(name = "ids", required = false) List<Long> ids,
RedirectAttributes redirect) {
int n = ids == null ? 0 : repo.approve(ids);
redirect.addFlashAttribute("message", n + " application(s) approved in one transaction");
return "redirect:/students";
}
}<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<%@ include file="head.jspf" %>
<title>${empty admission.id ? 'New application' : 'Edit application'}</title>
</head>
<body>
<nav class="navbar navbar-dark"><div class="container"><span class="navbar-brand">IGNOU Student Admission</span></div></nav>
<div class="container">
<div class="card card-form">
<div class="card-body">
<h1 class="h3 card-title">${empty admission.id ? 'New application' : 'Edit application'}</h1>
<form:form modelAttribute="admission" method="post" action="${pageContext.request.contextPath}/students/save" cssClass="row g-3">
<form:hidden path="id"/>
<div class="col-12">
<form:label path="name" cssClass="form-label required">Full name</form:label>
<form:input path="name" cssClass="form-control" cssErrorClass="form-control is-invalid"
required="required" minlength="3" maxlength="80"/>
<form:errors path="name" cssClass="invalid-feedback"/>
</div>
<div class="col-md-6">
<form:label path="email" cssClass="form-label required">Email</form:label>
<form:input path="email" type="email" cssClass="form-control" cssErrorClass="form-control is-invalid" required="required"/>
<form:errors path="email" cssClass="invalid-feedback"/>
</div>
<div class="col-md-6">
<form:label path="mobile" cssClass="form-label required">Mobile</form:label>
<form:input path="mobile" cssClass="form-control" cssErrorClass="form-control is-invalid"
required="required" pattern="[6-9][0-9]{9}" title="10 digits, starting 6 to 9"/>
<form:errors path="mobile" cssClass="invalid-feedback"/>
</div>
<div class="col-md-6">
<form:label path="dob" cssClass="form-label required">Date of birth</form:label>
<form:input path="dob" type="date" cssClass="form-control" cssErrorClass="form-control is-invalid"
required="required" max="${today}"/>
<form:errors path="dob" cssClass="invalid-feedback"/>
</div>
<div class="col-md-6">
<form:label path="programmeId" cssClass="form-label required">Programme</form:label>
<form:select path="programmeId" cssClass="form-select" cssErrorClass="form-select is-invalid" required="required">
<form:option value="" label="-- choose --"/>
<form:options items="${programmes}" itemValue="id" itemLabel="name"/>
</form:select>
<form:errors path="programmeId" cssClass="invalid-feedback"/>
</div>
<div class="col-12">
<form:label path="address" cssClass="form-label required">Address</form:label>
<form:textarea path="address" rows="3" cssClass="form-control" cssErrorClass="form-control is-invalid"
required="required" maxlength="255"/>
<form:errors path="address" cssClass="invalid-feedback"/>
</div>
<div class="col-md-6">
<label class="form-label required">Hostel required?</label>
<div class="form-check form-check-inline">
<form:radiobutton path="hostelRequired" value="true" cssClass="form-check-input" label=" Yes" required="required"/>
</div>
<div class="form-check form-check-inline">
<form:radiobutton path="hostelRequired" value="false" cssClass="form-check-input" label=" No"/>
</div>
<form:errors path="hostelRequired" cssClass="text-danger small d-block"/>
</div>
<div class="col-md-6">
<label class="form-label required">Courses</label>
<c:forEach items="${courseList}" var="course">
<div class="form-check">
<form:checkbox path="courseIds" value="${course.id}" cssClass="form-check-input" label=" ${course.code} ${course.title}"/>
</div>
</c:forEach>
<form:errors path="courseIds" cssClass="text-danger small d-block"/>
</div>
<div class="col-12 text-end">
<a class="btn btn-outline-secondary" href="${pageContext.request.contextPath}/students">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form:form>
</div>
</div>
</div>
</body>
</html>Output
Expected, checked by reading, not executed. After saving a new application the list page shows a green alert Saved Asha Verma (id 1) and a row:
[ ] 1 - Asha Verma MCA MCS-218, MCS-220 PENDING 2026-09-26 Edit DeleteTomcat’s console for that save:
Hibernate: insert into student (address,applied_on,dob,email,enrolment_no,hostel_required,mobile,name,programme_id,status) values (?,?,?,?,?,?,?,?,?,?)
Hibernate: insert into student_course (course_id,enrolled_on,student_id) values (?,?,?)
Hibernate: insert into student_course (course_id,enrolled_on,student_id) values (?,?,?)
Hibernate: insert into admission_status (changed_on,remark,status,student_id) values (?,?,?,?)Editing and saving with one course removed prints one update student ... and one delete from student_course where id=?. Delete prints the three DELETEs in child-first order.
Explanation
- Create and update share one method.
save(form)either makes anew Student()or loads the existing one withem.find, copies the fields, and rebuilds the course list. A loaded entity is managed: Hibernate compares it with the snapshot at commit and issues an UPDATE only for what changed (dirty checking).persistis called only for a new object. em.getReference(Programme.class, id)returns a proxy holding just the id; setting it writes the foreign key without a SELECT.- Clearing
s.getCourses()and adding newStudentCourseobjects is enough:orphanRemovaldeletes rows no longer in the list,cascadeinserts the new ones. formFor(id)converts entity to form inside the transaction becausecoursesis lazy; converting in the controller would hit a closed session.- The controller follows POST-redirect-GET: after a POST it redirects to
/students, so a browser refresh does not save twice;RedirectAttributes.addFlashAttributecarries the message across the redirect. - Delete is a POST button, not a link, so a crawler or a prefetching browser cannot delete rows. The Delete buttons use the HTML
formattribute to point at small forms outside the approval form, because forms cannot nest.
Question 28
Problem Statement
Write in lab recordApply batch update for student’s admission approval using Spring MVC and Hibernate.
Solution
Write in lab recordSteps
StudentRepository.approve()andStudentController.approve()are in the Q27 listings;students.jspbelow adds a checkbox to everyPENDINGrow and an “Approve selected” button posting to/students/approve.JpaConfig(Q25) setshibernate.jdbc.batch_size=20andhibernate.order_updates=true.- Create five applications, tick three, press Approve selected. The three rows turn green with enrolment numbers; the alert reads
3 application(s) approved in one transaction. - Prove it is one transaction: temporarily change
%06dinapprove()to%swith a text longer than 12 characters so the third UPDATE fails on column length; after the exception none of the three is approved.
The method under test, from StudentRepository.java:
public int approve(List<Long> ids) {
int approved = 0;
for (Long id : ids) {
Student s = find(id);
if (s.getStatus() != Status.PENDING) {
continue;
}
s.setEnrolmentNo(String.format("%d%06d", Year.now().getValue(), id));
s.changeStatus(Status.APPROVED, "Approved in batch");
if (++approved % 20 == 0) {
em.flush();
em.clear();
}
}
return approved;
}Program
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<%@ include file="head.jspf" %>
<title>Students</title>
</head>
<body>
<nav class="navbar navbar-dark"><div class="container"><span class="navbar-brand">IGNOU Student Admission</span></div></nav>
<div class="container mt-4">
<c:if test="${not empty message}">
<div class="alert alert-success">${message}</div>
</c:if>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 m-0">Applications</h1>
<a class="btn btn-primary" href="${pageContext.request.contextPath}/students/new">New application</a>
</div>
<%-- one form around the table: the ticked ids go to /students/approve --%>
<form method="post" action="${pageContext.request.contextPath}/students/approve">
<table class="table table-striped align-middle">
<thead>
<tr><th></th><th>Id</th><th>Enrolment</th><th>Name</th><th>Programme</th><th>Courses</th><th>Status</th><th>Applied</th><th></th></tr>
</thead>
<tbody>
<c:forEach items="${students}" var="s">
<tr>
<td><c:if test="${s.status == 'PENDING'}"><input class="form-check-input" type="checkbox" name="ids" value="${s.id}"></c:if></td>
<td>${s.id}</td>
<td>${empty s.enrolmentNo ? '-' : s.enrolmentNo}</td>
<td><c:out value="${s.name}"/></td>
<td>${s.programme.code}</td>
<td><c:forEach items="${s.courses}" var="sc" varStatus="st">${sc.course.code}<c:if test="${!st.last}">, </c:if></c:forEach></td>
<td><span class="badge ${s.status == 'APPROVED' ? 'text-bg-success' : s.status == 'REJECTED' ? 'text-bg-danger' : 'text-bg-warning'}">${s.status}</span></td>
<td>${s.appliedOn}</td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="${pageContext.request.contextPath}/students/${s.id}/edit">Edit</a>
<%-- the form attribute links this button to a form outside the approve form (forms cannot nest) --%>
<button class="btn btn-sm btn-outline-danger" type="submit" form="delete-${s.id}"
onclick="return confirm('Delete student ${s.id}?')">Delete</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
<button type="submit" class="btn btn-success">Approve selected</button>
</form>
<c:forEach items="${students}" var="s">
<form id="delete-${s.id}" method="post" action="${pageContext.request.contextPath}/students/${s.id}/delete"></form>
</c:forEach>
</div>
</body>
</html>Output
Expected, checked by reading, not executed. After approving ids 1, 2 and 4 the list shows:
1 2026000001 Asha Verma MCA MCS-218, MCS-220 APPROVED 2026-09-26 Edit Delete
2 2026000002 Rahul Singh MCA MCS-219 APPROVED 2026-09-26 Edit Delete
[ ] 3 - Priya Nair BCA BCS-011 PENDING 2026-09-26 Edit Delete
4 2026000004 Amit Kumar MCA MCS-220, MCS-221 APPROVED 2026-09-26 Edit Delete
[ ] 5 - Sunita Devi BCA BCS-012 PENDING 2026-09-26 Edit DeleteTomcat’s console prints the SELECT for each find, then at commit three update student set ... status=? where id=? statements followed by three insert into admission_status statements, grouped because of order_updates. SELECT * FROM admission_status in MySQL shows two rows for each approved student: PENDING from the application and APPROVED from this batch.
Explanation
- The browser sends the ticked ids as repeated
ids=1&ids=2&ids=4;@RequestParam List<Long> idscollects them. Untick everything andidsis absent, so it isrequired = falsewith a null check. approve()is one@Transactionalmethod, so the whole batch is one transaction: all rows are approved or, if any UPDATE fails, the rollback leaves every rowPENDING.- Inside the loop nothing is sent to MySQL; each managed
Studentis marked dirty. At flush time Hibernate emits the UPDATEs, and withhibernate.jdbc.batch_size=20the JDBC driver sends them in groups of 20 statements per round trip instead of one each. flush()thenclear()every 20 rows bounds memory: the persistence context does not hold thousands of entities for a long batch. For a lab-sized list the loop ends before the first flush and the commit does it all.- The status guard skips rows that are already approved or rejected, so re-submitting the same ids is harmless and returns a count of 0.
- The enrolment number is year plus zero-padded id, unique because the id is; a real registry would take the next value from a sequence table inside the same transaction.
Viva Questions
Do not copy. Read for understanding and the viva- Q: Why is
StudentCoursean entity instead of@ManyToMany? A: The join row carriesenrolled_on;@ManyToManycannot hold extra columns. - Q: What does
orphanRemoval = truedo? A: When a child is removed from the collection of a managed parent, Hibernate deletes its row at flush. - Q: Managed, detached, transient: what are they? A: Transient: new object, no row. Managed: loaded or persisted inside an open persistence context; changes are tracked. Detached: was managed, the context closed; changes are not tracked.
- Q: What is
LazyInitializationException? A: Touching a lazy collection after the session closed. Fix withjoin fetchor by doing the access inside the transaction. - Q: What is the N+1 problem? A: One query for the list and one more per row for a relation;
join fetchturns it into one query. - Q: How does the batch approval stay atomic? A: One
@Transactionalmethod; commit at the end or rollback on exception, nothing in between is visible. - Q: Why
flushandclearevery 20? A:flushsends the pending statements,clearfrees the managed entities; together they keep memory flat on large batches. - Q: Where is the Hibernate dialect configured? A: Nowhere; Hibernate 6 reads the database metadata from the connection and picks
MySQLDialectitself.
Common Mistakes
Do not copy. Read for understanding and the viva- Accessing
student.coursesin the JSP after afindAllwithoutjoin fetch, then reporting aLazyInitializationExceptionas a Hibernate bug. - Calling
em.mergeon a detached entity for every update instead of loading and changing the managed one; the collection handling gets unpredictable. - Putting
@Transactionalon the controller instead of the repository, which drags the JSP rendering into the transaction. - Deleting through a GET link; browsers prefetch links and delete rows nobody clicked.
- Setting
hbm2ddl.auto=createin the lab and losing every row on each redeploy. - Forgetting
distinctin the fetch-join query and showing each student once per course.
Session Summary
Write in lab record- Question 25:
student_admissionschema (five tables), entitiesProgramme,Course,Student,StudentCourse,AdmissionStatus, Hibernate configured inJpaConfig - Question 26:
ConsoleAppreading students through the HibernateSessionand HQL fetch joins, printed as a console table - Question 27:
StudentRepositoryandStudentControllerwith list, new, edit, save and delete, viewsstudents.jspandstudent-form.jsp - Question 28: batch approval of ticked applications in one transaction with JDBC batching, enrolment numbers assigned, history rows written