← Back to Lectures
Practical Work 9

Library Management System - Database Persistence

Migrate from CSV to database storage using JDBC with H2 embedded database

Duration 4 hours
Difficulty Advanced
Session 9 - JDBC & Database Access

Objectives

By the end of this practical work, you will be able to:

  • Understand JDBC architecture and components
  • Connect to a database using JDBC
  • Create tables using DDL statements
  • Implement CRUD operations with PreparedStatements
  • Handle database exceptions properly
  • Use transactions for data integrity
  • Apply the DAO (Data Access Object) pattern

Prerequisites

  • Completed Practical Work 8 (Library with Collections)
  • Understanding of SQL basics (SELECT, INSERT, UPDATE, DELETE)
  • Maven or Gradle for dependency management

Project Setup

Step 0.1: Add H2 Database Dependency

Add the H2 embedded database to your project:

Maven (pom.xml):

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

Gradle (build.gradle):

dependencies {
    implementation 'com.h2database:h2:2.2.224'
}
Why H2? H2 is an embedded database - no installation required! Perfect for learning and testing. It stores data in a local file.

Project Structure Update

library-system
src
com.library
model
(existing model classes)
dao
DatabaseManager.java
MediaItemDao.java
MemberDao.java
LoanDao.java
service
MediaCatalog.java
MemberService.java
LoanService.java
app
LibraryApp.java
data
library.mv.db (H2 database file)

Part 1: Database Connection Management

Step 1.1: Create DatabaseManager Class

Centralize database connection management:

package com.library.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class DatabaseManager {
    // H2 database URL - file-based storage in 'data' folder
    private static final String DB_URL = "jdbc:h2:./data/library";  // (#1:JDBC URL)
    private static final String DB_USER = "sa";
    private static final String DB_PASSWORD = "";

    private static DatabaseManager instance;
    private Connection connection;

    private DatabaseManager() {
        // Private constructor for singleton
    }

    public static synchronized DatabaseManager getInstance() {  // (#2:Singleton pattern)
        if (instance == null) {
            instance = new DatabaseManager();
        }
        return instance;
    }

    // Get a connection to the database
    public Connection getConnection() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);  // (#3:Get connection)
        }
        return connection;
    }

    // Initialize database schema
    public void initializeDatabase() throws SQLException {
        try (Connection conn = getConnection();
             Statement stmt = conn.createStatement()) {

            // Create MEMBERS table
            stmt.execute("""
                CREATE TABLE IF NOT EXISTS members (
                    id VARCHAR(50) PRIMARY KEY,
                    name VARCHAR(200) NOT NULL,
                    email VARCHAR(200) UNIQUE NOT NULL,
                    phone VARCHAR(50),
                    join_date DATE DEFAULT CURRENT_DATE
                )
                """);  // (#4:DDL statement)

            // Create MEDIA_ITEMS table
            stmt.execute("""
                CREATE TABLE IF NOT EXISTS media_items (
                    id VARCHAR(50) PRIMARY KEY,
                    media_type VARCHAR(20) NOT NULL,
                    title VARCHAR(300) NOT NULL,
                    year INT,
                    status VARCHAR(30) DEFAULT 'AVAILABLE',
                    -- Book specific
                    author VARCHAR(200),
                    isbn VARCHAR(30),
                    category VARCHAR(50),
                    page_count INT,
                    -- DVD specific
                    director VARCHAR(200),
                    duration_minutes INT,
                    rating VARCHAR(10),
                    -- Magazine specific
                    publisher VARCHAR(200),
                    issue_number INT,
                    month VARCHAR(20)
                )
                """);

            // Create LOANS table
            stmt.execute("""
                CREATE TABLE IF NOT EXISTS loans (
                    id VARCHAR(50) PRIMARY KEY,
                    item_id VARCHAR(50) NOT NULL,
                    member_id VARCHAR(50) NOT NULL,
                    checkout_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    due_date DATE NOT NULL,
                    return_date TIMESTAMP,
                    late_fee DECIMAL(10,2) DEFAULT 0,
                    FOREIGN KEY (item_id) REFERENCES media_items(id),
                    FOREIGN KEY (member_id) REFERENCES members(id)
                )
                """);  // (#5:Foreign key constraints)

            // Create indexes for faster lookups
            stmt.execute("CREATE INDEX IF NOT EXISTS idx_loans_member ON loans(member_id)");
            stmt.execute("CREATE INDEX IF NOT EXISTS idx_loans_item ON loans(item_id)");
            stmt.execute("CREATE INDEX IF NOT EXISTS idx_media_status ON media_items(status)");

            System.out.println("Database initialized successfully");
        }
    }

    // Close connection
    public void closeConnection() {
        if (connection != null) {
            try {
                connection.close();
                System.out.println("Database connection closed");
            } catch (SQLException e) {
                System.err.println("Error closing connection: " + e.getMessage());
            }
        }
    }
}
Code Annotations:
  1. JDBC URL: Protocol:database:path format
  2. Singleton: One instance manages all connections
  3. DriverManager: Factory for database connections
  4. DDL: Data Definition Language for schema
  5. Foreign keys: Ensure referential integrity

Part 2: Member DAO Implementation

Step 2.1: Create MemberDao Class

Implement CRUD operations for members:

package com.library.dao;

import com.library.model.Member;
import java.sql.*;
import java.time.LocalDate;
import java.util.*;

public class MemberDao {
    private final DatabaseManager dbManager;

    public MemberDao() {
        this.dbManager = DatabaseManager.getInstance();
    }

    // Create (Insert)
    public void insert(Member member) throws SQLException {
        String sql = """
            INSERT INTO members (id, name, email, phone, join_date)
            VALUES (?, ?, ?, ?, ?)
            """;

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {  // (#1:PreparedStatement)

            pstmt.setString(1, member.getId());        // (#2:Set parameters by index)
            pstmt.setString(2, member.getName());
            pstmt.setString(3, member.getEmail());
            pstmt.setString(4, member.getPhone());
            pstmt.setDate(5, Date.valueOf(member.getJoinDate()));  // (#3:Convert LocalDate)

            pstmt.executeUpdate();  // (#4:Execute INSERT)
        }
    }

    // Read (Select by ID)
    public Optional<Member> findById(String id) throws SQLException {
        String sql = "SELECT * FROM members WHERE id = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, id);

            try (ResultSet rs = pstmt.executeQuery()) {  // (#5:Execute SELECT)
                if (rs.next()) {
                    return Optional.of(mapResultSetToMember(rs));
                }
            }
        }
        return Optional.empty();
    }

    // Read all
    public List<Member> findAll() throws SQLException {
        String sql = "SELECT * FROM members ORDER BY name";
        List<Member> members = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {  // (#6:Iterate ResultSet)
                members.add(mapResultSetToMember(rs));
            }
        }
        return members;
    }

    // Update
    public void update(Member member) throws SQLException {
        String sql = """
            UPDATE members
            SET name = ?, email = ?, phone = ?
            WHERE id = ?
            """;

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, member.getName());
            pstmt.setString(2, member.getEmail());
            pstmt.setString(3, member.getPhone());
            pstmt.setString(4, member.getId());

            int rowsAffected = pstmt.executeUpdate();  // (#7:Returns row count)
            if (rowsAffected == 0) {
                throw new SQLException("Member not found: " + member.getId());
            }
        }
    }

    // Delete
    public boolean delete(String id) throws SQLException {
        String sql = "DELETE FROM members WHERE id = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, id);
            return pstmt.executeUpdate() > 0;
        }
    }

    // Find by email
    public Optional<Member> findByEmail(String email) throws SQLException {
        String sql = "SELECT * FROM members WHERE LOWER(email) = LOWER(?)";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, email);

            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(mapResultSetToMember(rs));
                }
            }
        }
        return Optional.empty();
    }

    // Check if email exists
    public boolean emailExists(String email) throws SQLException {
        String sql = "SELECT COUNT(*) FROM members WHERE LOWER(email) = LOWER(?)";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, email);

            try (ResultSet rs = pstmt.executeQuery()) {
                return rs.next() && rs.getInt(1) > 0;
            }
        }
    }

    // Count members
    public int count() throws SQLException {
        String sql = "SELECT COUNT(*) FROM members";

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            return rs.next() ? rs.getInt(1) : 0;
        }
    }

    // Helper: Map ResultSet row to Member object
    private Member mapResultSetToMember(ResultSet rs) throws SQLException {  // (#8:Result mapping)
        Member member = new Member(
            rs.getString("id"),
            rs.getString("name"),
            rs.getString("email")
        );
        member.setPhone(rs.getString("phone"));

        Date joinDate = rs.getDate("join_date");
        if (joinDate != null) {
            member.setJoinDate(joinDate.toLocalDate());
        }

        return member;
    }
}
Code Annotations:
  1. PreparedStatement: Prevents SQL injection, improves performance
  2. Parameter index: Starts at 1, not 0
  3. Date conversion: JDBC uses java.sql.Date
  4. executeUpdate: For INSERT/UPDATE/DELETE
  5. executeQuery: For SELECT, returns ResultSet
  6. ResultSet iteration: next() moves cursor forward
  7. Row count: Returns number of affected rows
  8. Result mapping: Convert database row to Java object

Part 3: MediaItem DAO Implementation

Step 3.1: Create MediaItemDao Class

package com.library.dao;

import com.library.model.*;
import java.sql.*;
import java.util.*;

public class MediaItemDao {
    private final DatabaseManager dbManager;

    public MediaItemDao() {
        this.dbManager = DatabaseManager.getInstance();
    }

    // Insert media item
    public void insert(MediaItem item) throws SQLException {
        String sql = """
            INSERT INTO media_items (
                id, media_type, title, year, status,
                author, isbn, category, page_count,
                director, duration_minutes, rating,
                publisher, issue_number, month
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """;

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            // Common fields
            pstmt.setString(1, item.getId());
            pstmt.setString(2, item.getMediaType());
            pstmt.setString(3, item.getTitle());
            pstmt.setInt(4, item.getYear());
            pstmt.setString(5, item.getStatus().name());

            // Initialize type-specific fields to null
            for (int i = 6; i <= 15; i++) {
                pstmt.setNull(i, Types.VARCHAR);  // (#1:Set null explicitly)
            }

            // Set type-specific fields
            if (item instanceof Book book) {
                pstmt.setString(6, book.getAuthor());
                pstmt.setString(7, book.getIsbn());
                pstmt.setString(8, book.getCategory().name());
                pstmt.setInt(9, book.getPageCount());
            } else if (item instanceof DVD dvd) {
                pstmt.setString(10, dvd.getDirector());
                pstmt.setInt(11, dvd.getDurationMinutes());
                pstmt.setString(12, dvd.getRating());
            } else if (item instanceof Magazine mag) {
                pstmt.setString(13, mag.getPublisher());
                pstmt.setInt(14, mag.getIssueNumber());
                pstmt.setString(15, mag.getMonth());
            }

            pstmt.executeUpdate();
        }
    }

    // Find by ID
    public Optional<MediaItem> findById(String id) throws SQLException {
        String sql = "SELECT * FROM media_items WHERE id = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, id);

            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(mapResultSetToMediaItem(rs));
                }
            }
        }
        return Optional.empty();
    }

    // Find all
    public List<MediaItem> findAll() throws SQLException {
        String sql = "SELECT * FROM media_items ORDER BY title";
        List<MediaItem> items = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                items.add(mapResultSetToMediaItem(rs));
            }
        }
        return items;
    }

    // Find by status
    public List<MediaItem> findByStatus(MediaStatus status) throws SQLException {
        String sql = "SELECT * FROM media_items WHERE status = ?";
        List<MediaItem> items = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, status.name());

            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    items.add(mapResultSetToMediaItem(rs));
                }
            }
        }
        return items;
    }

    // Search by title (partial match)
    public List<MediaItem> searchByTitle(String searchTerm) throws SQLException {
        String sql = "SELECT * FROM media_items WHERE LOWER(title) LIKE LOWER(?)";  // (#2:LIKE search)
        List<MediaItem> items = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, "%" + searchTerm + "%");  // (#3:Wildcards)

            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    items.add(mapResultSetToMediaItem(rs));
                }
            }
        }
        return items;
    }

    // Update status
    public void updateStatus(String id, MediaStatus status) throws SQLException {
        String sql = "UPDATE media_items SET status = ? WHERE id = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, status.name());
            pstmt.setString(2, id);
            pstmt.executeUpdate();
        }
    }

    // Delete
    public boolean delete(String id) throws SQLException {
        String sql = "DELETE FROM media_items WHERE id = ?";

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, id);
            return pstmt.executeUpdate() > 0;
        }
    }

    // Count by type
    public Map<String, Integer> countByType() throws SQLException {
        String sql = "SELECT media_type, COUNT(*) as cnt FROM media_items GROUP BY media_type";  // (#4:Aggregate query)
        Map<String, Integer> counts = new HashMap<>();

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                counts.put(rs.getString("media_type"), rs.getInt("cnt"));
            }
        }
        return counts;
    }

    // Helper: Map ResultSet to MediaItem
    private MediaItem mapResultSetToMediaItem(ResultSet rs) throws SQLException {
        String type = rs.getString("media_type");
        String id = rs.getString("id");
        String title = rs.getString("title");
        int year = rs.getInt("year");
        MediaStatus status = MediaStatus.valueOf(rs.getString("status"));

        MediaItem item;

        switch (type) {
            case "BOOK" -> {
                String author = rs.getString("author");
                String isbn = rs.getString("isbn");
                BookCategory category = BookCategory.valueOf(rs.getString("category"));
                int pages = rs.getInt("page_count");
                item = new Book(id, title, author, year, isbn, category, pages);
            }
            case "DVD" -> {
                String director = rs.getString("director");
                int duration = rs.getInt("duration_minutes");
                String rating = rs.getString("rating");
                item = new DVD(id, title, director, year, duration, rating);
            }
            case "MAGAZINE" -> {
                String publisher = rs.getString("publisher");
                int issue = rs.getInt("issue_number");
                String month = rs.getString("month");
                item = new Magazine(id, title, publisher, year, issue, month);
            }
            default -> throw new SQLException("Unknown media type: " + type);
        }

        item.setStatus(status);
        return item;
    }
}
Code Annotations:
  1. setNull: Explicitly set NULL for unused columns
  2. LIKE query: Pattern matching in SQL
  3. Wildcards: % matches any sequence of characters
  4. GROUP BY: Aggregate data by category

Part 4: Loan DAO with Transactions

Step 4.1: Create LoanDao with Transaction Support

package com.library.dao;

import com.library.model.*;
import java.sql.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

public class LoanDao {
    private final DatabaseManager dbManager;
    private final MediaItemDao mediaItemDao;

    public LoanDao() {
        this.dbManager = DatabaseManager.getInstance();
        this.mediaItemDao = new MediaItemDao();
    }

    // Create loan with transaction
    public void createLoan(Loan loan) throws SQLException {
        Connection conn = null;
        try {
            conn = dbManager.getConnection();
            conn.setAutoCommit(false);  // (#1:Start transaction)

            // 1. Insert loan record
            String loanSql = """
                INSERT INTO loans (id, item_id, member_id, checkout_date, due_date)
                VALUES (?, ?, ?, ?, ?)
                """;

            try (PreparedStatement pstmt = conn.prepareStatement(loanSql)) {
                pstmt.setString(1, loan.getLoanId());
                pstmt.setString(2, loan.getItem().getId());
                pstmt.setString(3, loan.getMember().getId());
                pstmt.setTimestamp(4, Timestamp.valueOf(loan.getCheckoutDateTime()));
                pstmt.setDate(5, Date.valueOf(loan.getDueDate()));
                pstmt.executeUpdate();
            }

            // 2. Update media item status
            String statusSql = "UPDATE media_items SET status = ? WHERE id = ?";
            try (PreparedStatement pstmt = conn.prepareStatement(statusSql)) {
                pstmt.setString(1, MediaStatus.BORROWED.name());
                pstmt.setString(2, loan.getItem().getId());
                pstmt.executeUpdate();
            }

            conn.commit();  // (#2:Commit transaction)

        } catch (SQLException e) {
            if (conn != null) {
                conn.rollback();  // (#3:Rollback on error)
            }
            throw e;
        } finally {
            if (conn != null) {
                conn.setAutoCommit(true);  // (#4:Restore auto-commit)
            }
        }
    }

    // Return item with transaction
    public void returnLoan(String loanId, double lateFee) throws SQLException {
        Connection conn = null;
        try {
            conn = dbManager.getConnection();
            conn.setAutoCommit(false);

            // 1. Get the loan to find the item ID
            String findSql = "SELECT item_id FROM loans WHERE id = ?";
            String itemId = null;

            try (PreparedStatement pstmt = conn.prepareStatement(findSql)) {
                pstmt.setString(1, loanId);
                try (ResultSet rs = pstmt.executeQuery()) {
                    if (rs.next()) {
                        itemId = rs.getString("item_id");
                    } else {
                        throw new SQLException("Loan not found: " + loanId);
                    }
                }
            }

            // 2. Update loan record
            String loanSql = """
                UPDATE loans
                SET return_date = ?, late_fee = ?
                WHERE id = ?
                """;

            try (PreparedStatement pstmt = conn.prepareStatement(loanSql)) {
                pstmt.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
                pstmt.setDouble(2, lateFee);
                pstmt.setString(3, loanId);
                pstmt.executeUpdate();
            }

            // 3. Update media item status
            String statusSql = "UPDATE media_items SET status = ? WHERE id = ?";
            try (PreparedStatement pstmt = conn.prepareStatement(statusSql)) {
                pstmt.setString(1, MediaStatus.AVAILABLE.name());
                pstmt.setString(2, itemId);
                pstmt.executeUpdate();
            }

            conn.commit();

        } catch (SQLException e) {
            if (conn != null) {
                conn.rollback();
            }
            throw e;
        } finally {
            if (conn != null) {
                conn.setAutoCommit(true);
            }
        }
    }

    // Find active loans (not returned)
    public List<LoanRecord> findActiveLoans() throws SQLException {  // (#5:Use record for results)
        String sql = """
            SELECT l.*, m.name as member_name, mi.title as item_title
            FROM loans l
            JOIN members m ON l.member_id = m.id
            JOIN media_items mi ON l.item_id = mi.id
            WHERE l.return_date IS NULL
            ORDER BY l.due_date
            """;  // (#6:JOIN query)

        List<LoanRecord> loans = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                loans.add(new LoanRecord(
                    rs.getString("id"),
                    rs.getString("item_id"),
                    rs.getString("item_title"),
                    rs.getString("member_id"),
                    rs.getString("member_name"),
                    rs.getTimestamp("checkout_date").toLocalDateTime(),
                    rs.getDate("due_date").toLocalDate()
                ));
            }
        }
        return loans;
    }

    // Find overdue loans
    public List<LoanRecord> findOverdueLoans() throws SQLException {
        String sql = """
            SELECT l.*, m.name as member_name, mi.title as item_title
            FROM loans l
            JOIN members m ON l.member_id = m.id
            JOIN media_items mi ON l.item_id = mi.id
            WHERE l.return_date IS NULL AND l.due_date < CURRENT_DATE
            ORDER BY l.due_date
            """;

        List<LoanRecord> loans = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                loans.add(new LoanRecord(
                    rs.getString("id"),
                    rs.getString("item_id"),
                    rs.getString("item_title"),
                    rs.getString("member_id"),
                    rs.getString("member_name"),
                    rs.getTimestamp("checkout_date").toLocalDateTime(),
                    rs.getDate("due_date").toLocalDate()
                ));
            }
        }
        return loans;
    }

    // Find loans by member
    public List<LoanRecord> findByMember(String memberId) throws SQLException {
        String sql = """
            SELECT l.*, m.name as member_name, mi.title as item_title
            FROM loans l
            JOIN members m ON l.member_id = m.id
            JOIN media_items mi ON l.item_id = mi.id
            WHERE l.member_id = ?
            ORDER BY l.checkout_date DESC
            """;

        List<LoanRecord> loans = new ArrayList<>();

        try (Connection conn = dbManager.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, memberId);

            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    loans.add(new LoanRecord(
                        rs.getString("id"),
                        rs.getString("item_id"),
                        rs.getString("item_title"),
                        rs.getString("member_id"),
                        rs.getString("member_name"),
                        rs.getTimestamp("checkout_date").toLocalDateTime(),
                        rs.getDate("due_date").toLocalDate()
                    ));
                }
            }
        }
        return loans;
    }

    // Calculate total outstanding fees
    public double getTotalOutstandingFees(double dailyRate) throws SQLException {
        String sql = """
            SELECT SUM(DATEDIFF(DAY, due_date, CURRENT_DATE)) as total_days_overdue
            FROM loans
            WHERE return_date IS NULL AND due_date < CURRENT_DATE
            """;

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            if (rs.next()) {
                int totalDays = rs.getInt("total_days_overdue");
                return totalDays * dailyRate;
            }
        }
        return 0.0;
    }

    // Get next loan ID
    public int getNextLoanId() throws SQLException {
        String sql = "SELECT MAX(CAST(SUBSTRING(id, 2) AS INT)) FROM loans";

        try (Connection conn = dbManager.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            if (rs.next()) {
                return rs.getInt(1) + 1;
            }
        }
        return 1;
    }

    // Record class for loan query results
    public record LoanRecord(
        String loanId,
        String itemId,
        String itemTitle,
        String memberId,
        String memberName,
        LocalDateTime checkoutDate,
        LocalDate dueDate
    ) {
        public boolean isOverdue() {
            return LocalDate.now().isAfter(dueDate);
        }

        public long getDaysOverdue() {
            if (!isOverdue()) return 0;
            return java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDate.now());
        }
    }
}
Code Annotations:
  1. setAutoCommit(false): Begin explicit transaction
  2. commit(): Make all changes permanent
  3. rollback(): Undo all changes on error
  4. Restore auto-commit: Return to normal mode
  5. Record for results: Immutable data carrier
  6. JOIN: Combine data from multiple tables

Part 5: Integrated Library Application

Step 5.1: Update LibraryApp to Use DAOs

package com.library.app;

import com.library.model.*;
import com.library.dao.*;
import com.library.exception.*;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.*;

public class LibraryApp {
    private final DatabaseManager dbManager;
    private final MemberDao memberDao;
    private final MediaItemDao mediaItemDao;
    private final LoanDao loanDao;
    private final Scanner scanner;
    private int loanIdCounter;

    public LibraryApp() {
        this.dbManager = DatabaseManager.getInstance();
        this.memberDao = new MemberDao();
        this.mediaItemDao = new MediaItemDao();
        this.loanDao = new LoanDao();
        this.scanner = new Scanner(System.in);
    }

    public void start() {
        System.out.println("=== Library Management System (Database Edition) ===\n");

        try {
            // Initialize database
            dbManager.initializeDatabase();
            loanIdCounter = loanDao.getNextLoanId();

            boolean running = true;
            while (running) {
                displayMenu();
                String choice = scanner.nextLine().trim();

                try {
                    running = processChoice(choice);
                } catch (SQLException e) {
                    System.out.println("Database error: " + e.getMessage());
                } catch (Exception e) {
                    System.out.println("Error: " + e.getMessage());
                }
            }
        } catch (SQLException e) {
            System.out.println("Failed to initialize database: " + e.getMessage());
        } finally {
            dbManager.closeConnection();
            System.out.println("Goodbye!");
        }
    }

    private void displayMenu() {
        System.out.println("""

            ╔══════════════════════════════════════╗
            ║     LIBRARY MENU (Database)          ║
            ╠══════════════════════════════════════╣
            ║  1. Browse Catalog                   ║
            ║  2. Search Items                     ║
            ║  3. Add New Item                     ║
            ║  4. Checkout Item                    ║
            ║  5. Return Item                      ║
            ║  6. View Active Loans                ║
            ║  7. View Overdue Items               ║
            ║  8. Register Member                  ║
            ║  9. View Member Loans                ║
            ║  10. Statistics                      ║
            ║  0. Exit                             ║
            ╚══════════════════════════════════════╝
            """);
        System.out.print("Enter choice: ");
    }

    private boolean processChoice(String choice) throws SQLException {
        switch (choice) {
            case "1" -> browseCatalog();
            case "2" -> searchItems();
            case "3" -> addNewItem();
            case "4" -> checkoutItem();
            case "5" -> returnItem();
            case "6" -> viewActiveLoans();
            case "7" -> viewOverdueItems();
            case "8" -> registerMember();
            case "9" -> viewMemberLoans();
            case "10" -> viewStatistics();
            case "0" -> { return false; }
            default -> System.out.println("Invalid choice");
        }
        return true;
    }

    private void browseCatalog() throws SQLException {
        List<MediaItem> items = mediaItemDao.findAll();

        if (items.isEmpty()) {
            System.out.println("Catalog is empty");
            return;
        }

        System.out.println("\n=== Library Catalog ===");
        for (MediaItem item : items) {
            System.out.printf("[%s] %s - %s [%s]%n",
                item.getMediaType(),
                item.getId(),
                item.getTitle(),
                item.getStatus());
        }
    }

    private void searchItems() throws SQLException {
        System.out.print("Enter search term: ");
        String term = scanner.nextLine();

        List<MediaItem> results = mediaItemDao.searchByTitle(term);

        if (results.isEmpty()) {
            System.out.println("No items found");
        } else {
            System.out.println("\nSearch Results:");
            for (MediaItem item : results) {
                System.out.println("  - " + item);
            }
        }
    }

    private void addNewItem() throws SQLException {
        System.out.println("Type: 1=Book, 2=DVD, 3=Magazine");
        String type = scanner.nextLine();

        System.out.print("ID: ");
        String id = scanner.nextLine();
        System.out.print("Title: ");
        String title = scanner.nextLine();
        System.out.print("Year: ");
        int year = Integer.parseInt(scanner.nextLine());

        MediaItem item;
        switch (type) {
            case "1" -> {
                System.out.print("Author: ");
                String author = scanner.nextLine();
                System.out.print("ISBN: ");
                String isbn = scanner.nextLine();
                System.out.print("Category (FICTION/NON_FICTION/TECHNOLOGY/SCIENCE): ");
                BookCategory cat = BookCategory.valueOf(scanner.nextLine().toUpperCase());
                item = new Book(id, title, author, year, isbn, cat);
            }
            case "2" -> {
                System.out.print("Director: ");
                String director = scanner.nextLine();
                System.out.print("Duration (min): ");
                int duration = Integer.parseInt(scanner.nextLine());
                System.out.print("Rating: ");
                String rating = scanner.nextLine();
                item = new DVD(id, title, director, year, duration, rating);
            }
            case "3" -> {
                System.out.print("Publisher: ");
                String publisher = scanner.nextLine();
                System.out.print("Issue #: ");
                int issue = Integer.parseInt(scanner.nextLine());
                System.out.print("Month: ");
                String month = scanner.nextLine();
                item = new Magazine(id, title, publisher, year, issue, month);
            }
            default -> {
                System.out.println("Invalid type");
                return;
            }
        }

        mediaItemDao.insert(item);
        System.out.println("Item added successfully!");
    }

    private void checkoutItem() throws SQLException {
        System.out.print("Item ID: ");
        String itemId = scanner.nextLine();
        System.out.print("Member ID: ");
        String memberId = scanner.nextLine();

        Optional<MediaItem> itemOpt = mediaItemDao.findById(itemId);
        Optional<Member> memberOpt = memberDao.findById(memberId);

        if (itemOpt.isEmpty()) {
            System.out.println("Item not found");
            return;
        }
        if (memberOpt.isEmpty()) {
            System.out.println("Member not found");
            return;
        }

        MediaItem item = itemOpt.get();
        Member member = memberOpt.get();

        if (item.getStatus() != MediaStatus.AVAILABLE) {
            System.out.println("Item is not available: " + item.getStatus());
            return;
        }

        String loanId = String.format("L%05d", loanIdCounter++);
        Loan loan = new Loan(loanId, item, member, item.getMaxLoanDays());
        loanDao.createLoan(loan);

        System.out.println("Checkout successful!");
        System.out.println("Loan ID: " + loanId);
        System.out.println("Due date: " + loan.getFormattedDueDate());
    }

    private void returnItem() throws SQLException {
        System.out.print("Loan ID: ");
        String loanId = scanner.nextLine();

        // Find the loan details to calculate late fee
        List<LoanDao.LoanRecord> activeLoans = loanDao.findActiveLoans();
        LoanDao.LoanRecord loan = null;

        for (LoanDao.LoanRecord l : activeLoans) {
            if (l.loanId().equals(loanId)) {
                loan = l;
                break;
            }
        }

        if (loan == null) {
            System.out.println("Active loan not found: " + loanId);
            return;
        }

        double lateFee = 0;
        if (loan.isOverdue()) {
            lateFee = loan.getDaysOverdue() * 0.50;  // $0.50 per day
            System.out.printf("Late fee: $%.2f (%d days overdue)%n",
                lateFee, loan.getDaysOverdue());
        }

        loanDao.returnLoan(loanId, lateFee);
        System.out.println("Return processed successfully!");
    }

    private void viewActiveLoans() throws SQLException {
        List<LoanDao.LoanRecord> loans = loanDao.findActiveLoans();

        if (loans.isEmpty()) {
            System.out.println("No active loans");
            return;
        }

        System.out.println("\n=== Active Loans ===");
        for (LoanDao.LoanRecord loan : loans) {
            String status = loan.isOverdue() ? "[OVERDUE]" : "";
            System.out.printf("%s: %s → %s (due: %s) %s%n",
                loan.loanId(),
                loan.itemTitle(),
                loan.memberName(),
                loan.dueDate(),
                status);
        }
    }

    private void viewOverdueItems() throws SQLException {
        List<LoanDao.LoanRecord> overdue = loanDao.findOverdueLoans();

        if (overdue.isEmpty()) {
            System.out.println("No overdue items - great!");
            return;
        }

        System.out.println("\n=== Overdue Items ===");
        for (LoanDao.LoanRecord loan : overdue) {
            System.out.printf("%s: %s - %d days overdue%n",
                loan.loanId(),
                loan.itemTitle(),
                loan.getDaysOverdue());
        }
    }

    private void registerMember() throws SQLException {
        System.out.print("ID: ");
        String id = scanner.nextLine();
        System.out.print("Name: ");
        String name = scanner.nextLine();
        System.out.print("Email: ");
        String email = scanner.nextLine();

        if (memberDao.emailExists(email)) {
            System.out.println("Email already registered!");
            return;
        }

        Member member = new Member(id, name, email);
        memberDao.insert(member);
        System.out.println("Member registered successfully!");
    }

    private void viewMemberLoans() throws SQLException {
        System.out.print("Member ID: ");
        String memberId = scanner.nextLine();

        Optional<Member> memberOpt = memberDao.findById(memberId);
        if (memberOpt.isEmpty()) {
            System.out.println("Member not found");
            return;
        }

        List<LoanDao.LoanRecord> loans = loanDao.findByMember(memberId);

        System.out.println("\nLoans for: " + memberOpt.get().getName());
        if (loans.isEmpty()) {
            System.out.println("No loan history");
        } else {
            for (LoanDao.LoanRecord loan : loans) {
                System.out.printf("  %s: %s (due: %s)%n",
                    loan.loanId(), loan.itemTitle(), loan.dueDate());
            }
        }
    }

    private void viewStatistics() throws SQLException {
        System.out.println("\n=== Library Statistics ===");

        Map<String, Integer> counts = mediaItemDao.countByType();
        int total = counts.values().stream().mapToInt(Integer::intValue).sum();

        System.out.printf("Total items: %d%n", total);
        for (Map.Entry<String, Integer> entry : counts.entrySet()) {
            System.out.printf("  %s: %d%n", entry.getKey(), entry.getValue());
        }

        System.out.printf("Total members: %d%n", memberDao.count());
        System.out.printf("Active loans: %d%n", loanDao.findActiveLoans().size());
        System.out.printf("Overdue items: %d%n", loanDao.findOverdueLoans().size());
    }

    public static void main(String[] args) {
        new LibraryApp().start();
    }
}

Exercises

Exercise 1: Member Loan Limit

Implement a loan limit for members:

  • Add max_loans column to members table
  • Check current active loans before checkout
  • Reject checkout if limit reached
  • Default limit: 5 items

Exercise 2: Reservation System

Create a reservation table and DAO:

  • Create reservations table
  • Allow members to reserve unavailable items
  • Notify when item becomes available
  • Auto-cancel reservations after 3 days

Exercise 3: Fee Payment History

Track fee payments:

  • Create payments table (id, member_id, amount, date)
  • Record late fee payments
  • Calculate member balance
  • Block checkout if balance > $10

Exercise 4: Database Backup

Implement database export/import:

  • Export all data to SQL script file
  • Import data from SQL script
  • Use H2's SCRIPT and RUNSCRIPT commands
  • Add backup option to menu

Deliverables

Bonus Challenges

Advanced Connection Pooling: Implement connection pooling using HikariCP
Advanced MySQL Migration: Adapt the code to work with MySQL instead of H2
Expert ORM Introduction: Refactor one DAO to use JPA/Hibernate

Resources