Spring Boot嵌入式复合键实体查询:类型转换异常解决方法咨询
看起来你遇到的是Spring Data REST无法自动将URL中的字符串路径参数转换为自定义嵌入式复合主键类的问题,这是因为框架默认不知道如何处理这种自定义的复合主键类型。下面我会给出最直接的解决方案,让你既能正常查询单个实体,也能让HAL响应里的_links生成正确的URL。
问题原因
Spring Data REST在生成HAL格式的响应时,会尝试将实体的主键转换为URL路径中的字符串,同时在查询单个实体时,又需要把URL中的字符串转回主键对象。对于你的MyEntityIdentifier这种自定义复合主键类,框架没有内置的转换器,所以抛出了No converter found capable of converting from type [java.lang.String] to type [com.MyApp.MyEntityIdentifier]的异常。
解决方案:实现自定义ID转换器
最优雅的解决方式是实现Spring Data REST提供的BackendIdConverter接口,告诉框架如何在字符串和你的复合主键类之间进行转换。
步骤1:实现BackendIdConverter
创建一个转换器类,负责字符串和MyEntityIdentifier的双向转换:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.stereotype.Component; import java.sql.Timestamp; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.Objects; @Component public class MyEntityIdentifierConverter implements BackendIdConverter { // 用一个清晰的分隔符拆分主键的两个部分,比如":" private static final String ID_SEPARATOR = ":"; @Override public Serializable fromId(String idString) { // 将URL中的字符串拆分为主键的两个部分 String[] idParts = idString.split(ID_SEPARATOR); if (idParts.length != 2) { throw new IllegalArgumentException("无效的ID格式,正确格式应为:idPartOne:ISO-8601时间戳"); } MyEntityIdentifier identifier = new MyEntityIdentifier(); identifier.setIdPartOne(idParts[0]); // 将时间戳字符串转换为Timestamp对象 try { Instant instant = Instant.parse(idParts[1]); identifier.setIdPartTwo(Timestamp.from(instant)); } catch (DateTimeParseException e) { throw new IllegalArgumentException("时间戳格式错误,请使用ISO-8601格式(例如:2024-05-20T14:30:00Z)", e); } return identifier; } @Override public String toId(Serializable primaryKey) { // 将复合主键对象转换为URL友好的字符串 if (!(primaryKey instanceof MyEntityIdentifier)) { throw new IllegalArgumentException("仅支持MyEntityIdentifier类型的主键"); } MyEntityIdentifier identifier = (MyEntityIdentifier) primaryKey; // 将Timestamp转换为ISO-8601格式的字符串,确保跨系统兼容性 return identifier.getIdPartOne() + ID_SEPARATOR + identifier.getIdPartTwo().toInstant().toString(); } @Override public boolean supports(Class<?> primaryKeyType) { // 指定该转换器仅处理MyEntityIdentifier类型 return MyEntityIdentifier.class.equals(primaryKeyType); } }
步骤2:完善复合主键类
确保你的MyEntityIdentifier类有无参构造函数、getter/setter方法,并且重写equals()和hashCode()(JPA要求复合主键类必须实现这些,否则可能出现实体比较错误):
import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.io.Serializable; import java.sql.Timestamp; import java.util.Objects; @Embeddable public class MyEntityIdentifier implements Serializable { @Column private String idPartOne; @Column private Timestamp idPartTwo; // 必须提供无参构造函数,Spring Data需要它来实例化对象 public MyEntityIdentifier() {} public MyEntityIdentifier(String idPartOne, Timestamp idPartTwo) { this.idPartOne = idPartOne; this.idPartTwo = idPartTwo; } // Getter和Setter方法 public String getIdPartOne() { return idPartOne; } public void setIdPartOne(String idPartOne) { this.idPartOne = idPartOne; } public Timestamp getIdPartTwo() { return idPartTwo; } public void setIdPartTwo(Timestamp idPartTwo) { this.idPartTwo = idPartTwo; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyEntityIdentifier that = (MyEntityIdentifier) o; return Objects.equals(idPartOne, that.idPartOne) && Objects.equals(idPartTwo, that.idPartTwo); } @Override public int hashCode() { return Objects.hash(idPartOne, idPartTwo); } }
步骤3:验证效果
现在重启你的应用:
- 当你访问
GET /api/myEntities获取列表时,响应中的_links.self和_links.myEntity会生成类似/api/myEntities/part1:2024-05-20T14:30:00Z的正确URL。 - 直接访问单个实体的URL(比如
GET /api/myEntities/part1:2024-05-20T14:30:00Z),框架会自动通过转换器将字符串转换为MyEntityIdentifier对象,成功查询到对应的实体。
备选方案:自定义查询方法
如果你不想实现转换器,也可以在仓库接口中定义一个通过主键两个部分查询的方法,暴露为REST接口:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import java.sql.Timestamp; @RepositoryRestResource(path = "myEntities") public interface MyEntityRepository extends JpaRepository<MyEntity, MyEntityIdentifier> { @RestResource(path = "by-id-parts", rel = "find-by-id-parts") MyEntity findById_IdPartOneAndId_IdPartTwo( @Param("partOne") String idPartOne, @Param("partTwo") Timestamp idPartTwo); }
这样你可以通过GET /api/myEntities/search/by-id-parts?partOne=xxx&partTwo=2024-05-20T14:30:00来查询单个实体,但这种方式不会替换默认的/api/myEntities/{id}路径,所以更推荐第一种转换器的方案。
内容的提问来源于stack exchange,提问作者TheRealJimShady

