Skip to content

JPA Entity Relationships, Naming Strategy, and Advanced Queries

A Physical Naming Strategy controls how Java entity and field names map to physical database table and column names.

Example entity:

@Entity
class EmployeeDetails {
}

Without naming strategy:

EmployeeDetails

With Spring Physical Naming Strategy:

employee_details
  • Consistent naming convention
  • Automatic camelCase to snake_case conversion
  • Reduces manual @Column annotations
  • Matches enterprise database naming standards

Configuration:

spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

Beyond derived methods, JPQL, and native queries, two more query mechanisms are worth knowing:

@NamedQuery(
name="Employee.findByDept",
query="SELECT e FROM Employee e WHERE e.department=:dept"
)
Specification<Employee> spec = (root, query, cb) ->
cb.equal(root.get("department"), "IT");

Specifications let you build queries programmatically and compose them (e.g. combine multiple Specifications with .and()/.or()), which is useful for dynamic filtering that derived methods and static JPQL can’t express cleanly.

Repository method:

List<Employee> findBySalaryBetween(Long minSalary, Long maxSalary);

Usage:

employeeRepo.findBySalaryBetween(3000000L, 3500000L);

JPQL:

@Query("SELECT e FROM Employee e WHERE e.salary BETWEEN :min AND :max")
List<Employee> findEmployees(Long min, Long max);
@Query("SELECT AVG(e.salary) FROM Employee e")
Double findAverageSalary();

Example: one Customer can have many Orders.

erDiagram
    CUSTOMER ||--o{ ORDER : places

Customer:

@Entity
public class Customer {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders;
}

Order:

@Entity
public class Order {
@Id
private Long id;
@ManyToOne
@JoinColumn(name="customer_id")
private Customer customer;
}

Relationship types: One-to-One, One-to-Many, Many-to-One, Many-to-Many.

A JPQL join lets you filter on a related entity’s fields directly, without needing to fetch the related entities eagerly:

@Query("SELECT o FROM Order o JOIN o.customer c WHERE c.name = :name")
List<Order> findOrdersByCustomer(String name);

This is distinct from a JOIN FETCH (used to solve the N+1 problem) — a plain JOIN is for filtering/joining conditions, not for eagerly loading the association into the result.

  • A Physical Naming Strategy is what converts EmployeeDetails to employee_details automatically, so you don’t need @Table/@Column overrides just to follow a snake_case DB convention.
  • Named Queries centralize JPQL alongside the entity; the Specification API is better suited to dynamic, composable filtering that a fixed query string can’t express.
  • Entity relationships (@OneToMany/@ManyToOne) are declared on both sides — the “many” side owns the foreign key (@JoinColumn), while the “one” side uses mappedBy to point back at it.
  • A plain JPQL JOIN filters on a related entity’s fields; use JOIN FETCH instead when you actually need to eagerly load the association to avoid N+1 queries.