Spring BootEntity Mapping
Spring Boot

Entity Mapping

JPA entity mapping defines how Java classes and their fields translate to database tables and columns. Spring Boot auto-configures Hibernate as the JPA provider. This entry covers @Entity setup, column mapping, primary key strategies, embedded objects, enum mapping, collection mapping, and inheritance strategies.

Basic Entity Setup

An entity class is annotated with @Entity and must have a no-arg constructor (Lombok's @NoArgsConstructor covers this). @Table customises the table name, schema, and unique constraints. Every persistent field maps to a column by default — only fields that need non-default behaviour require @Column.
Java
@Entity
@Table(
    name = "users",
    schema = "app",
    uniqueConstraints = {
        @UniqueConstraint(name = "uq_users_email",
                          columnNames = "email"),
        @UniqueConstraint(name = "uq_users_username",
                          columnNames = "username")
    },
    indexes = {
        @Index(name = "idx_users_email",      columnList = "email"),
        @Index(name = "idx_users_created_at", columnList = "created_at")
    }
)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "full_name",
            nullable = false,
            length = 100)
    private String name;

    @Column(nullable = false, unique = true, length = 255)
    private String email;

    @Column(nullable = false, length = 50)
    private String username;

    @Column(name = "is_active",
            nullable = false,
            columnDefinition = "boolean default true")
    private boolean active = true;

    @Column(name = "created_at",
            nullable = false,
            updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

Primary Key Strategies

JPA offers four @GeneratedValue strategies. IDENTITY delegates to a database auto-increment column — the most common choice for MySQL and PostgreSQL. SEQUENCE uses a database sequence — preferred for PostgreSQL when batch inserts are needed. TABLE uses a dedicated key table — portable but slow. AUTO lets Hibernate choose. UUID primary keys avoid sequential ID exposure.
Java
// ── IDENTITY — auto-increment column (MySQL, PostgreSQL SERIAL) ───────
@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

// ── SEQUENCE — database sequence (preferred for PostgreSQL) ───────────
@Entity
@SequenceGenerator(
    name            = "product_seq",
    sequenceName    = "product_id_seq",
    allocationSize  = 50    // fetch 50 IDs per round-trip — improves batch perf
)
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "product_seq")
    private Long id;
}

// ── UUID — application-generated, non-sequential ──────────────────────
@Entity
public class Event {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)   // Hibernate 6+ / JPA 3.1
    @Column(updatable = false, nullable = false,
            columnDefinition = "uuid")
    private UUID id;
}

// ── Assigned — application sets the ID manually ───────────────────────
@Entity
public class Country {

    @Id
    @Column(length = 2)
    private String isoCode;   // "US", "GB", "DE" — set by caller
}

// ── Composite key via @IdClass ─────────────────────────────────────────
@Data
public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
}

@Entity
@IdClass(OrderItemId.class)
public class OrderItem {

    @Id private Long orderId;
    @Id private Long productId;

    @Column(nullable = false)
    private int quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;
}

Column Mapping

@Column attributes control the DDL generated by Hibernate and enforce constraints at the database level. Precision and scale are required for BigDecimal to avoid platform-specific defaults. columnDefinition is an escape hatch for database-specific types such as jsonb, text[], or point.
Java
@Entity
@Table(name = "products")
@Getter @Setter @NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // ── String columns ─────────────────────────────────────────────────
    @Column(nullable = false, length = 200)
    private String name;

    @Column(columnDefinition = "TEXT")          // unbounded text
    private String description;

    @Column(length = 20, unique = true)
    private String sku;

    // ── Numeric columns ────────────────────────────────────────────────
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false, precision = 5, scale = 2)
    private BigDecimal vatRate;

    @Column(nullable = false)
    private int stockQuantity;

    // ── Boolean ────────────────────────────────────────────────────────
    @Column(nullable = false,
            columnDefinition = "boolean default true")
    private boolean published = true;

    // ── Date / time ────────────────────────────────────────────────────
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "published_at")
    private LocalDateTime publishedAt;

    @Column(name = "expiry_date")
    private LocalDate expiryDate;

    // ── Database-specific types ────────────────────────────────────────
    @Column(columnDefinition = "jsonb")         // PostgreSQL JSONB
    private String attributes;

    @Column(columnDefinition = "uuid")          // PostgreSQL UUID
    private UUID externalRef;

    @Column(columnDefinition = "text[]")        // PostgreSQL text array
    private String[] tags;

    // ── Read-only / computed column ────────────────────────────────────
    @Column(insertable = false, updatable = false)
    private LocalDateTime deletedAt;
}

Enum Mapping

Map enums with @Enumerated. ORDINAL stores the ordinal integer — fragile because inserting a new enum value at any position corrupts existing data. STRING stores the name — safe but verbose. For a custom database value (a short code, a legacy integer), use @Converter with AttributeConverter.
Java
// ── Enum definition ────────────────────────────────────────────────────
public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

// ── ORDINAL — avoid: inserting new values breaks existing data ─────────
@Enumerated(EnumType.ORDINAL)   // stores 0, 1, 2, 3, 4
private OrderStatus status;

// ── STRING — preferred: stores "PENDING", "CONFIRMED", etc. ───────────
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private OrderStatus status;

// ── Custom converter — store a short code ─────────────────────────────
public enum Priority {
    LOW("L"), MEDIUM("M"), HIGH("H"), CRITICAL("C");

    private final String code;
    Priority(String code) { this.code = code; }
    public String getCode() { return code; }

    public static Priority fromCode(String code) {
        return Arrays.stream(values())
            .filter(p -> p.code.equals(code))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException(
                "Unknown priority code: " + code));
    }
}

@Converter(autoApply = true)    // applies to all Priority fields globally
public class PriorityConverter
        implements AttributeConverter<Priority, String> {

    @Override
    public String convertToDatabaseColumn(Priority priority) {
        return priority == null ? null : priority.getCode();
    }

    @Override
    public Priority convertToEntityAttribute(String code) {
        return code == null ? null : Priority.fromCode(code);
    }
}

// ── Entity using the converter ────────────────────────────────────────
@Entity
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private OrderStatus status;     // STRING via @Enumerated

    @Column(nullable = false, length = 1)
    private Priority priority;      // "L"/"M"/"H"/"C" via converter
}

Embedded Objects

@Embeddable marks a class whose fields are stored in the owning entity's table — no join, no extra table. @Embedded places the embeddable inside an entity. @AttributeOverride renames the columns when the same embeddable is used more than once in one entity.
Java
// ── Embeddable ─────────────────────────────────────────────────────────
@Embeddable
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class Address {

    @Column(name = "street",       nullable = false, length = 255)
    private String street;

    @Column(name = "city",         nullable = false, length = 100)
    private String city;

    @Column(name = "state",        length = 100)
    private String state;

    @Column(name = "postal_code",  nullable = false, length = 20)
    private String postalCode;

    @Column(name = "country_code", nullable = false, length = 2)
    private String countryCode;
}

// ── Entity using the embeddable once ──────────────────────────────────
@Entity
@Table(name = "customers")
@Getter @Setter @NoArgsConstructor
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Embedded                          // columns: street, city, state, ...
    private Address address;
}

// ── Entity using the same embeddable twice — needs @AttributeOverride
@Entity
@Table(name = "orders")
@Getter @Setter @NoArgsConstructor
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street",      column = @Column(name = "billing_street")),
        @AttributeOverride(name = "city",        column = @Column(name = "billing_city")),
        @AttributeOverride(name = "postalCode",  column = @Column(name = "billing_postal_code")),
        @AttributeOverride(name = "countryCode", column = @Column(name = "billing_country_code"))
    })
    private Address billingAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street",      column = @Column(name = "shipping_street")),
        @AttributeOverride(name = "city",        column = @Column(name = "shipping_city")),
        @AttributeOverride(name = "postalCode",  column = @Column(name = "shipping_postal_code")),
        @AttributeOverride(name = "countryCode", column = @Column(name = "shipping_country_code"))
    })
    private Address shippingAddress;
}

Collection Mapping

@ElementCollection stores a collection of basic types or embeddables in a separate table without a dedicated entity class. It is appropriate for simple value collections (tags, phone numbers, addresses). For collections of full entities, use @OneToMany instead.
Java
@Entity
@Table(name = "articles")
@Getter @Setter @NoArgsConstructor
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 300)
    private String title;

    // ── Collection of strings ─────────────────────────────────────────
    @ElementCollection
    @CollectionTable(
        name = "article_tags",
        joinColumns = @JoinColumn(name = "article_id")
    )
    @Column(name = "tag", length = 50)
    private Set<String> tags = new HashSet<>();

    // ── Collection of embeddables ─────────────────────────────────────
    @ElementCollection
    @CollectionTable(
        name = "article_authors",
        joinColumns = @JoinColumn(name = "article_id")
    )
    private List<Author> authors = new ArrayList<>();

    // ── Map: locale → translated title ───────────────────────────────
    @ElementCollection
    @CollectionTable(
        name = "article_translations",
        joinColumns = @JoinColumn(name = "article_id")
    )
    @MapKeyColumn(name = "locale")
    @Column(name = "translated_title")
    private Map<String, String> translations = new HashMap<>();
}

@Embeddable
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class Author {
    @Column(name = "author_name",  nullable = false, length = 100)
    private String name;
    @Column(name = "author_email", length = 255)
    private String email;
}

Inheritance Mapping

JPA supports three inheritance strategies. SINGLE_TABLE stores all subclasses in one table with a discriminator column — fastest queries, but nullable columns for subtype-specific fields. JOINED stores each subclass in its own table joined to the parent — clean schema, extra joins. TABLE_PER_CLASS gives each subclass its own complete table — no joins, but no polymorphic queries across subtypes.
Java
// ── SINGLE_TABLE — one table, discriminator column ────────────────────
@Entity
@Table(name = "payments")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type",
                     discriminatorType = DiscriminatorType.STRING,
                     length = 20)
@Getter @Setter @NoArgsConstructor
public abstract class Payment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal amount;

    @Column(nullable = false)
    private LocalDateTime createdAt;
}

@Entity
@DiscriminatorValue("CARD")
@Getter @Setter @NoArgsConstructor
public class CardPayment extends Payment {
    @Column(name = "card_last4", length = 4)
    private String cardLast4;
    @Column(name = "card_brand", length = 20)
    private String cardBrand;
}

@Entity
@DiscriminatorValue("BANK")
@Getter @Setter @NoArgsConstructor
public class BankPayment extends Payment {
    @Column(name = "account_number", length = 34)
    private String accountNumber;
    @Column(name = "bank_code",      length = 11)
    private String bankCode;
}

// ── JOINED — separate table per subclass, joined to parent ────────────
@Entity
@Table(name = "notifications")
@Inheritance(strategy = InheritanceType.JOINED)
@Getter @Setter @NoArgsConstructor
public abstract class Notification {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false) private String recipient;
    @Column(nullable = false) private LocalDateTime sentAt;
}

@Entity
@Table(name = "email_notifications")
@PrimaryKeyJoinColumn(name = "notification_id")
@Getter @Setter @NoArgsConstructor
public class EmailNotification extends Notification {
    @Column(nullable = false, length = 300) private String subject;
    @Column(columnDefinition = "TEXT")      private String htmlBody;
}

@Entity
@Table(name = "sms_notifications")
@PrimaryKeyJoinColumn(name = "notification_id")
@Getter @Setter @NoArgsConstructor
public class SmsNotification extends Notification {
    @Column(nullable = false, length = 160) private String message;
    @Column(nullable = false, length = 20)  private String phoneNumber;
}

Auditing with @CreatedDate and @LastModifiedDate

Spring Data JPA's auditing support populates created and updated timestamps automatically. Enable it with @EnableJpaAuditing, extend a base entity or use @EntityListeners directly, and annotate the timestamp fields. This eliminates manual timestamp management across every entity.
Java
// ── Enable auditing on the application class or a @Configuration ──────
@SpringBootApplication
@EnableJpaAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// ── Auditable base entity — extend to share across all entities ────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class AuditableEntity {

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(name = "created_by", updatable = false, length = 100)
    private String createdBy;

    @LastModifiedBy
    @Column(name = "updated_by", length = 100)
    private String updatedBy;
}

// ── Provide current user for @CreatedBy / @LastModifiedBy ─────────────
@Component
public class SpringSecurityAuditorAware
        implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(
            SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
}

// ── Entity extending the base ─────────────────────────────────────────
@Entity
@Table(name = "products")
@Getter @Setter @NoArgsConstructor
public class Product extends AuditableEntity {

    @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;
}
// Hibernate automatically sets createdAt and updatedAt on persist/merge.
// No manual LocalDateTime.now() calls needed anywhere.