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.