Spring Data JPA
Spring Data JPA eliminates most of the boilerplate around relational persistence. You define entities and repository interfaces, and Spring generates the implementation at runtime, backed by Hibernate as the default JPA provider.
Defining an entity
An @Entity maps a Java class to a database table. JPA needs an identifier and a no-args constructor.
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private BigDecimal price;
protected Product() { } // required by JPA
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
// getters and setters
}
Note:
GenerationType.IDENTITYdelegates ID generation to the database’s auto-increment column, the right choice for most MySQL and PostgreSQL schemas.
The repository interface
Extend JpaRepository<T, ID> and you immediately get CRUD methods, pagination, and sorting, no implementation required.
public interface ProductRepository extends JpaRepository<Product, Long> {
}
Out of the box you can call save, findById, findAll, deleteById, count, and findAll(Pageable).
Derived query methods
Spring parses method names and builds the query for you. The naming convention is findBy + property + optional keyword.
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByName(String name);
List<Product> findByPriceLessThan(BigDecimal max);
List<Product> findByNameContainingIgnoreCase(String fragment);
Optional<Product> findFirstByOrderByPriceDesc();
}
| Keyword | Generated condition |
|---|---|
findByNameAndPrice | name = ? AND price = ? |
findByPriceBetween | price BETWEEN ? AND ? |
findByNameStartingWith | name LIKE ?% |
findByActiveTrue | active = true |
Custom queries with @Query
When method names get unwieldy, write JPQL (or native SQL) explicitly.
@Query("SELECT p FROM Product p WHERE p.price > :floor ORDER BY p.price")
List<Product> pricierThan(@Param("floor") BigDecimal floor);
@Query(value = "SELECT * FROM products WHERE price > ?1", nativeQuery = true)
List<Product> pricierThanNative(BigDecimal floor);
Relationships overview
JPA models associations with mapping annotations. Always specify the fetch strategy deliberately.
| Annotation | Relationship | Default fetch |
|---|---|---|
@OneToMany | Parent → children | LAZY |
@ManyToOne | Child → parent | EAGER |
@OneToOne | One-to-one | EAGER |
@ManyToMany | Many-to-many | LAZY |
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<LineItem> items = new ArrayList<>();
}
Warning: Default
@ManyToOneand@OneToOnefetching is EAGER, which can trigger surprise joins and the N+1 query problem. Setfetch = FetchType.LAZYand load what you need explicitly.
A working example with H2
H2 is an in-memory database, ideal for demos and tests. Add the dependency and configure it.
spring:
datasource:
url: jdbc:h2:mem:shopdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true # browse at /h2-console
Seed and query the repository at startup with a CommandLineRunner.
@Bean
CommandLineRunner seed(ProductRepository repo) {
return args -> {
repo.save(new Product("Keyboard", new BigDecimal("79.99")));
repo.save(new Product("Mouse", new BigDecimal("39.50")));
repo.findByPriceLessThan(new BigDecimal("50"))
.forEach(p -> System.out.println("Cheap: " + p.getName()));
};
}
Output:
Hibernate: insert into products (name, price) values (?, ?)
Hibernate: insert into products (name, price) values (?, ?)
Hibernate: select ... from products p where p.price < ?
Cheap: Mouse
Best Practices
- Set fetch types explicitly; prefer LAZY for associations and fetch eagerly only when needed via
JOIN FETCH. - Use
ddl-auto: validate(or Flyway/Liquibase migrations) in production; neverupdateorcreateagainst real data. - Keep transactional logic in
@Servicemethods annotated with@Transactional, not in repositories. - Return
Optionalfrom single-result finders to make absence explicit. - Map entities to DTOs before returning them from controllers to avoid lazy-loading serialization issues.
- Watch the generated SQL with
show-sqlduring development and eliminate N+1 queries.