Comprehensive Guide to JPA Composite Primary Keys and Data Versioning

Nov 26, 2025 · Programming · 14 views · 7.8

Keywords: JPA | Composite Primary Key | Data Versioning

Abstract: This technical paper provides an in-depth exploration of implementing composite primary keys in JPA using both @EmbeddedId and @IdClass annotations. Through detailed code examples, it demonstrates how to create versioned data entities and implement data duplication functionality. The article covers entity design, Spring Boot configuration, and practical data operations, offering developers a complete reference for composite key implementation in enterprise applications.

Fundamental Concepts of Composite Primary Keys

In relational database design, composite primary keys consist of multiple columns that collectively form a unique identifier. When a single column cannot guarantee data uniqueness, composite primary keys provide an effective solution. Within the JPA specification, composite primary key implementation primarily relies on two core annotations: @EmbeddedId and @IdClass.

@EmbeddedId Implementation Approach

The @EmbeddedId annotation implements composite primary keys through embedded classes, offering better encapsulation and code organization. First, create an embeddable class implementing the Serializable interface:

@Embeddable
public class DataVersionKey implements Serializable {
    @Column(name = "Id", nullable = false)
    private Long id;
    
    @Column(name = "Version", nullable = false)
    private Integer version;
    
    public DataVersionKey() {}
    
    public DataVersionKey(Long id, Integer version) {
        this.id = id;
        this.version = version;
    }
    
    // Must implement equals and hashCode methods
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DataVersionKey that = (DataVersionKey) o;
        return Objects.equals(id, that.id) && 
               Objects.equals(version, that.version);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, version);
    }
    
    // Getter and Setter methods
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Integer getVersion() { return version; }
    public void setVersion(Integer version) { this.version = version; }
}

Reference this embedded class in the entity class using the @EmbeddedId annotation:

@Entity
@Table(name = "versioned_data")
public class VersionedData {
    @EmbeddedId
    private DataVersionKey dataVersionKey;
    
    @Column(name = "column_a")
    private String columnA;
    
    @Column(name = "created_time")
    private LocalDateTime createdTime;
    
    public VersionedData() {}
    
    public VersionedData(DataVersionKey dataVersionKey, String columnA) {
        this.dataVersionKey = dataVersionKey;
        this.columnA = columnA;
        this.createdTime = LocalDateTime.now();
    }
    
    // Getter and Setter methods
    public DataVersionKey getDataVersionKey() { return dataVersionKey; }
    public void setDataVersionKey(DataVersionKey dataVersionKey) { this.dataVersionKey = dataVersionKey; }
    public String getColumnA() { return columnA; }
    public void setColumnA(String columnA) { this.columnA = columnA; }
    public LocalDateTime getCreatedTime() { return createdTime; }
    public void setCreatedTime(LocalDateTime createdTime) { this.createdTime = createdTime; }
}

@IdClass Implementation Approach

As an alternative approach, the @IdClass annotation allows direct definition of primary key fields within the entity class, while requiring a corresponding ID class:

public class DataVersionKey implements Serializable {
    private Long id;
    private Integer version;
    
    public DataVersionKey() {}
    
    public DataVersionKey(Long id, Integer version) {
        this.id = id;
        this.version = version;
    }
    
    // Getter and Setter methods
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Integer getVersion() { return version; }
    public void setVersion(Integer version) { this.version = version; }
}

@Entity
@IdClass(DataVersionKey.class)
@Table(name = "versioned_data")
public class VersionedData {
    @Id
    @Column(name = "id")
    private Long id;
    
    @Id
    @Column(name = "version")
    private Integer version;
    
    @Column(name = "column_a")
    private String columnA;
    
    // Other fields and methods identical to @EmbeddedId approach
}

Data Version Duplication Implementation

In version management scenarios, duplicating data entries and creating new versions is a common requirement. The following code demonstrates how to implement data version duplication functionality:

@Service
public class VersionedDataService {
    
    @Autowired
    private VersionedDataRepository repository;
    
    public VersionedData createNewVersion(Long dataId, String newData) {
        // Find current latest version
        Integer currentVersion = repository.findMaxVersionById(dataId)
            .orElse(-1);
        
        // Create new version
        Integer newVersion = currentVersion + 1;
        DataVersionKey newKey = new DataVersionKey(dataId, newVersion);
        
        VersionedData newVersionData = new VersionedData(newKey, newData);
        return repository.save(newVersionData);
    }
    
    public List<VersionedData> getAllVersions(Long dataId) {
        return repository.findByDataVersionKeyIdOrderByDataVersionKeyVersionDesc(dataId);
    }
}

@Repository
public interface VersionedDataRepository 
    extends JpaRepository<VersionedData, DataVersionKey> {
    
    @Query("SELECT MAX(v.dataVersionKey.version) FROM VersionedData v WHERE v.dataVersionKey.id = :id")
    Optional<Integer> findMaxVersionById(@Param("id") Long id);
    
    List<VersionedData> findByDataVersionKeyIdOrderByDataVersionKeyVersionDesc(Long id);
}

Spring Boot Environment Configuration

Configure JPA and database connections in Spring Boot projects:

# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/version_data_demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

# Logging Configuration
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

Composite Primary Key Query Operations

Using composite primary keys for data retrieval:

@Service
public class DataQueryService {
    
    @Autowired
    private VersionedDataRepository repository;
    
    public Optional<VersionedData> findByIdAndVersion(Long id, Integer version) {
        DataVersionKey key = new DataVersionKey(id, version);
        return repository.findById(key);
    }
    
    public Optional<VersionedData> findLatestVersion(Long id) {
        List<VersionedData> versions = repository
            .findByDataVersionKeyIdOrderByDataVersionKeyVersionDesc(id);
        return versions.isEmpty() ? Optional.empty() : Optional.of(versions.get(0));
    }
}

Technical Key Points Summary

Critical considerations in composite primary key implementation:

Embeddable classes must implement the Serializable interface, as required by the JPA specification. Additionally, proper overriding of equals() and hashCode() methods is essential to ensure correct object comparison.

In the @IdClass approach, field names and types in the ID class must exactly match the primary key fields in the entity class. While this approach results in a more dispersed code structure, it offers better flexibility in specific scenarios.

In version management scenarios, it's recommended to implement automatic version number incrementation and conflict detection at the business logic layer to ensure data consistency. Consider adding timestamp fields to record the creation time of each version for auditing and tracking purposes.

In practical applications, composite primary key design requires careful consideration of query performance and business requirements. Well-designed primary keys can significantly improve data access efficiency while simplifying business logic implementation.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.