Spring Data JPA

Entities & @Entity Basics

18 min Lesson 2 of 13

Entities & @Entity Basics

In Spring Data JPA, a persistent entity is a plain Java class that Hibernate maps to a relational database table. The mapping is declared entirely through annotations — no XML, no code generation step. This lesson walks through everything you need to create a correct, production-grade entity: the required annotations, the column-naming rules, identity strategies, and the performance trade-offs you must understand before you write your first save() call.

The Minimum Viable Entity

Two annotations are mandatory for every entity: @Entity marks the class as a JPA-managed type, and @Id designates the field that maps to the primary key column. Spring Boot 3 uses the jakarta.persistence package (Jakarta EE 9+), not the old javax.persistence one.

package com.example.store.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; @Entity public class Product { @Id private Long id; private String name; private double price; // JPA requires a public or protected no-argument constructor protected Product() {} public Product(Long id, String name, double price) { this.id = id; this.name = name; this.price = price; } // getters and setters ... }
Why the no-arg constructor? Hibernate instantiates entities reflectively when loading rows from the database. It calls the no-arg constructor first, then sets each field. Without one the JPA provider will throw an exception at startup. Mark it protected (not public) to discourage application code from calling it directly.

Table and Column Name Resolution

By default, Hibernate 6 derives the table name from the class name and the column name from each field name, using its configured ImplicitNamingStrategy. Spring Boot sets the strategy to SpringImplicitNamingStrategy, which converts camelCase to snake_case: a field named unitPrice becomes the column unit_price.

Use @Table and @Column when you need to override the defaults or constrain the schema:

import jakarta.persistence.*; @Entity @Table(name = "products", uniqueConstraints = @UniqueConstraint(columnNames = "sku")) public class Product { @Id private Long id; @Column(name = "product_name", nullable = false, length = 200) private String name; @Column(name = "unit_price", nullable = false) private double price; @Column(unique = true, length = 50) private String sku; }
Always set nullable = false on non-optional columns. Hibernate can generate the DDL (spring.jpa.hibernate.ddl-auto=validate in CI) and it will flag missing NOT NULL constraints. This catches schema drift early rather than at runtime.

Primary Key Generation Strategies

Choosing the right @GeneratedValue strategy affects insert throughput, portability, and how you batch inserts. JPA defines four strategies through the GenerationType enum:

  • AUTO (default): Hibernate picks a strategy based on the database dialect. On PostgreSQL it uses a sequence; on MySQL it falls back to a table generator. Avoid AUTO in production — its behaviour can change when you switch databases or upgrade Hibernate.
  • IDENTITY: Delegates to the database's auto-increment column (SERIAL / AUTO_INCREMENT). Simple, but breaks JDBC batch inserts because Hibernate must flush each row individually to retrieve its generated ID before it can cascade to child entities.
  • SEQUENCE (recommended for PostgreSQL / Oracle): Allocates a block of IDs from a database sequence in a single round-trip (allocationSize controls the block size, default 50). Hibernate assigns IDs in memory and batches INSERTs efficiently.
  • TABLE: A portable but slow fallback that uses a dedicated lock table. Avoid unless your database truly lacks sequences.
// Preferred for PostgreSQL: sequence with a 50-ID allocation block @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq") @SequenceGenerator(name = "product_seq", sequenceName = "product_id_seq", allocationSize = 50) private Long id;
// Convenient for MySQL / MariaDB, but disables batch inserts @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
IDENTITY and batching do not mix. If you need to insert thousands of rows at once (e.g. bulk data imports), use SEQUENCE or assign IDs manually with a UUID so Hibernate can batch. Enabling spring.jpa.properties.hibernate.jdbc.batch_size=50 in application.properties has no effect while IDENTITY is in use.

UUIDs as Primary Keys

For distributed systems or APIs where you want to expose entity IDs without leaking sequential information, a UUID primary key is a common pattern. Hibernate 6 natively supports java.util.UUID:

import java.util.UUID; import jakarta.persistence.*; import org.hibernate.annotations.UuidGenerator; @Entity @Table(name = "orders") public class Order { @Id @UuidGenerator // Hibernate 6 native — generates a time-based v7 UUID private UUID id; @Column(nullable = false) private String status; protected Order() {} }

@UuidGenerator (Hibernate-specific) is preferred over @GeneratedValue(strategy = GenerationType.UUID) (JPA 3.1 standard) because it lets you control the UUID version. Both avoid a database round-trip, so bulk inserts batch correctly.

Basic Field Type Mappings

Hibernate maps the standard Java types to SQL types automatically. Key mappings to know:

  • StringVARCHAR(255) by default; override with @Column(length = N) or @Lob for large text.
  • int / Integer, long / LongINTEGER / BIGINT.
  • boolean / BooleanBOOLEAN (or TINYINT(1) on MySQL).
  • java.time.LocalDate, LocalDateTime, Instant → native date/time columns. No @Temporal needed in Hibernate 6.
  • BigDecimalDECIMAL; always use for monetary values — never double or float in financial columns.
  • Enums: annotate with @Enumerated(EnumType.STRING) to store the name ("ACTIVE") rather than the ordinal (0), which breaks if the enum order changes.
import jakarta.persistence.*; import java.math.BigDecimal; import java.time.Instant; @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 200) private String name; @Column(nullable = false, precision = 10, scale = 2) private BigDecimal price; // never double for money @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private ProductStatus status; @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; protected Product() {} }

equals() and hashCode() for Entities

Hibernate places entities in Set collections and compares them during dirty checking. The default Object.equals() (identity comparison) can produce subtle bugs when the same database row is loaded in two separate EntityManager sessions. The recommended approach is to base equality on the natural business key (e.g. sku) rather than the surrogate ID, because the surrogate ID is null before the first save().

@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Product other)) return false; return sku != null && sku.equals(other.sku); } @Override public int hashCode() { // stable, never changes even before persist return getClass().hashCode(); }

Summary

An entity is a plain Java class annotated with @Entity and a single @Id field. Use @Table and @Column to document and constrain the schema. Pick a generation strategy deliberately: SEQUENCE for throughput, IDENTITY for simplicity, UUID for distributed systems. Use BigDecimal for money, @Enumerated(EnumType.STRING) for enums, and the modern java.time types for dates. Define equals() on a business key, not the surrogate ID. With these foundations in place you are ready to explore the repository abstraction in the next lesson.