You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 16:47:15