ORMs (Object-Relational Mapping)

Bridging the gap between objects and databases

← Back to Index

What is ORM?

Object-Relational Mapping (ORM) is a programming technique that converts data between incompatible type systems: object-oriented programming languages and relational databases. ORMs allow developers to work with database data using their language's native objects instead of writing SQL.

The Impedance Mismatch Problem

// Object-Oriented World vs Relational World

┌────────────────────────────────────────────────────────────────────┐
│         OBJECTS (Java)              │        TABLES (SQL)          │
├────────────────────────────────────────────────────────────────────┤
│                                     │                              │
│  class Employee {                   │  CREATE TABLE employees (    │
│      Long id;                       │      id BIGINT PRIMARY KEY,  │
│      String name;                   │      name VARCHAR(100),      │
│      Department dept;  ← Object     │      dept_id BIGINT  ← FK    │
│      List<Project> projects; ←Many │  );                          │
│  }                                  │                              │
│                                     │  CREATE TABLE emp_projects ( │
│  Inheritance:                       │      emp_id BIGINT,          │
│  class Manager extends Employee     │      proj_id BIGINT          │
│                                     │  ); ← Join table             │
│                                     │                              │
│  Encapsulation:                     │  No encapsulation -          │
│  private fields, methods            │  just columns                │
│                                     │                              │
│  Identity:                          │  Identity:                   │
│  emp1 == emp2 (reference)           │  id = id (value)             │
│                                     │                              │
└────────────────────────────────────────────────────────────────────┘

// Key mismatches ORMs solve:
// 1. Granularity - Objects can have complex structure, tables are flat
// 2. Inheritance - Objects have it, tables don't
// 3. Identity - Object identity vs primary key
// 4. Associations - References vs foreign keys
// 5. Navigation - Objects traverse, SQL joins

What ORM Does

// WITHOUT ORM (Manual mapping)
public Employee findById(long id) {
    String sql = "SELECT * FROM employees WHERE id = ?";

    try (Connection conn = getConnection();
         PreparedStatement stmt = conn.prepareStatement(sql)) {

        stmt.setLong(1, id);
        ResultSet rs = stmt.executeQuery();

        if (rs.next()) {
            Employee emp = new Employee();
            emp.setId(rs.getLong("id"));
            emp.setName(rs.getString("name"));
            emp.setEmail(rs.getString("email"));
            emp.setSalary(rs.getBigDecimal("salary"));
            // Load department? More queries...
            // Load projects? Even more queries...
            return emp;
        }
    }
    return null;
}

// WITH ORM (Automatic mapping)
public Employee findById(long id) {
    return entityManager.find(Employee.class, id);  // That's it!
}

ORM Benefits:

  • Productivity: Write less boilerplate code
  • Maintainability: Schema changes in one place
  • Type Safety: Compile-time checking
  • Database Independence: Switch databases easier
  • Caching: Built-in caching mechanisms
  • Lazy Loading: Load data on demand

ORM Frameworks in Java

Framework Type Description Best For
Hibernate Full ORM Most popular, JPA provider, feature-rich Most enterprise applications
EclipseLink Full ORM Reference JPA implementation JPA-strict applications
MyBatis SQL Mapper SQL-centric, more control Complex SQL, DBA teams
jOOQ SQL Builder Type-safe SQL, code generation SQL-heavy applications
Spring Data JPA Repository Abstraction over JPA Spring Boot applications
JDBI SQL Abstraction Lightweight, SQL-focused Simple JDBC wrapper

Framework Categories

// 1. FULL ORM (Hibernate, EclipseLink)
// - Complete object-relational mapping
// - Automatic SQL generation
// - Caching, lazy loading, dirty checking
// - You work with objects, framework handles SQL

@Entity
public class Employee {
    @Id private Long id;
    private String name;
    @ManyToOne private Department dept;
}

Employee emp = em.find(Employee.class, 1L);  // No SQL written!

// 2. SQL MAPPER (MyBatis)
// - You write SQL, framework maps results
// - More control over queries
// - No automatic relationship handling

// mapper.xml
<select id="findById" resultType="Employee">
    SELECT * FROM employees WHERE id = #{id}
</select>

Employee emp = mapper.findById(1L);  // Your SQL, mapped result

// 3. SQL BUILDER (jOOQ)
// - Type-safe SQL construction
// - Compile-time SQL validation
// - Generated code from schema

Result<Record> result = create
    .select(EMPLOYEE.NAME, DEPARTMENT.NAME)
    .from(EMPLOYEE)
    .join(DEPARTMENT).on(EMPLOYEE.DEPT_ID.eq(DEPARTMENT.ID))
    .where(EMPLOYEE.SALARY.gt(50000))
    .fetch();

Hibernate: The Most Popular ORM

Key Features

// Hibernate provides:

// 1. TRANSPARENT PERSISTENCE
// - Persist objects without implementing interfaces
// - POJOs with annotations

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private BigDecimal price;
}

// 2. AUTOMATIC DIRTY CHECKING
// - Hibernate tracks changes to managed entities
// - Updates generated automatically at flush time

Product product = em.find(Product.class, 1L);
product.setPrice(new BigDecimal("99.99"));  // Changed!
// No explicit save needed - Hibernate sees the change
tx.commit();  // UPDATE product SET price = 99.99 WHERE id = 1

// 3. LAZY LOADING
// - Load relationships on demand

@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;

Order order = em.find(Order.class, 1L);  // Items NOT loaded
order.getItems().size();  // NOW items are loaded

// 4. CACHING
// - First level cache (session-scoped)
// - Second level cache (shared)

Product p1 = em.find(Product.class, 1L);  // DB query
Product p2 = em.find(Product.class, 1L);  // From L1 cache!
System.out.println(p1 == p2);  // true - same instance

// 5. HQL (Hibernate Query Language)
// - Object-oriented SQL

List<Product> products = em.createQuery(
    "SELECT p FROM Product p WHERE p.price > :minPrice",
    Product.class
)
.setParameter("minPrice", new BigDecimal("50"))
.getResultList();

Hibernate Architecture

// Hibernate Core Components

┌─────────────────────────────────────────────────────────────────────┐
│                        APPLICATION                                   │
└────────────────────────────┬────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│                   SessionFactory / EntityManagerFactory              │
│   - One per database (expensive to create)                          │
│   - Thread-safe                                                      │
│   - Creates Sessions                                                 │
└────────────────────────────┬────────────────────────────────────────┘
                             │ creates
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│                   Session / EntityManager                            │
│   - One per request/transaction                                      │
│   - NOT thread-safe                                                  │
│   - Contains persistence context (L1 cache)                         │
│   - Manages entity lifecycle                                         │
└────────────────────────────┬────────────────────────────────────────┘
                             │ uses
                             ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       JDBC / Connection Pool                         │
└─────────────────────────────────────────────────────────────────────┘

MyBatis: SQL-Centric Approach

When to Choose MyBatis

// MyBatis is better when:
// - You have complex, hand-tuned SQL
// - DBAs want control over SQL
// - Working with stored procedures
// - Legacy database with unusual schema
// - Need database-specific features

MyBatis Configuration

<!-- Mapper XML -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.EmployeeMapper">

    <!-- Result mapping -->
    <resultMap id="employeeResultMap" type="Employee">
        <id property="id" column="emp_id"/>
        <result property="name" column="emp_name"/>
        <result property="email" column="emp_email"/>
        <association property="department" javaType="Department">
            <id property="id" column="dept_id"/>
            <result property="name" column="dept_name"/>
        </association>
    </resultMap>

    <!-- Queries -->
    <select id="findById" resultMap="employeeResultMap">
        SELECT
            e.id as emp_id,
            e.name as emp_name,
            e.email as emp_email,
            d.id as dept_id,
            d.name as dept_name
        FROM employees e
        LEFT JOIN departments d ON e.dept_id = d.id
        WHERE e.id = #{id}
    </select>

    <select id="findByDepartment" resultMap="employeeResultMap">
        SELECT * FROM employees WHERE dept_id = #{deptId}
    </select>

    <!-- Dynamic SQL -->
    <select id="search" resultMap="employeeResultMap">
        SELECT * FROM employees
        <where>
            <if test="name != null">
                AND name LIKE #{name}
            </if>
            <if test="email != null">
                AND email = #{email}
            </if>
            <if test="minSalary != null">
                AND salary >= #{minSalary}
            </if>
        </where>
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO employees (name, email, dept_id)
        VALUES (#{name}, #{email}, #{department.id})
    </insert>

</mapper>

MyBatis Mapper Interface

public interface EmployeeMapper {

    Employee findById(long id);

    List<Employee> findByDepartment(long deptId);

    List<Employee> search(EmployeeSearchCriteria criteria);

    void insert(Employee employee);

    void update(Employee employee);

    void delete(long id);

    // With annotations (alternative to XML)
    @Select("SELECT * FROM employees WHERE id = #{id}")
    Employee findByIdSimple(long id);

    @Insert("INSERT INTO employees (name, email) VALUES (#{name}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertSimple(Employee employee);
}

// Usage
@Service
public class EmployeeService {

    private final EmployeeMapper mapper;

    public Employee getEmployee(long id) {
        return mapper.findById(id);  // Your SQL, automatic mapping
    }
}

jOOQ: Type-Safe SQL

Why jOOQ?

// jOOQ generates Java code from your database schema
// Provides compile-time type safety for SQL

// Traditional JPQL - errors at runtime
em.createQuery("SELECT e FROM Employe e");  // Typo! Runtime error

// jOOQ - errors at compile time
create.selectFrom(EMPLOYE);  // Won't compile if EMPLOYE doesn't exist

jOOQ Examples

import static org.jooq.impl.DSL.*;
import static com.example.generated.Tables.*;  // Generated from DB

public class EmployeeRepository {

    private final DSLContext create;

    // Simple query
    public List<Employee> findAll() {
        return create
            .selectFrom(EMPLOYEES)
            .fetchInto(Employee.class);
    }

    // Query with conditions
    public List<Employee> findBySalaryRange(BigDecimal min, BigDecimal max) {
        return create
            .selectFrom(EMPLOYEES)
            .where(EMPLOYEES.SALARY.between(min, max))
            .orderBy(EMPLOYEES.SALARY.desc())
            .fetchInto(Employee.class);
    }

    // Join query
    public List<EmployeeWithDept> findWithDepartment() {
        return create
            .select(EMPLOYEES.NAME, DEPARTMENTS.NAME.as("dept_name"))
            .from(EMPLOYEES)
            .join(DEPARTMENTS).on(EMPLOYEES.DEPT_ID.eq(DEPARTMENTS.ID))
            .fetchInto(EmployeeWithDept.class);
    }

    // Aggregate query
    public Map<String, BigDecimal> avgSalaryByDepartment() {
        return create
            .select(DEPARTMENTS.NAME, avg(EMPLOYEES.SALARY))
            .from(EMPLOYEES)
            .join(DEPARTMENTS).on(EMPLOYEES.DEPT_ID.eq(DEPARTMENTS.ID))
            .groupBy(DEPARTMENTS.NAME)
            .fetchMap(DEPARTMENTS.NAME, avg(EMPLOYEES.SALARY));
    }

    // Insert
    public void insert(Employee emp) {
        create
            .insertInto(EMPLOYEES, EMPLOYEES.NAME, EMPLOYEES.EMAIL, EMPLOYEES.SALARY)
            .values(emp.getName(), emp.getEmail(), emp.getSalary())
            .execute();
    }

    // Update
    public void giveRaise(Long deptId, BigDecimal percentage) {
        create
            .update(EMPLOYEES)
            .set(EMPLOYEES.SALARY, EMPLOYEES.SALARY.multiply(BigDecimal.ONE.add(percentage)))
            .where(EMPLOYEES.DEPT_ID.eq(deptId))
            .execute();
    }

    // Complex dynamic query
    public List<Employee> search(SearchCriteria criteria) {
        SelectConditionStep<Record> query = create
            .selectFrom(EMPLOYEES)
            .where(trueCondition());  // Start with always-true

        if (criteria.getName() != null) {
            query = query.and(EMPLOYEES.NAME.containsIgnoreCase(criteria.getName()));
        }
        if (criteria.getMinSalary() != null) {
            query = query.and(EMPLOYEES.SALARY.ge(criteria.getMinSalary()));
        }
        if (criteria.getDeptId() != null) {
            query = query.and(EMPLOYEES.DEPT_ID.eq(criteria.getDeptId()));
        }

        return query.fetchInto(Employee.class);
    }
}

Choosing the Right Approach

Decision Matrix

Scenario Recommended Reason
New enterprise app JPA (Hibernate) Productivity, ecosystem, Spring integration
Complex queries, DBA team MyBatis Full SQL control, query optimization
Type-safe SQL, refactoring jOOQ Compile-time validation, IDE support
Simple CRUD, Spring Boot Spring Data JPA Minimal code, rapid development
Stored procedures heavy MyBatis or jOOQ Better stored procedure support
Database-first design jOOQ Code generation from schema
Domain-first design JPA (Hibernate) Code generates schema

Hybrid Approaches

// You can combine approaches!

// Spring Data JPA for simple CRUD
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByDepartmentId(Long deptId);
}

// Native query for complex cases
@Query(value = """
    SELECT e.*, d.name as dept_name
    FROM employees e
    JOIN departments d ON e.dept_id = d.id
    WHERE e.salary > (SELECT AVG(salary) FROM employees)
    """, nativeQuery = true)
List<EmployeeProjection> findHighEarners();

// Or use jOOQ for complex reporting while JPA handles CRUD
@Service
public class ReportService {

    private final DSLContext dsl;  // jOOQ

    public List<SalaryReport> generateSalaryReport() {
        return dsl
            .select(
                DEPARTMENTS.NAME,
                count(EMPLOYEES.ID).as("emp_count"),
                sum(EMPLOYEES.SALARY).as("total_salary"),
                avg(EMPLOYEES.SALARY).as("avg_salary")
            )
            .from(EMPLOYEES)
            .join(DEPARTMENTS).on(EMPLOYEES.DEPT_ID.eq(DEPARTMENTS.ID))
            .groupBy(DEPARTMENTS.NAME)
            .having(count(EMPLOYEES.ID).gt(5))
            .orderBy(avg(EMPLOYEES.SALARY).desc())
            .fetchInto(SalaryReport.class);
    }
}

ORM Best Practices

General Guidelines

  • Know your ORM: Understand how it generates SQL
  • Monitor SQL: Always enable SQL logging in development
  • Watch for N+1: Use JOIN FETCH or batch loading
  • Use DTOs: For read-only data, project into DTOs
  • Transaction boundaries: Keep transactions short
  • Don't fight the ORM: If ORM makes it hard, maybe use native SQL

Hibernate-Specific

  • Prefer LAZY: Make all relationships lazy by default
  • Avoid open-session-in-view: Fetch data in service layer
  • Use natural IDs: For business-meaningful lookups
  • Consider stateless session: For batch operations

MyBatis-Specific

  • Use result maps: For complex mappings
  • Leverage dynamic SQL: <if>, <choose>, <foreach>
  • Enable caching: For read-heavy queries

Summary

  • ORM: Bridges object-oriented and relational worlds
  • Hibernate: Full-featured ORM, JPA provider, most popular
  • MyBatis: SQL mapper, you control the SQL
  • jOOQ: Type-safe SQL builder, compile-time validation
  • Spring Data JPA: Repository abstraction over JPA
  • Trade-offs: Productivity vs Control vs Type Safety
  • Hybrid: Mix approaches for different use cases
  • Key: Understand generated SQL, monitor performance