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.
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 TypedQueryquery = 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 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(); CriteriaQuerycq = 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();
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(); } }