Case Study 1: Building a Library Catalog
Overview
A library catalog exercises the full range of database programming concepts: multiple related tables, CRUD operations, search, and reporting. We build a complete catalog application with SQLite, demonstrating proper database design, parameterized queries, and data-aware controls.
Database Schema
CREATE TABLE authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
birth_year INTEGER,
nationality TEXT
);
CREATE TABLE books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author_id INTEGER NOT NULL,
isbn TEXT UNIQUE,
year_published INTEGER,
genre TEXT,
copies_total INTEGER DEFAULT 1,
copies_available INTEGER DEFAULT 1,
FOREIGN KEY (author_id) REFERENCES authors(id)
);
CREATE TABLE borrowings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id INTEGER NOT NULL,
borrower_name TEXT NOT NULL,
borrow_date TEXT NOT NULL DEFAULT (date('now')),
due_date TEXT NOT NULL,
return_date TEXT,
FOREIGN KEY (book_id) REFERENCES books(id)
);
Three tables with two foreign key relationships. The borrowings table tracks who has borrowed which book. When return_date is NULL, the book is still checked out.
Key Operations
Search by Title or Author
procedure TfrmLibrary.SearchBooks(const SearchTerm: string);
begin
FQuery.Close;
FQuery.SQL.Text :=
'SELECT b.id, b.title, a.name as author, b.year_published, ' +
' b.genre, b.copies_available ' +
'FROM books b JOIN authors a ON b.author_id = a.id ' +
'WHERE b.title LIKE :search OR a.name LIKE :search ' +
'ORDER BY b.title';
FQuery.Params.ParamByName('search').AsString := '%' + SearchTerm + '%';
FQuery.Open;
end;
Check Out a Book
procedure TfrmLibrary.CheckOutBook(BookID: Integer; const Borrower: string);
var
Q: TSQLQuery;
begin
Q := TSQLQuery.Create(nil);
try
Q.Database := FConn;
Q.Transaction := FTrans;
{ Verify copies are available }
Q.SQL.Text := 'SELECT copies_available FROM books WHERE id = :id';
Q.Params.ParamByName('id').AsInteger := BookID;
Q.Open;
if Q.FieldByName('copies_available').AsInteger <= 0 then
begin
ShowMessage('No copies available for checkout.');
Q.Close;
Exit;
end;
Q.Close;
{ Create borrowing record }
Q.SQL.Text :=
'INSERT INTO borrowings (book_id, borrower_name, due_date) ' +
'VALUES (:bookid, :name, date(''now'', ''+14 days''))';
Q.Params.ParamByName('bookid').AsInteger := BookID;
Q.Params.ParamByName('name').AsString := Borrower;
Q.ExecSQL;
{ Decrement available copies }
Q.SQL.Text :=
'UPDATE books SET copies_available = copies_available - 1 WHERE id = :id';
Q.Params.ParamByName('id').AsInteger := BookID;
Q.ExecSQL;
FTrans.Commit;
FQuery.Refresh;
StatusBar1.Panels[0].Text := 'Book checked out to ' + Borrower;
finally
Q.Free;
end;
end;
Overdue Books Report
procedure TfrmLibrary.ShowOverdueBooks;
begin
FReportQuery.Close;
FReportQuery.SQL.Text :=
'SELECT b.title, a.name as author, br.borrower_name, ' +
' br.borrow_date, br.due_date, ' +
' julianday(''now'') - julianday(br.due_date) as days_overdue ' +
'FROM borrowings br ' +
'JOIN books b ON br.book_id = b.id ' +
'JOIN authors a ON b.author_id = a.id ' +
'WHERE br.return_date IS NULL AND br.due_date < date(''now'') ' +
'ORDER BY br.due_date ASC';
FReportQuery.Open;
end;
Lessons Learned
- Multiple related tables with foreign keys provide a natural, normalized data model.
- Parameterized queries are used for every user-facing operation.
- SQLite date functions (
date('now'),julianday()) handle date arithmetic. - Transactions ensure that checkout operations (insert borrowing + decrement copies) succeed or fail atomically.
- JOIN queries combine data from multiple tables for display and reporting.