Goal: save everything in a SQLite database through DAOs, so the LMS survives a restart.

Warning Add the SQLite driver first: download sqlite-jdbc-3.46.0.0.jar (group org.xerial) from Maven Central into a lib/ folder and add it to your IDE's libraries. Never commit the jar or lms.db.

Three tables, two foreign keys
Three tables, two foreign keys
Where JDBC fits: only lms.dao knows SQL
Where JDBC fits: only lms.dao knows SQL

1. The connection and the tables

src/lms/dao/Database.java
package lms.dao;

import lms.exception.DataAccessException;

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

/**
 * Database - the ONE place that knows how to reach the database (Week 11).
 *
 * Default is SQLite: a single file "lms.db" next to the program. No server
 * to install. The URL can be overridden with  -Dlms.db.url=...  which is how
 * the tests use a throw-away in-memory database.
 *
 * Tables:
 *   items   (id PK, type, title, detail)
 *   members (id PK, name, type)
 *   loans   (item_id FK, member_id FK, borrowed_at, returned_at NULL until returned)
 */
public final class Database {

    public static final String DEFAULT_URL = "jdbc:sqlite:lms.db";

    private Database() { }

    public static String url() {
        return System.getProperty("lms.db.url", DEFAULT_URL);
    }

    /** Opens a NEW connection. Callers close it with try-with-resources. */
    public static Connection getConnection() throws SQLException {
        Connection con = DriverManager.getConnection(url());
        if (url().startsWith("jdbc:sqlite:")) {
            // SQLite ignores foreign keys unless you switch them on per connection.
            try (Statement st = con.createStatement()) {
                st.execute("PRAGMA foreign_keys = ON");
            }
        }
        return con;
    }

    /** Creates the tables if they do not exist yet. Safe to call every start-up. */
    public static void init() {
        String[] ddl = {
            "CREATE TABLE IF NOT EXISTS items ("
                + " id VARCHAR(20) PRIMARY KEY,"
                + " type VARCHAR(10) NOT NULL,"
                + " title VARCHAR(200) NOT NULL,"
                + " detail VARCHAR(200) NOT NULL)",
            "CREATE TABLE IF NOT EXISTS members ("
                + " id VARCHAR(20) PRIMARY KEY,"
                + " name VARCHAR(100) NOT NULL,"
                + " type VARCHAR(10) NOT NULL)",
            "CREATE TABLE IF NOT EXISTS loans ("
                + " item_id VARCHAR(20) NOT NULL,"
                + " member_id VARCHAR(20) NOT NULL,"
                + " borrowed_at VARCHAR(30) NOT NULL,"
                + " returned_at VARCHAR(30),"
                + " PRIMARY KEY (item_id, borrowed_at),"
                + " FOREIGN KEY (item_id) REFERENCES items(id),"
                + " FOREIGN KEY (member_id) REFERENCES members(id))"
        };
        try (Connection con = getConnection(); Statement st = con.createStatement()) {
            for (String sql : ddl) st.execute(sql);
        } catch (SQLException e) {
            throw new DataAccessException("Could not create the database tables", e);
        }
    }
}
src/lms/exception/DataAccessException.javaTranslate SQLException into something the app understands
package lms.exception;

/**
 * UNCHECKED wrapper for java.sql.SQLException (Week 11).
 *
 * SQLException is a low-level, checked, database-specific exception. If it
 * leaked up to the GUI, every layer would need to know about SQL. Instead
 * the DAO catches it and rethrows this one, with a friendly message.
 */
public class DataAccessException extends RuntimeException {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

2. One DAO per table

Every query uses ? placeholders and try-with-resources. ItemDAO turns a type column back into the right subclass.

src/lms/dao/ItemDAO.java
package lms.dao;

import lms.exception.DataAccessException;
import lms.model.*;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * ItemDAO - Data Access Object for items (Week 11).
 *
 * A DAO hides ALL the SQL for one kind of thing. Nothing outside lms.dao
 * writes SQL. It maps between rows and OBJECTS: the "type" column decides
 * whether a row becomes a Book, a DVD or a Magazine.
 */
public class ItemDAO {

    public void insert(LibraryItem item) {
        String sql = "INSERT INTO items (id, type, title, detail) VALUES (?, ?, ?, ?)";
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, item.getId());
            ps.setString(2, item.getClass().getSimpleName());
            ps.setString(3, item.getTitle());
            ps.setString(4, detailOf(item));
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new DataAccessException("Could not save item " + item.getId(), e);
        }
    }

    public List<LibraryItem> findAll() {
        String sql = "SELECT id, type, title, detail FROM items ORDER BY id";
        List<LibraryItem> result = new ArrayList<>();
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                result.add(toItem(rs.getString("id"), rs.getString("type"),
                        rs.getString("title"), rs.getString("detail")));
            }
        } catch (SQLException e) {
            throw new DataAccessException("Could not read items", e);
        }
        return result;
    }

    /** Row -> object. This is the "factory" step. */
    private LibraryItem toItem(String id, String type, String title, String detail) {
        return switch (type) {
            case "Book" -> new Book(id, title, detail);
            case "DVD" -> new DVD(id, title, Integer.parseInt(detail));
            case "Magazine" -> new Magazine(id, title, Integer.parseInt(detail));
            default -> throw new DataAccessException("Unknown item type in database: " + type, null);
        };
    }

    /** Object -> the single "detail" column. */
    private String detailOf(LibraryItem item) {
        if (item instanceof Book b) return b.getAuthor();
        if (item instanceof DVD d) return String.valueOf(d.getDurationMinutes());
        if (item instanceof Magazine m) return String.valueOf(m.getIssueNumber());
        return "";
    }
}
src/lms/dao/MemberDAO.java
package lms.dao;

import lms.exception.DataAccessException;
import lms.model.Member;
import lms.model.Staff;
import lms.model.Student;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/** MemberDAO - all SQL for the members table (Week 11). */
public class MemberDAO {

    public void insert(Member member) {
        String sql = "INSERT INTO members (id, name, type) VALUES (?, ?, ?)";
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, member.getMemberId());
            ps.setString(2, member.getName());
            ps.setString(3, member instanceof Staff ? "STAFF" : "STUDENT");
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new DataAccessException("Could not save member " + member.getMemberId(), e);
        }
    }

    public List<Member> findAll() {
        String sql = "SELECT id, name, type FROM members ORDER BY id";
        List<Member> result = new ArrayList<>();
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                String id = rs.getString("id");
                String name = rs.getString("name");
                result.add("STAFF".equals(rs.getString("type")) ? new Staff(id, name) : new Student(id, name));
            }
        } catch (SQLException e) {
            throw new DataAccessException("Could not read members", e);
        }
        return result;
    }
}
src/lms/dao/LoanDAO.javaIncludes a three-table JOIN
package lms.dao;

import lms.exception.DataAccessException;

import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * LoanDAO - all SQL for the loans table (Week 11).
 *
 * A loan row with returned_at = NULL means "still on loan".
 * There is no separate "is borrowed" column to keep in sync - the loans
 * table IS the truth.
 */
public class LoanDAO {

    /** A small immutable data holder (a Java record). */
    public record OpenLoan(String itemId, String memberId) { }

    public void recordLoan(String itemId, String memberId) {
        String sql = "INSERT INTO loans (item_id, member_id, borrowed_at) VALUES (?, ?, ?)";
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, itemId);
            ps.setString(2, memberId);
            ps.setString(3, LocalDateTime.now().withNano(0).toString());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new DataAccessException("Could not record the loan of " + itemId, e);
        }
    }

    public void recordReturn(String itemId) {
        String sql = "UPDATE loans SET returned_at = ? WHERE item_id = ? AND returned_at IS NULL";
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {
            ps.setString(1, LocalDateTime.now().withNano(0).toString());
            ps.setString(2, itemId);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new DataAccessException("Could not record the return of " + itemId, e);
        }
    }

    /** Used at start-up to restore "who has what". */
    public List<OpenLoan> findOpenLoans() {
        String sql = "SELECT item_id, member_id FROM loans WHERE returned_at IS NULL";
        List<OpenLoan> result = new ArrayList<>();
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) result.add(new OpenLoan(rs.getString(1), rs.getString(2)));
        } catch (SQLException e) {
            throw new DataAccessException("Could not read open loans", e);
        }
        return result;
    }

    /** A JOIN across three tables: rows of {borrowed, item title, member name, returned}. */
    public List<String[]> history() {
        String sql = "SELECT l.borrowed_at, i.title, m.name, l.returned_at "
                   + "FROM loans l "
                   + "JOIN items i ON i.id = l.item_id "
                   + "JOIN members m ON m.id = l.member_id "
                   + "ORDER BY l.borrowed_at DESC";
        List<String[]> rows = new ArrayList<>();
        try (Connection con = Database.getConnection();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                String returned = rs.getString(4);
                rows.add(new String[]{rs.getString(1).replace('T', ' '), rs.getString(2), rs.getString(3),
                        returned == null ? "(on loan)" : returned.replace('T', ' ')});
            }
        } catch (SQLException e) {
            throw new DataAccessException("Could not read loan history", e);
        }
        return rows;
    }
}

3. Test the DAOs from the console before the GUI

src/lms/DaoDemo.java
package lms;

import lms.dao.*;
import lms.model.*;

/**
 * DaoDemo - test your DAOs from the console BEFORE touching the GUI.
 * If a DAO is wrong, you want to find out here, not behind a button.
 */
public class DaoDemo {
    public static void main(String[] args) {
        Database.init();
        ItemDAO items = new ItemDAO();
        MemberDAO members = new MemberDAO();
        LoanDAO loans = new LoanDAO();

        items.insert(new Book("B100", "Head First Java", "Kathy Sierra"));
        items.insert(new DVD("D100", "Databases 101", 90));
        members.insert(new Student("M100", "Amal Fernando"));

        System.out.println("--- items in the database ---");
        items.findAll().forEach(System.out::println);
        System.out.println("--- members in the database ---");
        members.findAll().forEach(System.out::println);

        loans.recordLoan("B100", "M100");
        System.out.println("--- open loans ---");
        loans.findOpenLoans().forEach(System.out::println);

        loans.recordReturn("B100");
        System.out.println("--- open loans after return: " + loans.findOpenLoans().size());

        System.out.println("--- history (JOIN) ---");
        for (String[] row : loans.history()) System.out.println(String.join(" | ", row));

        System.out.println("--- foreign key check: loan for a member that does not exist ---");
        try {
            loans.recordLoan("B100", "M999");
        } catch (lms.exception.DataAccessException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}
java -cp "out:lib/*" lms.DaoDemo
--- items in the database --- Book{id='B100', title='Head First Java', borrowed=false} DVD{id='D100', title='Databases 101', borrowed=false} --- members in the database --- Student{id='M100', name='Amal Fernando', borrowedCount=0, limit=3} --- open loans --- OpenLoan[itemId=B100, memberId=M100] --- open loans after return: 0 --- history (JOIN) --- 2026-10-13 00:51:57 | Head First Java | Amal Fernando | 2026-10-13 00:51:57 --- foreign key check: loan for a member that does not exist --- Rejected: Could not record the loan of B100

The last line is the foreign key doing its job: a loan for member M999 is refused.

4. Plug the DAOs into the controller

Order matters. Validate by constructing → save to the database → then change memory. For borrowing: rules first, then the database, and undo if the database fails.

src/lms/service/LibraryController.java
package lms.service;

import lms.dao.ItemDAO;
import lms.dao.LoanDAO;
import lms.dao.MemberDAO;
import lms.exception.ItemNotAvailableException;
import lms.model.*;

import java.util.List;

/**
 * LibraryController - the C in MVC (Week 9); now also talks to the DAOs (Week 11).
 *
 *   Model      = lms.model + Library   (data and rules; knows nothing about windows)
 *   View       = lms.ui.MainFrame      (windows; knows nothing about the rules)
 *   Controller = this class            (receives what the user typed, calls the model)
 *
 * The view never touches Book/DVD/Library directly. It asks the controller.
 * That is what lets us swap the console for Swing - and later swap the
 * in-memory list for a database - without rewriting the other layers.
 */
public class LibraryController {

    private final Library library;
    private final ItemDAO itemDAO;
    private final MemberDAO memberDAO;
    private final LoanDAO loanDAO;

    public LibraryController(Library library, ItemDAO itemDAO, MemberDAO memberDAO, LoanDAO loanDAO) {
        this.library = library;
        this.itemDAO = itemDAO;
        this.memberDAO = memberDAO;
        this.loanDAO = loanDAO;
    }

    /** Start-up: rebuild the in-memory model from the database. */
    public void loadFromDatabase() {
        for (LibraryItem item : itemDAO.findAll()) library.addItem(item);
        for (Member m : memberDAO.findAll()) library.addMember(m);
        for (LoanDAO.OpenLoan loan : loanDAO.findOpenLoans()) {
            try {
                library.borrowItem(loan.itemId(), loan.memberId());   // replays the loan through the model's rules
            } catch (ItemNotAvailableException e) {
                throw new IllegalStateException("Database holds an impossible loan: " + loan, e);
            }
        }
    }

    /** Order matters: validate (construct) -> save to DB -> add to memory. */
    public void addItem(String type, String id, String title, String detail) {
        if (library.hasItem(id)) {
            throw new IllegalArgumentException("An item with id '" + id + "' already exists");
        }
        LibraryItem item = switch (type) {
            case "Book" -> new Book(id, title, detail);
            case "DVD" -> new DVD(id, title, parseWholeNumber(detail, "Duration"));
            case "Magazine" -> new Magazine(id, title, parseWholeNumber(detail, "Issue number"));
            default -> throw new IllegalArgumentException("Unknown item type: " + type);
        };
        itemDAO.insert(item);
        library.addItem(item);
    }

    public void addMember(String type, String id, String name) {
        if (library.getMembers().stream().anyMatch(m -> m.getMemberId().equals(id))) {
            throw new IllegalArgumentException("A member with id '" + id + "' already exists");
        }
        Member member = "Staff".equals(type) ? new Staff(id, name) : new Student(id, name);
        memberDAO.insert(member);
        library.addMember(member);
    }

    public void borrow(String itemId, String memberId) throws ItemNotAvailableException {
        library.borrowItem(itemId, memberId);          // rules first
        try {
            loanDAO.recordLoan(itemId, memberId);      // then persist
        } catch (RuntimeException e) {
            library.returnItem(itemId);                // undo the in-memory change
            throw e;
        }
    }

    public void giveBack(String itemId) throws ItemNotAvailableException {
        library.returnItem(itemId);
        loanDAO.recordReturn(itemId);
    }

    public List<String[]> loanHistory() { return loanDAO.history(); }

    public List<LibraryItem> getItems() { return library.getItems(); }

    /** An empty search box means "show everything". */
    public List<LibraryItem> search(String text) {
        return (text == null || text.isBlank()) ? library.getItems() : library.search(text);
    }

    /** Plain-text report for a dialog. */
    public String loanReport() {
        var counts = library.loansPerMember();
        if (counts.isEmpty()) return "No items are currently on loan.";
        StringBuilder sb = new StringBuilder("Items currently on loan:\n");
        counts.forEach((name, n) -> sb.append("  ").append(name).append(": ").append(n).append('\n'));
        sb.append("\nAvailable: ").append(library.availableItems().size());
        return sb.toString();
    }
    public List<Member> getMembers() { return library.getMembers(); }

    private int parseWholeNumber(String text, String label) {
        try {
            return Integer.parseInt(text.trim());
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(label + " must be a whole number");
        }
    }
}
src/lms/GuiMain.javaStart-up: init, seed only if empty, load, show
package lms;

import lms.dao.*;
import lms.model.*;
import lms.service.Library;
import lms.service.LibraryController;
import lms.ui.MainFrame;

import javax.swing.*;

/** Week 11 start-up: database -> model -> controller -> view. */
public class GuiMain {
    public static void main(String[] args) {
        Database.init();                                  // create tables if needed

        ItemDAO itemDAO = new ItemDAO();
        MemberDAO memberDAO = new MemberDAO();
        LoanDAO loanDAO = new LoanDAO();
        seedIfEmpty(itemDAO, memberDAO);                  // first run only

        Library library = new Library();                  // MODEL
        LibraryController controller =                    // CONTROLLER
                new LibraryController(library, itemDAO, memberDAO, loanDAO);
        controller.loadFromDatabase();

        SwingUtilities.invokeLater(() -> new MainFrame(controller).setVisible(true));   // VIEW
    }

    private static void seedIfEmpty(ItemDAO itemDAO, MemberDAO memberDAO) {
        if (itemDAO.findAll().isEmpty()) {
            itemDAO.insert(new Book("B001", "Clean Code", "Robert C. Martin"));
            itemDAO.insert(new Book("B002", "Refactoring", "Martin Fowler"));
            itemDAO.insert(new DVD("D001", "Introduction to Algorithms", 120));
            itemDAO.insert(new Magazine("G001", "Software Weekly", 42));
        }
        if (memberDAO.findAll().isEmpty()) {
            memberDAO.insert(new Student("M001", "Nadeesha Perera"));
            memberDAO.insert(new Staff("M002", "Mr. Kasun Silva"));
        }
    }
}

5. Run it twice

mkdir -p out javac -d out $(find src -name "*.java") java -cp "out:lib/*" lms.GuiMain # use ; instead of : on Windows
Borrow some items and press Loan History (the JOIN)
Borrow some items and press Loan History (the JOIN)
Close and reopen: the loans are still there
Close and reopen: the loans are still there

Debugging JDBC

Error Cause
No suitable driver found Driver jar not on the classpath
no such table: items Database.init() not called first
FOREIGN KEY constraint failed Insert items/members before loans
database is locked Another program has lms.db open

Checkpoint: is it done?

  • lms.db is created and is not in git status
  • Items, members and open loans survive a restart
  • No SQL or java.sql import anywhere outside lms.dao
  • ER diagram committed; PR: Week 11: JDBC Database Connectivity