Advanced — Cheat Sheet
Hibernate & JPA · 7 topics. Download the PDF or the Instagram carousel and share it.
@Auditing with Spring Data
Enable @EnableJpaAuditing and annotate fields with @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy; an AuditorAware bean supplies the current user identity.
- ✓@EnableJpaAuditing must reference the AuditorAware bean by name via auditorAwareRef when multiple beans exist.
- ✓@EntityListeners(AuditingEntityListener.class) must be on the entity or its MappedSuperclass for fields to populate.
- ✓AuditorAware returns Optional.empty() for unauthenticated requests — @CreatedBy field will be left null.
- ✓@Column(updatable=false) on @CreatedDate and @CreatedBy prevents accidental overwrite on updates.
- ✓Hibernate Envers is the right tool for full change history; Spring Auditing only captures latest-modifier metadata.
- ✓For batch jobs or scheduled tasks without a SecurityContext, return "system" from AuditorAware as a fallback.
// Enable auditing in any @Configuration class
@Configuration
@EnableJpaAuditing(auditorAwareRef = "currentUserAuditor")
public class JpaConfig {}
// Shared audit base entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class AuditableEntity {
@CreatedDate
@Column(updatable = false, nullable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(length = 100)
private String updatedBy;
}
// Domain entity inherits audit fields
@Entity
@Table(name = "orders")
public class Order extends AuditableEntity {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String status;
private BigDecimal amount;
// createdAt, updatedAt, createdBy, updatedBy inherited
}Hibernate Validator (Bean Validation)
Hibernate Validator is the reference implementation of Jakarta Bean Validation; constraints like @NotNull, @Size, @Email, and @Pattern are enforced on persist and merge.
- ✓Hibernate Validator enforces constraints automatically before JPA persist/merge operations
- ✓@NotNull checks for null; @NotBlank also rejects blank strings — prefer @NotBlank for String
- ✓@Valid in Spring MVC triggers validation before the controller method; @Validated supports groups
- ✓MethodArgumentNotValidException carries all field errors — handle it to return structured JSON
- ✓Custom constraints: define annotation with @Constraint + implement ConstraintValidator<A,T>
- ✓@Valid cascades into @Embedded and nested @Valid-annotated objects
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
@Email(message = "Invalid email format")
@NotNull
@Column(unique = true)
private String email;
@Pattern(regexp = "^\\+?[0-9]{10,15}$", message = "Invalid phone number")
private String phone;
@Min(value = 0, message = "Age cannot be negative")
@Max(value = 150)
private int age;
@Valid // cascade validation into nested object
@Embedded
private Address address;
}Schema Generation (hbm2ddl)
spring.jpa.hibernate.ddl-auto controls schema generation: validate (production), update (development), create-drop (tests); use Flyway or Liquibase for production migrations.
- ✓Never use ddl-auto=update or create-drop in production — use validate or none only.
- ✓Flyway (or Liquibase) version-controls schema changes, enables rollbacks, and prevents double-application of scripts.
- ✓Spring Boot auto-configures Flyway; add flyway-core to pom.xml and put scripts in db/migration.
- ✓Naming convention: V{version}__{description}.sql — double underscore, no spaces.
- ✓Use baseline-on-migrate=true when introducing Flyway to an existing database.
- ✓Testcontainers + Flyway in tests gives you a real DB migration smoke test on every build.
# application.properties # PRODUCTION: validate entity mappings against existing schema, fail fast if mismatch spring.jpa.hibernate.ddl-auto=validate # DEVELOPMENT: let Hibernate update schema (adds columns, creates tables) # WARNING: never on production — update can drop NOT NULL or rename incorrectly spring.jpa.hibernate.ddl-auto=update # TESTING: drop and recreate schema before each test run spring.jpa.hibernate.ddl-auto=create-drop # CI/TESTING ALTERNATIVE: create schema fresh (no drop on close) spring.jpa.hibernate.ddl-auto=create # NONE: disable Hibernate DDL entirely (use with Flyway/Liquibase) spring.jpa.hibernate.ddl-auto=none # Show generated DDL (useful during development) spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Export Hibernate-generated DDL to file without executing spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=schema.sql
Custom AttributeConverter
AttributeConverter<X,Y> maps between a Java type and a DB column type; use for enums, JSON objects, encrypted values, or custom domain types not natively supported by JPA.
- ✓AttributeConverter<X,Y>: X = Java type, Y = DB column type (usually String or primitives)
- ✓autoApply=true applies the converter globally to all fields of that type — no @Convert needed
- ✓Converters run on every read/write — keep them stateless and fast (avoid heavy I/O)
- ✓Storing enums by code (not name) decouples code from DB; rename the enum freely without migration
- ✓@Convert(converter=…) on a field overrides or disables autoApply for that specific field
- ✓Spring-managed @Component converters can use @Autowired to inject services
public enum OrderStatus {
PENDING("P"), PLACED("PL"), SHIPPED("SH"), DELIVERED("DE"), CANCELLED("CA");
private final String code;
OrderStatus(String code) { this.code = code; }
public String getCode() { return code; }
public static OrderStatus fromCode(String code) {
return Arrays.stream(values())
.filter(s -> s.code.equals(code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown code: " + code));
}
}
@Converter(autoApply = true) // auto-applies to all OrderStatus fields in all entities
public class OrderStatusConverter implements AttributeConverter<OrderStatus, String> {
@Override
public String convertToDatabaseColumn(OrderStatus status) {
return status == null ? null : status.getCode();
}
@Override
public OrderStatus convertToEntityAttribute(String code) {
return code == null ? null : OrderStatus.fromCode(code);
}
}Multi-Tenancy in Hibernate
Hibernate supports SCHEMA, DATABASE, and DISCRIMINATOR multi-tenancy strategies; the CurrentTenantIdentifierResolver and ConnectionProvider resolve the tenant per request.
- ✓SCHEMA: shared DB, separate schema per tenant — good isolation, moderate ops complexity.
- ✓DATABASE: separate DB instance per tenant — highest isolation, highest resource cost.
- ✓DISCRIMINATOR: single schema + tenant_id column — simplest ops, weakest isolation.
- ✓CurrentTenantIdentifierResolver resolves the tenant ID per request (e.g. from a header or JWT claim).
- ✓AbstractRoutingDataSource routes JDBC connections to the correct tenant data source.
- ✓@TenantId (Hibernate 6.1+) marks the discriminator field; Hibernate appends the filter automatically.
// TenantContext — store/clear per request
public class TenantContext {
private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();
public static void set(String tenantId) { CURRENT.set(tenantId); }
public static String get() { return CURRENT.get(); }
public static void clear() { CURRENT.remove(); }
}
// Servlet filter (or Spring Security filter) sets the tenant
@Component
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
String tenant = ((HttpServletRequest) req).getHeader("X-Tenant-Id");
TenantContext.set(tenant);
try { chain.doFilter(req, res); }
finally { TenantContext.clear(); }
}
}
// CurrentTenantIdentifierResolver
@Component
public class TenantIdentifierResolver
implements CurrentTenantIdentifierResolver<String> {
@Override
public String resolveCurrentTenantIdentifier() {
String tenant = TenantContext.get();
return (tenant != null) ? tenant : "public"; // fallback to default schema
}
@Override
public boolean validateExistingCurrentSessions() { return true; }
}Hibernate Envers (Entity Auditing)
@Audited on an entity instructs Envers to record every change in a revision table; query historical data with AuditReader to retrieve entity state at a specific revision.
- ✓@Audited creates a mirror _aud table and REVINFO table automatically
- ✓@NotAudited on a field excludes it from the audit trail (e.g. large binary fields)
- ✓AuditReader.find(Class, id, revision) retrieves entity state at a specific point in time
- ✓RevisionType: ADD (insert), MOD (update), DEL (delete) — stored in each _aud row
- ✓Spring Data Envers RevisionRepository provides findRevisions() and findLastChangeRevision()
- ✓Envers adds write overhead — every persisted change requires an additional INSERT into the _aud table
<!-- pom.xml — Envers is included in spring-boot-starter-data-jpa -->
<!-- For RevisionRepository, add spring-data-envers separately -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>
@Entity
@Audited // audit all fields
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
@NotAudited // exclude this field from auditing
private byte[] imageData;
}
// Envers generates:
// products_aud (id, rev, revtype, name, price)
// revinfo (rev SERIAL PK, revtstmp BIGINT)Hibernate Statistics & Logging
Enable hibernate.generate_statistics and show_sql to see query counts, cache hit ratios, and connection acquisition time — essential for identifying performance bottlenecks.
- ✓show-sql: true prints SQL to stdout — for production use logging.level.org.hibernate.SQL: DEBUG instead
- ✓hibernate.generate_statistics=true enables the Statistics API and Micrometer metric collection
- ✓getQueryExecutionCount() > expected per request is a reliable N+1 detector in integration tests and dev interceptors
- ✓getCollectionFetchCount() tracks lazy collection initialisations — high counts indicate missing JOIN FETCH or batch fetch
- ✓Spring Boot 3 auto-configures Micrometer Hibernate metrics when generate_statistics=true is set
- ✓Cache hit rate (L2 hits / (hits + misses)) < 80% for entities that should be cached indicates cache misconfiguration
# application.yml — development SQL visibility
spring:
jpa:
show-sql: true # print SQL to stdout (dev only)
properties:
hibernate:
format_sql: true # pretty-print SQL
use_sql_comments: true # add JPQL context as SQL comments
generate_statistics: true # enable statistics collection
session.events.log.LOG_QUERIES_SLOWER_THAN_MS: 50 # log slow queries
# Production-safe: route through SLF4J (respects log level config)
logging:
level:
org.hibernate.SQL: DEBUG # SQL statements
org.hibernate.orm.jdbc.bind: TRACE # bind parameter values
org.hibernate.stat: DEBUG # statistics logging
# Example console output with show-sql + format_sql:
# /* select generatedAlias0 from Order as generatedAlias0 */
# select
# o1_0.id, o1_0.customer_id, o1_0.total
# from
# orders o1_0
# where
# o1_0.customer_id=?