JPA Bookstore Project

1 / 33
📚

Building a Bookstore with JPA,
NetBeans & MySQL

A complete, step-by-step guide — from installation to a working persistent bookstore application.

Part 1 · Installation Part 2 · Preparation Part 3 · Project Build NetBeans IDE MySQL Workbench

Use ← → arrow keys or the buttons below to navigate

🎯

What You Will Learn

2 / 33

Learning Outcomes

  1. Install and configure all required tools
  2. Set up a MySQL bookstore database
  3. Create a Jakarta EE JPA project in NetBeans
  4. Map a Book entity to a database table
  5. Perform CRUD operations using JPA
  6. Verify results in MySQL Workbench

Tools You Will Use

Java JDK 21

Runtime + compiler for all Java code

🛠️

Apache NetBeans 21+

IDE with built-in JPA & GlassFish support

🐬

MySQL 8 + Workbench

Database server + visual query tool

Installation & Setup

3 / 33
Part 1 · Installation

Install Your Tools

Four components must be installed before you write a single line of code. We'll do them in the right order.

① JDK 21 ② NetBeans IDE ③ MySQL Server 8 ④ MySQL Workbench

Step 1 · Install Java JDK 21

4 / 33

Download

  1. 1
    Go to https://adoptium.net (Eclipse Temurin — free & open source)
  2. 2
    Select Java 21 (LTS), your OS, and click Latest Release
  3. 3
    Run the installer — tick "Set JAVA_HOME" and "Add to PATH" checkboxes

Verify Installation

$ java -version
openjdk version "21.0.x" 2024-xx-xx
$ javac -version
javac 21.0.x
✅ LTS = Long Term Support
Always use an LTS version (11, 17, 21) for course work — they receive patches for years.
⚠️ Windows Users
After install, open System Properties → Environment Variables and confirm JAVA_HOME points to your JDK folder (e.g., C:\Program Files\Eclipse Adoptium\jdk-21).
🍎 macOS / Linux
The installer sets paths automatically. Alternatively use Homebrew: brew install --cask temurin@21
🛠️

Step 2 · Install Apache NetBeans 21

5 / 33

Download & Install

  1. 1
    Visit https://netbeans.apache.org/front/main/download
  2. 2
    Download the Apache NetBeans 21 installer for your OS
  3. 3
    Run the installer — accept the default path, ensure it detects JDK 21
  4. 4
    Launch NetBeans; dismiss the "Getting Started" tab

Enable Required Plugins

Go to Tools → Plugins → Available Plugins
Search for and install: "Jakarta EE" and "GlassFish" bundles if not already present.

Add GlassFish / Payara Server

  1. 1
    Download GlassFish 7 from https://glassfish.org (ZIP)
  2. 2
    Extract to a stable folder (e.g., C:\servers\glassfish7)
  3. 3
    In NetBeans: Window → Services → Servers → Add Server
  4. 4
    Choose GlassFish Server → browse to the extracted folder
💡 Alternative: Payara 6
Payara is a production-ready GlassFish fork — works identically and is also free.
🐬

Step 3 · Install MySQL Server 8 + Workbench

6 / 33

MySQL Installer (Windows — easiest)

  1. 1
    Go to https://dev.mysql.com/downloads/installer
  2. 2
    Download mysql-installer-community-8.x.x.msi
  3. 3
    Choose Developer Default setup (installs Server + Workbench + Connector/J)
  4. 4
    Set root password — write it down! You'll need it for JPA config
  5. 5
    Keep default port 3306

macOS / Linux

# macOS (Homebrew)
$ brew install mysql
$ brew services start mysql
$ mysql_secure_installation

# Ubuntu/Debian
$ sudo apt install mysql-server
$ sudo mysql_secure_installation
Then install Workbench separately:
macOS: brew install --cask mysqlworkbench
Linux: Download .deb/.rpm from dev.mysql.com/downloads/workbench
✅ Verify MySQL is running — open MySQL Workbench, click the Local Instance tile, enter your root password. If the schema list loads, you're ready.
🔌

Step 4 · MySQL Connector/J (JDBC Driver)

7 / 33
What is Connector/J? It is the official JDBC driver that lets your Java application communicate with MySQL. JPA sits on top of JDBC, so this driver is essential.

Option A — Maven (Recommended)

If your project uses Maven (pom.xml), add this dependency — Maven downloads the driver automatically:

XML / pom.xml <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency>

Option B — Manual JAR

  1. 1
    Download mysql-connector-j-8.x.x.jar from dev.mysql.com/downloads/connector/j
  2. 2
    In NetBeans project: right-click Libraries → Add JAR/Folder
  3. 3
    Browse to and select the downloaded JAR
Also add to GlassFish!
Copy the JAR into glassfish7/glassfish/domains/domain1/lib/ for server-managed connections.

What to Prepare

8 / 33
Part 2 · Preparation

Prepare Your Environment

Before writing JPA code you need a database, a schema, a table, and the correct project structure in NetBeans.

① Create MySQL DB ② Create books table ③ New Maven project ④ persistence.xml ⑤ JDBC pool in GlassFish
🗄️

Create the Bookstore Database (Workbench)

9 / 33

Using MySQL Workbench GUI

  1. 1
    Open Workbench → double-click Local instance (root)
  2. 2
    In Navigator → Schemas, right-click → Create Schema
  3. 3
    Name it bookstoredb, charset utf8mb4, collation utf8mb4_unicode_ci
  4. 4
    Click Apply → Apply → Finish

OR — Run SQL directly

SQL CREATE DATABASE bookstoredb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookstoredb; -- Verify SHOW DATABASES;
✅ You should see bookstoredb in the result. Keep this connection open — you'll use Workbench to inspect data throughout the project.
📋

Create the books Table

10 / 33

SQL — Create Table

SQL USE bookstoredb; CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, author VARCHAR(150) NOT NULL, isbn VARCHAR(20) UNIQUE, price DECIMAL(8,2), stock_qty INT DEFAULT 0 ); -- Seed a test row INSERT INTO books (title, author, isbn, price, stock_qty) VALUES ('Clean Code', 'Robert C. Martin', '978-0132350884', 49.99, 10);

Table Structure

ColumnTypeNotes
idINTAuto PK — maps to @Id @GeneratedValue
titleVARCHAR(255)Required
authorVARCHAR(150)Required
isbnVARCHAR(20)Unique constraint
priceDECIMAL(8,2)Maps to BigDecimal
stock_qtyINTMaps to int
💡 JPA will manage this table for you — once your entity is configured, you can also let JPA generate the table by setting hibernate.hbm2ddl.auto = create in persistence.xml.
🆕

Step A · Create the Maven Project in NetBeans

11 / 33
What is Maven? Maven is a build tool that automatically downloads the libraries (JARs) your project needs. When you create a Maven project, NetBeans generates a pom.xml file — your project's shopping list of dependencies.

New Project Wizard — Exact Steps

  1. 1
    In the top menu bar click File → New Project…
    (or press Ctrl+Shift+N on Windows / Cmd+Shift+N on Mac)
    A dialog box appears.
  2. 2
    In the Categories panel on the left, scroll down and click Java with Maven.
    ⚠️ Do NOT choose plain "Java" — that uses the older Ant build system.
  3. 3
    In the Projects panel on the right, click Java Application.
    Then click Next >
  4. 4
    Fill in the Name and Location screen exactly as shown opposite →
    Then click Finish.
⚠️ Delete the auto-generated App.java
NetBeans creates a file called App.java inside com.bookstore.
Right-click it in the Projects panel → Delete.
You will write your own Main.java later.

Wizard Fields — What to Type

FieldWhat to enter
Project NameBookstoreJPA
Project LocationAny folder (e.g. your Desktop)
Group Idcom.bookstore
Artifact IdBookstoreJPA (auto-filled)
VersionLeave as 1.0-SNAPSHOT

What you see after Finish

In the Projects panel on the left you will see:
📁 BookstoreJPA
  📁 Source Packages    ← your .java files go here
  📁 Other Sources     ← persistence.xml goes here
  📁 Dependencies      ← Maven JARs appear here
  📁 Project Files     ← contains pom.xml
📂

Step B · Create Java Packages & the META-INF Folder

12 / 33
What is a Java Package? A package is simply a named folder for organising your .java files. The name com.bookstore.entity tells Java to create nested folders: com/ → inside it bookstore/ → inside it entity/. Keeping entity classes, DAO classes, and the main class in separate packages is standard professional practice.

① Create package com.bookstore.entity

  1. 1
    In the Projects panel, find Source Packages under your project and right-click it.
  2. 2
    From the context menu choose New → Java Package…
  3. 3
    In the Package Name field type:
    com.bookstore.entity
    Click Finish.

② Create package com.bookstore.dao

  1. 1
    Right-click Source Packages again → New → Java Package…
  2. 2
    Package Name: com.bookstore.dao
    Click Finish.
✅ You should now see under Source Packages:
📦 com.bookstore
📦 com.bookstore.dao
📦 com.bookstore.entity

③ Create the META-INF folder & persistence.xml

Why META-INF? JPA always looks for persistence.xml inside a folder named META-INF on the Java classpath. In a Maven project the classpath root is src/main/resources/, so you must create the folder there — not inside src/main/java/.
  1. 1
    In the Projects panel expand Other Sources
    → then expand src/main/resources
  2. 2
    Right-click src/main/resourcesNew → Folder…
  3. 3
    Folder Name: META-INF
    (all capital letters, hyphen between META and INF)
    Click Finish.
  4. 4
    Right-click the new META-INF folder → New → XML Document…
    File Name: persistence  (NetBeans adds .xml)
    Click Finish. Replace the generated XML content with the config from the next slide.
✅ Complete project structure
📁 Source Packages
  📦 com.bookstore    ← Main.java
  📦 com.bookstore.dao   ← BookDAO.java
  📦 com.bookstore.entity← Book.java
📁 Other Sources
  📁 src/main/resources
    📁 META-INF
      📄 persistence.xml
📦

Configure pom.xml — Dependencies

13 / 33

Complete pom.xml

XML / pom.xml <project> <modelVersion>4.0.0</modelVersion> <groupId>com.bookstore</groupId> <artifactId>BookstoreJPA</artifactId> <version>1.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> <dependencies> <!-- Jakarta Persistence API --> <dependency> <groupId>jakarta.persistence</groupId> <artifactId>jakarta.persistence-api</artifactId> <version>3.1.0</version> </dependency> <!-- Hibernate as JPA provider --> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>6.4.1.Final</version> </dependency> <!-- MySQL JDBC Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency> </dependencies> </project>
⚙️

Configure persistence.xml

14 / 33
📌 Location: src/main/resources/META-INF/persistence.xml — this file tells JPA which database to connect to and which provider (Hibernate) to use.
XML <?xml version="1.0" encoding="UTF-8"?> <persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence"> <persistence-unit name="BookstorePU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <class>com.bookstore.entity.Book</class> <properties> <!-- JDBC Connection --> <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/bookstore?useSSL=false&amp;serverTimezone=UTC"/> <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="jakarta.persistence.jdbc.user" value="root"/> <property name="jakarta.persistence.jdbc.password" value="YourPasswordHere"/> <!-- Hibernate settings --> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> </properties> </persistence-unit> </persistence>
⚠️ Security note — replace YourPasswordHere with your actual root password. Never commit passwords to a public repository.
🚨 Common student error — the &amp; in the JDBC URL
The URL must contain useSSL=false&amp;serverTimezone=UTC
In XML, the & character must be written as &amp; — a bare & will cause the error:
"The reference to entity serverTimezone must end with the ';' delimiter"

Build the Project

15 / 33
Part 3 · Project Build

Write the JPA Code

Now the fun part — entity class, DAO, and a main class that performs full CRUD operations on the bookstore database.

① Book entity ② BookDAO ③ Main class (CRUD) ④ Run & verify
📝

Book.java — The Entity Class

16 / 33
File: src/main/java/com/bookstore/entity/Book.java
Java package com.bookstore.entity; import jakarta.persistence.*; import java.math.BigDecimal; @Entity @Table(name = "books") public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 255) private String title; @Column(nullable = false, length = 150) private String author; @Column(unique = true, length = 20) private String isbn; @Column(precision = 8, scale = 2) private BigDecimal price; @Column(name = "stock_qty") private int stockQty; // ── No-arg constructor (required by JPA) ── public Book() { } // ── Convenience constructor ── public Book( String title, String author, String isbn, BigDecimal price, int stockQty) { this.title = title; this.author = author; this.isbn = isbn; this.price = price; this.stockQty = stockQty; } // ── Getters & Setters on next slide ── }
📝

Book.java — Getters, Setters & toString

17 / 33
Java // ── Getters ────────────────────────────────── public Long getId() { return id; } public String getTitle() { return title; } public String getAuthor() { return author; } public String getIsbn() { return isbn; } public BigDecimal getPrice() { return price; } public int getStockQty() { return stockQty; } // ── Setters ────────────────────────────────── public void setTitle(String title) { this.title = title; } public void setAuthor(String author) { this.author = author; } public void setIsbn(String isbn) { this.isbn = isbn; } public void setPrice(BigDecimal price) { this.price = price; } public void setStockQty(int stockQty) { this.stockQty = stockQty; } // ── toString ───────────────────────────────── @Override public String toString() { return "Book{" + "id=" + id + ", title='" + title + "'" + ", author='" + author + "'" + ", price=" + price + ", stock=" + stockQty + "}"; }

Annotation Recap

AnnotationPurpose
@EntityMarks class as JPA entity
@TableMaps to table name books
@IdPrimary key field
@GeneratedValueAuto-increment PK
@ColumnCustomise column constraints
💡 NetBeans shortcut — right-click in the class body → Insert Code → Getter and Setter to generate all getters/setters automatically.
🗃️

BookDAO.java — Data Access Object

18 / 33
File: src/main/java/com/bookstore/dao/BookDAO.java — wraps all JPA calls so the rest of your code never touches EntityManager directly.
Java package com.bookstore.dao; import com.bookstore.entity.Book; import jakarta.persistence.*; import java.util.List; public class BookDAO { private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("BookstorePU"); // ── CREATE ────────────────────────────────────── public void save(Book book) { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(book); em.getTransaction().commit(); em.close(); } // ── READ (by id) ──────────────────────────────── public Book findById(Long id) { EntityManager em = emf.createEntityManager(); Book book = em.find(Book.class, id); em.close(); return book; } // ── READ (all books) ──────────────────────────── public List<Book> findAll() { EntityManager em = emf.createEntityManager(); List<Book> list = em .createQuery("SELECT b FROM Book b", Book.class) .getResultList(); em.close(); return list; } // ── UPDATE ────────────────────────────────────── public void update(Book book) { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.merge(book); em.getTransaction().commit(); em.close(); } // ── DELETE ────────────────────────────────────── public void delete(Long id) { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); Book book = em.find(Book.class, id); if (book != null) em.remove(book); em.getTransaction().commit(); em.close(); } public void close() { emf.close(); } }
▶️

Main.java — Run CRUD Operations

19 / 33
File: src/main/java/com/bookstore/Main.java
Java package com.bookstore; import com.bookstore.dao.BookDAO; import com.bookstore.entity.Book; import java.math.BigDecimal; import java.util.List; public class Main { public static void main(String[] args) { BookDAO dao = new BookDAO(); // ── CREATE ───────────────────────────────────── Book b1 = new Book("Clean Code", "Robert C. Martin", "978-0132350884", new BigDecimal("49.99"), 10); Book b2 = new Book("Effective Java", "Joshua Bloch", "978-0134685991", new BigDecimal("54.99"), 7); dao.save(b1); dao.save(b2); System.out.println("✅ Books saved: " + b1 + ", " + b2); // ── READ ALL ─────────────────────────────────── List<Book> books = dao.findAll(); System.out.println("\n📚 All Books:"); books.forEach(b -> System.out.println(" " + b)); // ── UPDATE ───────────────────────────────────── b1.setPrice(new BigDecimal("44.99")); dao.update(b1); System.out.println("\n✏️ Updated price: " + dao.findById(b1.getId())); // ── DELETE ───────────────────────────────────── dao.delete(b2.getId()); System.out.println("\n🗑️ After deleting b2: " + dao.findAll()); dao.close(); } }
🚀

Running the Project in NetBeans

20 / 33

Steps to Run

  1. 1
    Right-click project → Build (downloads Maven deps)
  2. 2
    Confirm no red underlines in the editor (all imports resolved)
  3. 3
    Right-click Main.javaRun File (or press F6)
  4. 4
    Watch the Output panel at the bottom of NetBeans

Expected Console Output

✅ Books saved: Book{id=1, title='Clean Code'...}
Book{id=2, title='Effective Java'...}

📚 All Books:
Book{id=1, title='Clean Code', price=49.99}
Book{id=2, title='Effective Java', price=54.99}

✏️ Updated price: Book{id=1, ..., price=44.99}

🗑️ After deleting b2: [Book{id=1...}]

Verify in MySQL Workbench

  1. 1
    Switch to Workbench → run query:
SQL SELECT * FROM bookstoredb.books;
You should see 1 row (Clean Code, price 44.99 after update, b2 deleted).
Can't connect? Check:
• MySQL service is running
• Password in persistence.xml is correct
useSSL=false is in the JDBC URL
🗺️

How JPA Maps Entity → Table

21 / 33
JAVA (JPA Entity)
MySQL (Database Table)
@Entity class Book { }
@Id Long id
@Column String title
@Column String author
@Column String isbn
@Column BigDecimal price
@Column int stockQty
Hibernate ORM
TABLE books
PK id INT AUTO_INCREMENT
title VARCHAR(255) NOT NULL
author VARCHAR(150) NOT NULL
isbn VARCHAR(20) UNIQUE
price DECIMAL(8,2)
stock_qty INT DEFAULT 0
Class name → Table name
Bookbooks (via @Table(name="books")).
Without the annotation, Hibernate defaults to the class name.
Field name → Column name
stockQtystock_qty (via @Column(name="stock_qty")).
Without it, the Java field name is used directly.
🔍

Useful JPQL Queries for the Bookstore

22 / 33
JPQL (Jakarta Persistence Query Language) queries entities and fields — not tables and columns — so it works across any supported database.

Find by Author

Java public List<Book> findByAuthor(String author) { EntityManager em = emf.createEntityManager(); return em .createQuery( "SELECT b FROM Book b WHERE b.author = :a", Book.class) .setParameter("a", author) .getResultList(); }

Find Cheap Books

Java public List<Book> findUnder(BigDecimal maxPrice) { EntityManager em = emf.createEntityManager(); return em .createQuery( "SELECT b FROM Book b WHERE b.price < :p ORDER BY b.price", Book.class) .setParameter("p", maxPrice) .getResultList(); }

Count Books

Java public long countBooks() { EntityManager em = emf.createEntityManager(); return em .createQuery( "SELECT COUNT(b) FROM Book b", Long.class) .getSingleResult(); }

Search by Title Keyword

Java public List<Book> searchTitle(String kw) { EntityManager em = emf.createEntityManager(); return em .createQuery( "SELECT b FROM Book b WHERE b.title LIKE :k", Book.class) .setParameter("k", "%" + kw + "%") .getResultList(); }
🔧

Common Errors & Fixes

23 / 33
Error / SymptomLikely CauseFix
Communications link failure MySQL not running or wrong port Start MySQL service; check port is 3306 in JDBC URL
Access denied for user 'root' Wrong password in persistence.xml Confirm password; consider creating a dedicated DB user
Unknown database 'bookstoredb' Database not created yet Run CREATE DATABASE bookstoredb in Workbench
No Persistence provider found Hibernate JARs not in classpath Run Maven → Clean & Build to re-download deps
Table 'books' doesn't exist hbm2ddl.auto not set to create/update Set hibernate.hbm2ddl.auto=update in persistence.xml
ClassNotFoundException: com.mysql.cj... Connector/J not on classpath Add mysql-connector-j dependency to pom.xml; rebuild
Entities saved but IDs are 0 Transaction not committed Ensure em.getTransaction().commit() is called

Lab Setup Checklist — Click to Track Progress

24 / 33

Installation

  • JDK 21 installed & JAVA_HOME set
  • NetBeans 21 installed
  • GlassFish/Payara server added
  • MySQL 8 Server running
  • MySQL Workbench connected

Database

  • bookstoredb schema created
  • books table created
  • Test row inserted & visible in Workbench

Project Setup

  • Maven project created in NetBeans
  • pom.xml dependencies added
  • persistence.xml in META-INF

Code

  • Book.java entity created
  • BookDAO.java with CRUD methods
  • Main.java runs without errors
  • Data verified in MySQL Workbench
🧠

Quiz — JPA Annotations

25 / 33
Q1. Which annotation marks a Java class as a JPA-managed persistent entity?
@Persistent
@Entity
@Table
@Mapped
Q2. What does GenerationType.IDENTITY mean in @GeneratedValue?
JPA generates the ID using a sequence table
The application must set the ID manually
The database auto-increments the column (e.g., MySQL AUTO_INCREMENT)
JPA assigns a UUID as the primary key
🧠

Quiz — Persistence & DAO

26 / 33
Q3. In the DAO, which EntityManager method saves a new entity to the database?
em.merge(entity)
em.persist(entity)
em.save(entity)
em.insert(entity)
Q4. Where must persistence.xml be placed in a Maven project?
src/main/java/META-INF/
src/main/java/
src/main/resources/META-INF/
src/META-INF/
🧠

Quiz — Connections & Configuration

27 / 33
Q5. Which property in persistence.xml controls whether Hibernate automatically creates/updates the database schema?
hibernate.auto_schema
hibernate.hbm2ddl.auto
hibernate.schema.update
jakarta.persistence.ddl
Q6. What is the default MySQL port used in the JDBC URL?
5432
1521
3306
8080

Named Queries Optional

28 / 33
What are Named Queries? Pre-defined JPQL queries attached to the entity class with @NamedQuery. They are parsed once at startup — more efficient and easier to maintain.

Define on the Entity

Java @Entity @Table(name = "books") @NamedQueries({ @NamedQuery( name = "Book.findAll", query = "SELECT b FROM Book b" ), @NamedQuery( name = "Book.findByAuthor", query = "SELECT b FROM Book b WHERE b.author = :author" ) }) public class Book { ... }

Call from DAO

Java public List<Book> findAll() { EntityManager em = emf.createEntityManager(); return em .createNamedQuery("Book.findAll", Book.class) .getResultList(); } public List<Book> byAuthor(String a) { EntityManager em = emf.createEntityManager(); return em .createNamedQuery("Book.findByAuthor", Book.class) .setParameter("author", a) .getResultList(); }
🔗

Adding a Category — @ManyToOne Optional

29 / 33

Category Entity

Java @Entity @Table(name="categories") public class Category { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @Column(nullable=false) private String name; @OneToMany(mappedBy="category", cascade=CascadeType.ALL) private List<Book> books; // constructors, getters, setters… }

Update Book Entity

Java // Inside Book.java — add this field: @ManyToOne @JoinColumn(name="category_id") private Category category; // getter / setter public Category getCategory() { return category; } public void setCategory(Category c) { category = c; }
Resulting SQL: Hibernate adds a category_id foreign key column to the books table automatically when hbm2ddl.auto=update.
🗂️

Complete Project File Map

30 / 33
Structure BookstoreJPA/ ├── pom.xml ← Maven deps └── src/ └── main/ ├── java/ │ └── com/bookstore/ │ ├── entity/ │ │ └── Book.java ← @Entity │ ├── dao/ │ │ └── BookDAO.java ← CRUD methods │ └── Main.java ← Entry point └── resources/ └── META-INF/ └── persistence.xml ← DB config

Data Flow Summary

1. Main.java calls BookDAO

Application code creates entity objects and calls DAO methods

2. BookDAO uses EntityManager

DAO opens an EM, begins a transaction, calls persist/merge/remove

3. Hibernate translates to SQL

The JPA provider generates INSERT/UPDATE/DELETE/SELECT SQL

4. MySQL executes the SQL

Data is persisted in the books table — visible in Workbench

📖

Key Concepts — Quick Reference

31 / 33
ConceptWhat It IsIn Our Project
JPAStandard API for ORM in Java/Jakarta EEThe annotations + EntityManager we use
HibernateThe JPA provider (implementation)Declared in pom.xml; does the SQL work
EntityA Java class mapped to a DB tableBook.java ← → books table
EntityManagerJPA's API for DB operationsUsed inside BookDAO to persist/find/merge/remove
Persistence UnitNamed config block in persistence.xmlBookstorePU — references DB + entity class
JPQLObject-oriented query language for JPASELECT b FROM Book b WHERE b.author = :a
CRUDCreate, Read, Update, Delete operationspersist / find / merge / remove in BookDAO
DAOData Access Object patternBookDAO.java — isolates DB logic
🧪

Lab Tasks — Try It Yourself

32 / 33

🟢 Core Tasks

  1. Follow slides 4–7: install all tools
  2. Create bookstoredb and books table in Workbench
  3. Create the Maven project with correct pom.xml
  4. Write Book.java, BookDAO.java, Main.java
  5. Run Main — confirm output in console and in Workbench

🔵 Extend the Entity

  1. Add a publishYear (int) field to Book
  2. Add a findByYear(int year) JPQL method to BookDAO
  3. Re-run and verify the new column appears in Workbench

🟠 Extension Challenges

  1. Convert inline JPQL queries to @NamedQuery
  2. Add a Category entity with a @ManyToOne relationship to Book
  3. Create a CategoryDAO with its own CRUD methods
  4. Write a query that retrieves all books in a given category
⭐ Challenge — Create a BookstoreMenu class that loops on a simple text menu (add/list/search/exit) so users can interact with the bookstore from the console.

JPA Bookstore — NetBeans & MySQL

33 / 33
🎓

You've built a working JPA Bookstore!

JDK 21 ✓ NetBeans ✓ MySQL ✓ Hibernate ✓ CRUD ✓

Next: Week 3 — Relationships, Cascading, and Jakarta EE Web Tier

📂

Files created

Book.java · BookDAO.java · Main.java · persistence.xml · pom.xml

🧠

Concepts covered

@Entity, @Id, @Column, EntityManager, JPQL, CRUD, DAO pattern

JPQL, Callbacks & Listeners

34 / 45
Week 4 Lab — Adding to BookstoreJPA

Extending Our Project

We will add Customer and Address entities to the existing BookstoreJPA project, covering three lab requirements: JPQL named queries, lifecycle callbacks, and entity listeners.

① Address entity ② Customer entity ③ Named & native queries ④ Callbacks ⑤ Listeners ⑥ CustomerDAO
🗂️

What to Add to the Existing Project

35 / 45
You do NOT need a new project. Add all files below to your existing BookstoreJPA project. The persistence.xml and pom.xml only need small additions.

New Files to Create

Structure src/main/java/com/bookstore/ ├── entity/ │ ├── Book.java ← already exists │ ├── Address.java ← NEW (Project 1) │ └── Customer.java ← NEW (Projects 1+2+3) ├── dao/ │ ├── BookDAO.java ← already exists │ └── CustomerDAO.java ← NEW ├── listener/ ← NEW package │ ├── CustomerValidator.java │ └── CustomerAgeListener.java ├── Main.java ← already exists └── Week4Main.java ← NEW entry point

Steps Before Writing Code

  1. 1
    Right-click Source Packages → New → Java Package
    Name: com.bookstore.listener → Finish
  2. 2
    Create each new .java file by right-clicking the correct package → New → Java Class
  3. 3
    Add the 2 new entity class names to persistence.xml
    (shown on slide 38)
💡 Combining all 3 projects
The Customer entity will satisfy all three lab projects at once — named queries (P1), callback methods (P2), and external listeners (P3) are all declared on the same class.
🏠

Address.java — Entity

36 / 45
File: src/main/java/com/bookstore/entity/Address.java
Java package com.bookstore.entity; import jakarta.persistence.*; @Entity @Table(name = "addresses") public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "street1") private String street1; @Column(name = "city") private String city; @Column(name = "zipcode") private String zipcode; @Column(name = "country") private String country; public Address() { } public Address( String street1, String city, String zipcode, String country) { this.street1 = street1; this.city = city; this.zipcode = zipcode; this.country = country; } public Long getId() { return id; } public String getStreet1() { return street1; } public String getCity() { return city; } public String getZipcode() { return zipcode; } public String getCountry() { return country; } public void setStreet1(String s) { street1 = s; } public void setCity(String c) { city = c; } public void setZipcode(String z) { zipcode = z; } public void setCountry(String c) { country = c; } @Override public String toString() { return street1 + ", " + city + " " + zipcode + ", " + country; } }

Resulting Table

ColumnType
idBIGINT PK AUTO_INCREMENT
street1VARCHAR(255)
cityVARCHAR(255)
zipcodeVARCHAR(255)
countryVARCHAR(255)
How Customer links to Address
The Customer entity has a @ManyToOne field pointing to Address. Hibernate adds an address_id foreign key column to the customers table automatically.
Cascade saves
When persisting a Customer, you must either persist the Address first, or add cascade = CascadeType.PERSIST to the @ManyToOne annotation on Customer. We use cascade in this project.
👤

Customer.java — Fields, Named & Native Queries

37 / 45
File: src/main/java/com/bookstore/entity/Customer.java — this one class satisfies all three lab projects.
Java package com.bookstore.entity; import jakarta.persistence.*; import java.util.Date; import com.bookstore.listener.CustomerValidator; import com.bookstore.listener.CustomerAgeListener; // ── Project 1: Named JPQL queries ─────────────────────── @NamedQueries({ @NamedQuery( name = "Customer.findAll", query = "SELECT c FROM Customer c" ), @NamedQuery( name = "Customer.findByFirstName", query = "SELECT c FROM Customer c WHERE c.firstName = :firstName" ) }) // ── Project 1: Native SQL query ────────────────────────── @NamedNativeQuery( name = "Customer.findAllNative", query = "SELECT * FROM customers", resultClass = Customer.class ) // ── Project 3: Register external listeners ─────────────── @EntityListeners({ CustomerValidator.class, CustomerAgeListener.class }) @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; private String email; @Column(name = "phone_number") private String phoneNumber; @Temporal(TemporalType.DATE) @Column(name = "date_of_birth") private Date dateOfBirth; @Transient // NOT persisted — age changes each year private Integer age; @Temporal(TemporalType.TIMESTAMP) @Column(name = "creation_date") private Date creationDate; @ManyToOne(cascade = CascadeType.PERSIST) @JoinColumn(name = "address_id") private Address address; // constructors, getters, setters — next slide }
👤

Customer.java — Constructors, Getters & Setters

38 / 45
Java // ── No-arg constructor (required by JPA) ── public Customer() { } // ── Convenience constructor ── public Customer( String firstName, String lastName, String email, String phoneNumber, Date dateOfBirth, Address address) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.phoneNumber = phoneNumber; this.dateOfBirth = dateOfBirth; this.address = address; } // ── Getters ── public Long getId() { return id; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public String getPhoneNumber() { return phoneNumber; } public Date getDateOfBirth() { return dateOfBirth; } public Integer getAge() { return age; } public Date getCreationDate(){ return creationDate; } public Address getAddress() { return address; } // ── Setters ── public void setFirstName(String fn) { firstName = fn; } public void setLastName(String ln) { lastName = ln; } public void setEmail(String e) { email = e; } public void setPhoneNumber(String p) { phoneNumber = p; } public void setDateOfBirth(Date d) { dateOfBirth = d; } public void setAge(Integer a) { age = a; } public void setCreationDate(Date cd) { creationDate= cd; } public void setAddress(Address addr) { address = addr; } @Override public String toString() { return "Customer{" + "id=" + id + ", name='" + firstName + " " + lastName + "'" + ", email='" + email + "'" + ", age=" + age + "}"; }

Key Annotation Recap

AnnotationMeaning
@TransientField exists in Java but is not stored in DB
@TemporalTells JPA how to store a java.util.Date (DATE / TIMESTAMP)
@ManyToOneMany customers can share one address
@JoinColumnNames the FK column (address_id) in customers table
cascade = PERSISTSaves the Address automatically when the Customer is saved
@NamedQueryPre-compiled JPQL (uses entity/field names)
@NamedNativeQueryPre-compiled raw SQL (uses table/column names)
@EntityListenersRegisters external listener classes for lifecycle events
🔒

CustomerValidator.java — Pre-Persist / Pre-Update Listener

39 / 45
File: src/main/java/com/bookstore/listener/CustomerValidator.java
Java package com.bookstore.listener; import com.bookstore.entity.Customer; import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; public class CustomerValidator { @PrePersist @PreUpdate public void validate(Customer customer) { if (customer.getFirstName() == null || customer.getFirstName().trim().isEmpty()) { throw new IllegalArgumentException( "First name cannot be empty." ); } if (customer.getLastName() == null || customer.getLastName().trim().isEmpty()) { throw new IllegalArgumentException( "Last name cannot be empty." ); } System.out.println( "[Validator] Passed for: " + customer.getFirstName() + " " + customer.getLastName() ); } }

How Lifecycle Listeners Work

em.persist(customer)
@PrePersist
validate() fires
em.merge(customer)
@PreUpdate
validate() fires
Listener vs. Callback — what's the difference?
A callback (Project 2) is a method inside the entity class itself.
A listener (Project 3) is a method in a separate class registered via @EntityListeners. Listeners are preferred because they keep business logic out of the entity.
Listener method signature rule
The method must take the entity as its single parameter: void validate(Customer c).
Unlike callback methods in the entity itself, which take no parameters.
🎂

CustomerAgeListener.java — Post-Persist / Post-Load Listener

40 / 45
File: src/main/java/com/bookstore/listener/CustomerAgeListener.java
Java package com.bookstore.listener; import com.bookstore.entity.Customer; import jakarta.persistence.PostLoad; import jakarta.persistence.PostPersist; import jakarta.persistence.PostUpdate; import java.util.Calendar; public class CustomerAgeListener { @PostPersist @PostLoad @PostUpdate public void calculateAge(Customer customer) { if (customer.getDateOfBirth() == null) { return; } Calendar dob = Calendar.getInstance(); dob.setTime(customer.getDateOfBirth()); Calendar today = Calendar.getInstance(); int age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR); // Adjust if birthday hasn't occurred yet this year if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) { age--; } customer.setAge(age); System.out.println( "[AgeListener] Age set to " + age + " for: " + customer.getFirstName() ); } }

All JPA Lifecycle Events

AnnotationFires
@PrePersistBefore em.persist()
@PostPersistAfter INSERT committed to DB
@PreUpdateBefore em.merge() / dirty flush
@PostUpdateAfter UPDATE committed to DB
@PreRemoveBefore em.remove()
@PostRemoveAfter DELETE committed to DB
@PostLoadAfter entity loaded from DB (find / query)
Why @Transient + @PostLoad?
age is marked @Transient so it is never saved to the database. But every time a Customer is loaded from the DB, @PostLoad fires and recalculates the current age from the stored dateOfBirth — always up to date.
🗃️

CustomerDAO.java — Named & Native Queries

41 / 45
File: src/main/java/com/bookstore/dao/CustomerDAO.java
Java package com.bookstore.dao; import com.bookstore.entity.Customer; import jakarta.persistence.*; import java.util.List; public class CustomerDAO { private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("BookstorePU"); // ── CREATE ────────────────────────────────────────────── public void save(Customer customer) { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(customer); // triggers @PrePersist listener em.getTransaction().commit(); // triggers @PostPersist listener em.close(); } // ── Named Query: all customers (JPQL) ─────────────────── public List<Customer> findAll() { EntityManager em = emf.createEntityManager(); return em .createNamedQuery("Customer.findAll", Customer.class) .getResultList(); // triggers @PostLoad for each row } // ── Named Query: by first name (JPQL) ─────────────────── public List<Customer> findByFirstName(String firstName) { EntityManager em = emf.createEntityManager(); return em .createNamedQuery("Customer.findByFirstName", Customer.class) .setParameter("firstName", firstName) .getResultList(); } // ── Named Native Query: raw SQL ────────────────────────── public List<Customer> findAllNative() { EntityManager em = emf.createEntityManager(); return em .createNamedQuery("Customer.findAllNative", Customer.class) .getResultList(); } public void close() { emf.close(); } }
⚙️

Update persistence.xml — Register New Entities

42 / 45
You must register every entity class in persistence.xml. Open src/main/resources/META-INF/persistence.xml and add the two new <class> lines highlighted below.
XML <persistence-unit name="BookstorePU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <!-- Already registered --> <class>com.bookstore.entity.Book</class> <!-- ADD THESE TWO NEW LINES --> <class>com.bookstore.entity.Address</class> <class>com.bookstore.entity.Customer</class> <properties> <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/bookstore?useSSL=false&amp;serverTimezone=UTC"/> <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="jakarta.persistence.jdbc.user" value="root"/> <property name="jakarta.persistence.jdbc.password" value="YourPassword"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> </properties> </persistence-unit>
hibernate.hbm2ddl.auto = update means Hibernate will automatically create the new addresses and customers tables in MySQL when you run the project. No SQL needed.
▶️

Week4Main.java — Testing All Three Projects

43 / 45
File: src/main/java/com/bookstore/Week4Main.java — right-click this file → Run File to test
Java package com.bookstore; import com.bookstore.dao.CustomerDAO; import com.bookstore.entity.Address; import com.bookstore.entity.Customer; import java.util.Calendar; import java.util.List; public class Week4Main { public static void main(String[] args) { CustomerDAO dao = new CustomerDAO(); // ── Build addresses ───────────────────────────────────── Address addr1 = new Address("10 Main St", "Brisbane", "4000", "Australia"); Address addr2 = new Address("5 Park Ave", "Sydney", "2000", "Australia"); // ── Build customers with date of birth ────────────────── Calendar cal1 = Calendar.getInstance(); cal1.set(1990, Calendar.MARCH, 15); Calendar cal2 = Calendar.getInstance(); cal2.set(1985, Calendar.JULY, 22); Customer c1 = new Customer( "Alice", "Smith", "[email protected]", "0400111222", cal1.getTime(), addr1 ); Customer c2 = new Customer( "Bob", "Jones", "[email protected]", "0400333444", cal2.getTime(), addr2 ); // ── Persist (triggers @PrePersist validate, @PostPersist calculateAge) dao.save(c1); dao.save(c2); System.out.println("Customers saved."); // ── Named JPQL query: all customers ────────────────────── System.out.println(" --- Named Query: All Customers ---"); List<Customer> all = dao.findAll(); // triggers @PostLoad calculateAge all.forEach(c -> System.out.println(c)); // ── Named JPQL query: by first name ────────────────────── System.out.println(" --- Named Query: Find by First Name 'Alice' ---"); dao.findByFirstName("Alice") .forEach(c -> System.out.println(c)); // ── Named Native query: raw SQL ─────────────────────────── System.out.println(" --- Native Query: All Customers (raw SQL) ---"); dao.findAllNative() .forEach(c -> System.out.println(c)); dao.close(); } }

Expected Console Output & Workbench Verification

44 / 45

Console Output (trimmed)

[Validator] Passed for: Alice Smith
[AgeListener] Age set to 35 for: Alice
[Validator] Passed for: Bob Jones
[AgeListener] Age set to 40 for: Bob
Customers saved.

--- Named Query: All Customers ---
[AgeListener] Age set to 35 for: Alice
Customer{id=1, name='Alice Smith', age=35}
[AgeListener] Age set to 40 for: Bob
Customer{id=2, name='Bob Jones', age=40}

--- Named Query: Find by First Name 'Alice' ---
Customer{id=1, name='Alice Smith', age=35}

--- Native Query: All Customers (raw SQL) ---
Customer{id=1, name='Alice Smith', age=35}
Customer{id=2, name='Bob Jones', age=40}

Verify in MySQL Workbench

SQL -- Check new tables were auto-created SHOW TABLES FROM bookstore; -- Check addresses SELECT * FROM bookstore.addresses; -- Check customers (note: no 'age' column) SELECT * FROM bookstore.customers; -- Check FK relationship SELECT c.first_name, c.last_name, a.city FROM customers c JOIN addresses a ON c.address_id = a.id;
Key things to confirm:
1. No age column in customers table — it is @Transient
2. customers.address_id FK column links to addresses.id
3. The listener print lines appear before "Customers saved" on persist, and again on every load — confirming the lifecycle events fired

JPQL, Callbacks & Listeners — Summary

45 / 45
🎓

Week 4 Lab Complete!

@NamedQuery ✓ @NamedNativeQuery ✓ @Transient ✓ @PrePersist ✓ @PostLoad ✓ @EntityListeners ✓
📋

Project 1

Customer + Address entities, @NamedQuery (JPQL), @NamedNativeQuery (SQL)

🔁

Project 2

@Transient age, @PrePersist validate(), @PostLoad calculateAge()

👂

Project 3

External listener classes, @EntityListeners, same events but decoupled