Spring Boot启动报错:无法确定TreeMap对应的JdbcType
Spring Boot启动失败:无法确定
TreeMap<LocalDate, Double>对应的JdbcType 执行./gradlew bootRun启动应用时,ApplicationContext启动失败,核心报错为:无法确定java.util.TreeMap<java.time.LocalDate, java.lang.Double>对应的推荐JdbcType。问题出在CreditCard实体类的balanceHistory字段,代码如下:
package com.shepherdmoney.interviewproject.model; import com.shepherdmoney.interviewproject.repository.UserRepository; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.ToString; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.time.LocalDate; import java.util.Optional; import java.util.TreeMap; @Entity @Getter @Setter @ToString @RequiredArgsConstructor public class CreditCard { @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String issuanceBank; private String number; private int userId; // the id of the user private TreeMap<LocalDate, Double> balanceHistory = new TreeMap<>(); public CreditCard(int userId) { this.userId = userId; // initialize balance history. It must have a date value that matches today's date. balanceHistory.put(LocalDate.now(), 0.0); } public CreditCard(String issuanceBank, String number, int userId) { this.issuanceBank = issuanceBank; this.number = number; this.userId = userId; // initialize balance history. It must have a date value that matches today's date. balanceHistory.put(LocalDate.now(), 0.0); } /** * * @param ur the user repo of this application * @return the name of this credit card's owner */ public String getUsername(UserRepository ur) { User u = ur.findById(userId).orElseThrow(); return u.getName(); } /** * Given `date` and `balance`, update this credit card's balance history. * Update means to add `balance` to the current balance at the date specified by `date` * @param date * @param balance */ public void updateBalance(LocalDate date, double balance) { double currentBalance = balanceHistory.getOrDefault(date, 0.0); balanceHistory.put(date, currentBalance + balance); } }
解决方案
JPA默认不支持直接映射TreeMap<LocalDate, Double>这类复杂Map类型到数据库,以下是三种可行的处理方式:
1. 使用@ElementCollection映射为关联表
通过JPA集合映射注解,让Hibernate自动创建关联表存储键值对:
修改balanceHistory字段的注解:
import jakarta.persistence.*; // ...其他注解保持不变 @ElementCollection @MapKeyColumn(name = "balance_date") // 指定LocalDate键对应的数据库列名 @Column(name = "balance_amount") // 指定Double值对应的数据库列名 @CollectionTable( name = "credit_card_balance_history", // 关联表名称 joinColumns = @JoinColumn(name = "credit_card_id") // 关联外键列 ) private TreeMap<LocalDate, Double> balanceHistory = new TreeMap<>();
这种方式符合关系型数据库设计,支持对历史记录做单独查询。
2. 序列化为JSON字符串存储
将TreeMap序列化为JSON字符串存入数据库TEXT字段,适合不需要对Map内容做数据库查询的场景:
首先添加自定义转换器:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; import java.time.LocalDate; import java.util.TreeMap; @Converter(autoApply = true) public class TreeMapJsonConverter implements AttributeConverter<TreeMap<LocalDate, Double>, String> { private final ObjectMapper objectMapper = new ObjectMapper() .registerModule(new JavaTimeModule()); // 支持LocalDate序列化 @Override public String convertToDatabaseColumn(TreeMap<LocalDate, Double> attribute) { try { return objectMapper.writeValueAsString(attribute); } catch (JsonProcessingException e) { throw new RuntimeException("序列化TreeMap失败", e); } } @Override public TreeMap<LocalDate, Double> convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, objectMapper.getTypeFactory().constructMapType(TreeMap.class, LocalDate.class, Double.class)); } catch (JsonProcessingException e) { throw new RuntimeException("反序列化JSON失败", e); } } }
然后修改balanceHistory字段:
import jakarta.persistence.Column; import jakarta.persistence.Convert; // ...其他注解保持不变 @Column(columnDefinition = "TEXT") @Convert(converter = TreeMapJsonConverter.class) private TreeMap<LocalDate, Double> balanceHistory = new TreeMap<>();
3. 拆分实体类(推荐用于复杂业务场景)
将余额历史拆分为独立实体,通过@OneToMany关联,更符合关系型数据库设计规范,适合需要对历史记录做统计、查询的场景:
创建BalanceHistory实体:
import jakarta.persistence.*; import java.time.LocalDate; @Entity public class BalanceHistory { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne @JoinColumn(name = "credit_card_id", nullable = false) private CreditCard creditCard; private LocalDate date; private Double balance; // 构造方法、Getter、Setter public BalanceHistory() {} public BalanceHistory(CreditCard creditCard, LocalDate date, Double balance) { this.creditCard = creditCard; this.date = date; this.balance = balance; } // Getter和Setter省略 }
修改CreditCard类的balanceHistory字段及相关方法:
import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; import java.util.Optional; // ...其他注解保持不变 @OneToMany(mappedBy = "creditCard", cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("date DESC") // 按日期倒序排列,满足业务要求的最新日期在前 private List<BalanceHistory> balanceHistory = new ArrayList<>(); // 构造方法修改 public CreditCard(int userId) { this.userId = userId; this.balanceHistory.add(new BalanceHistory(this, LocalDate.now(), 0.0)); } public CreditCard(String issuanceBank, String number, int userId) { this.issuanceBank = issuanceBank; this.number = number; this.userId = userId; this.balanceHistory.add(new BalanceHistory(this, LocalDate.now(), 0.0)); } // 更新余额方法修改 public void updateBalance(LocalDate date, double balance) { Optional<BalanceHistory> existingRecord = balanceHistory.stream() .filter(record -> record.getDate().equals(date)) .findFirst(); if (existingRecord.isPresent()) { BalanceHistory record = existingRecord.get(); record.setBalance(record.getBalance() + balance); } else { balanceHistory.add(new BalanceHistory(this, date, balance)); } }
内容的提问来源于stack exchange,提问作者HankY
相关产品推荐
相关产品推荐

