8 Hidden Hibernate Annotations That Supercharge Spring Boot 3

This guide explores eight lesser‑known Hibernate annotations—such as @EntityGraph, @Formula, @SQLRestriction, @SQLDelete, @JoinFormula, @Cache, @DynamicUpdate/Insert, and @Filter/FilterDef—demonstrating how they improve performance, simplify queries, and enhance data handling in Spring Boot 3 applications for developers.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
8 Hidden Hibernate Annotations That Supercharge Spring Boot 3

Introduction

Spring Data JPA greatly simplifies database access by wrapping the JPA specification, automatically generating CRUD methods from repository interfaces and reducing boilerplate code. It supports lazy loading, caching, transaction management, type‑safe queries, pagination, and complex association handling. This article introduces eight lesser‑known but powerful Hibernate annotations that address performance tuning, query optimization, and development efficiency.

Practical Cases

2.1 @EntityGraph

The @EntityGraph annotation defines the loading strategy for entity associations, allowing eager fetching of specified attributes to solve the N+1 query problem.

@Entity
@NamedEntityGraph(
    name = "Order.EG",
    attributeNodes = {
        @NamedAttributeNode("customer"),
        @NamedAttributeNode("items")
    }
)
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<OrderItem> items = new ArrayList<>();
}

public interface OrderRepository extends JpaRepository<Order, Long> {
    @EntityGraph(value = "Order.EG", type = EntityGraphType.LOAD)
    List<Order> findAll();
}

Calling findAll() loads the configured associations in a single SQL statement, eliminating N+1 queries.

SQL output for @EntityGraph
SQL output for @EntityGraph

2.2 @Formula

@Formula enables dynamic calculation of a property using a SQL expression, without persisting a dedicated column. It is read‑only and can compute values such as discounts or aggregated data.

@Entity
@Table(name = "t_book")
public class Book {
    // ... other fields ...
    @Formula("price * 0.9")
    private BigDecimal discountedPrice;

    @Formula("(select avg(r.rating) from review r where r.book_id = id)")
    private Double averageRating;
}

When querying Book, the generated SQL includes the computed columns.

SQL output for @Formula
SQL output for @Formula

2.3 @SQLRestriction

@SQLRestriction injects a native SQL condition into the SQL generated for an entity or collection.

@Entity
@Table(name = "o_user")
@SQLRestriction("age > 10")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private Integer age;
    // ... other fields ...
}

Queries on User now include the age > 10 clause.

SQL output for @SQLRestriction
SQL output for @SQLRestriction

2.4 @SQLDelete

@SQLDelete replaces the default DELETE statement with a custom DML, often used for soft‑delete implementations.

@Entity
@Table(name = "t_person")
@SQLDelete(sql = "update t_person p set p.deleted = 1 where p.id = ?")
public class Person {
    // ... other fields ...
    @Column(columnDefinition = "int default 0")
    private Integer deleted = 0; // 0: normal, 1: deleted
}

public interface PersonRepository extends JpaRepository<Person, Long> {}

Executing a delete on Person runs the custom UPDATE statement instead of a physical DELETE.

SQL output for @SQLDelete
SQL output for @SQLDelete

2.5 @JoinFormula

@JoinFormula allows a custom join condition written in native SQL when the default association mapping is insufficient.

@Entity
@Table(name = "t_document")
public class Document {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinFormula("(SELECT v.id FROM t_document_version v WHERE v.document_id = id ORDER BY v.version_number DESC LIMIT 1)")
    private DocumentVersion latestVersion;

    private Integer versionCount;
}

@Entity
@Table(name = "t_document_version")
public class DocumentVersion {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long documentId;
    private Integer versionNumber;
    private String content;
}

Querying Document generates SQL that joins the latest version via the sub‑query.

SQL output for @JoinFormula
SQL output for @JoinFormula

2.6 @Cache and @CacheableQuery

These annotations provide fine‑grained second‑level cache control. Example below caches the Country entity and its collection of State objects.

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Country {
    @Id
    private String code;
    private String name;

    @OneToMany(mappedBy = "country")
    @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
    private Set<State> states;
}

Using these annotations can significantly improve application performance by reducing database hits.

2.7 @DynamicUpdate & @DynamicInsert

@DynamicUpdate generates UPDATE statements that include only the fields that have changed; @DynamicInsert generates INSERT statements that include only non‑null fields, reducing unnecessary column writes.

@Entity
@Table(name = "t_book")
@DynamicInsert
@DynamicUpdate
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String isbn;
    private String description;
    private Integer page;
    private BigDecimal price;
    private String rating;
}

@Resource
private BookRepository bookRepository;

@Test
public void testSave() {
    Book book = new Book();
    book.setTitle("Spring Boot3实战案例200讲");
    book.setPrice(BigDecimal.valueOf(70));
    this.bookRepository.save(book);
}

The generated SQL contains only the title and price columns, demonstrating the performance benefit.

SQL output for @DynamicInsert/@DynamicUpdate
SQL output for @DynamicInsert/@DynamicUpdate

2.8 @Filter & @FilterDef

@FilterDef defines a reusable filter template with parameters; @Filter applies the template to an entity, enabling dynamic SQL conditions such as soft‑delete or multi‑tenant filtering.

@FilterDef(
    name = "priceRangeFilter",
    parameters = {
        @ParamDef(name = "minPrice", type = BigDecimal.class),
        @ParamDef(name = "maxPrice", type = BigDecimal.class)
    }
)
@Filter(name = "priceRangeFilter", condition = "price >= :minPrice AND price <= :maxPrice")
public class Book {}

private final EntityManager entityManager;

@Transactional(readOnly = true)
public List<Book> findBooksByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
    Session session = entityManager.unwrap(Session.class);
    session.enableFilter("priceRangeFilter")
           .setParameter("minPrice", minPrice)
           .setParameter("maxPrice", maxPrice);
    return bookRepository.findAll();
}

The filter must be enabled within a transaction and the same Hibernate Session for it to take effect.

SQL output for @Filter
SQL output for @Filter

All the annotations demonstrated above are provided by Hibernate.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentSpring BootAnnotationsHibernatejpa
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.