Java Database Connectivity Stack

JDBC: The Foundation

At the lowest level, we have JDBC (Java Database Connectivity) API which provides a standard API for connecting to databases. The JDBC API is comprised of two packages:

To use the JDBC API with a particular database management system, you need a JDBC technology-based driver to mediate between JDBC technology and the database. Working directly with JDBC would look something like this:

// Establish connection
Connection conn = DriverManager.getConnection(
    "jdbc:sqlserver://localhost:1433;databaseName=MyAppDB", "user", "password");

// Create a statement
PreparedStatement stmt = conn.prepareStatement(
    "SELECT Id, Name, Description, UnitPrice, IsFeatured FROM Products WHERE Id = ?");
stmt.setString(1, productId.toString());

// Execute query and process results
ResultSet rs = stmt.executeQuery();
Product product = null;
if (rs.next()) {
    product = new Product();
    product.setId(UUID.fromString(rs.getString("Id")));
    product.setName(rs.getString("Name"));
    product.setDescription(rs.getString("Description"));
    product.setUnitPrice(rs.getBigDecimal("UnitPrice"));
    product.setIsFeatured(rs.getBoolean("IsFeatured"));
}

// Close resources
rs.close();
stmt.close();
conn.close();

While using JDBC directly (as oppose to using it through a wrapper) is powerful and give us full control over database access, this approach is verbose, error-prone, and requires explicit resource management. We'd need to write similar boilerplate code for every database operation, which is clearly not sustainable for a large application.

JPA: Object-Relational Mapping (ORM)

An ORM solution implies, in the Java world, the JPA specification and a JPA implementation —Hibernate being the most popular nowadays.

JPA (Jakarta Persistence API, formerly Java Persistence API) is a object-relational mapping (ORM) specification defining an API that manages the persistence of objects and object/relational mappings. It allows us to work with Java objects rather than SQL statements and result sets. The core of JPA revolves around the EntityManager, which manages the lifecycle of entities.

// Get an EntityManager from the EntityManagerFactory
EntityManager em = entityManagerFactory.createEntityManager();

// Begin transaction
em.getTransaction().begin();

// Simple CRUD operations
// Create
Product newProduct = new Product();
newProduct.setId(UUID.randomUUID());
newProduct.setName("New Widget");
newProduct.setDescription("A fantastic new widget");
newProduct.setUnitPrice(new BigDecimal("19.99"));
newProduct.setIsFeatured(true);
em.persist(newProduct);

// Read
Product foundProduct = em.find(Product.class, productId);

// Update
foundProduct.setPrice(new BigDecimal("24.99"));
em.merge(foundProduct);

// Delete
em.remove(foundProduct);

// Commit transaction
em.getTransaction().commit();

// Close EntityManager
em.close();

JPA also provides JPQL (Java Persistence Query Language), a SQL-like language that operates on entities rather than tables:

// Create a query using JPQL
TypedQuery query = em.createQuery(
    "SELECT p FROM Product p WHERE p.unitPrice < :price", Product.class);
query.setParameter("price", new BigDecimal("50.00"));
List affordableProducts = query.getResultList();

Hibernate: JPA Implementation

Hibernate is the most popular implementation of this specification. So, JPA will specify what must be done to persist objects, while Hibernate will determine how to do it.

It adds additional features and optimizations beyond what JPA requires. While we could use Hibernate's native API, it's generally recommended to program to the JPA interface for better portability.

Hibernate handles many complex scenarios efficiently, such as lazy loading of relationships, caching to improve performance, and sophisticated mapping strategies. It also provides a powerful criteria API for building type-safe queries:

// Using Hibernate Criteria API (via JPA)
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(Product.class);
Root product = cq.from(Product.class);
cq.select(product)
  .where(cb.like(product.get("name"), "%Widget%"));
List widgets = em.createQuery(cq).getResultList();

Spring Data JPA: Repositories Simplify Everything

So JPA and Hibernate are great, but we still have to write a lot of repetitive code for common operations and things like pagination, auditing and other common features. This is where Spring Data JPA comes in.

Spring Data JPA significantly reduces code verbosity by eliminating several layers of boilerplate compared to using plain JPA or native Hibernate. When using just JPA/Hibernate, you would typically need:

With Spring Data JPA, all of this is handled for you:

When comparing these approaches, using plain JPA looks like this:

// Service layer
public Product findProduct(UUID id) {
    EntityManager em = entityManagerFactory.createEntityManager();
    try {
        em.getTransaction().begin();
        Product product = em.find(Product.class, id);
        em.getTransaction().commit();
        return product;
    } catch (Exception e) {
        if (em.getTransaction().isActive()) {
            em.getTransaction().rollback();
        }
        throw e;
    } finally {
        em.close();
    }
}