MeteorCat / JPA的日期转化

Created Fri, 29 Dec 2023 15:56:09 +0800 Modified Wed, 29 Oct 2025 23:25:00 +0800
1407 Words

JPA日期时间转化

之前讲过可以 JPA 通过内部转化实现自动映射, 如以下情况:

@Entity
@Table(name = "tbl_admin_info")
public class AdminInfoModel {

    /**
     * 自定义时间转化器
     */
    @Convert(converter = ConvertDateAndTimestamp.class)
    @Column(nullable = false, columnDefinition = "INT COMMENT '最后更新时间'")
    private Date updateTime = new Date(0L);
}

但是如果响应 JSON 数据的时候就带来麻烦了, 默认响应 JSON 的情况会直接将 new Date(0) 格式转化为默认时间戳时间, 也就是 1900-xx-xx xx:xx:xx 格式.

在多次实践后发现自定义转化的问题特别多, 包括 JSON 格式的转化没办法通过 JPA 进行精准|模糊查询等(因为内部转化 Map|List 是没办法加入 CrudRepository 作为条件检索, MySQL|MariaDB后续更新的JSON查询语法都没法调用).

也就是这种情况之下开始回过头来审视 Convert 自定义转化利弊.

AttributeConverter 功与过

类似于 JSON 格式当中解构, 如下面的 JSON 数组解析器:


/**
 * List转化JSON数据
 */
public class ConvertListAndJson implements AttributeConverter<List<Object>, String> {

    final static ObjectMapper mapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(List<Object> strings) {
        try {
            return mapper.writeValueAsString(strings);
        } catch (JsonProcessingException exception) {
            throw new RuntimeException("数据格式错误");
        }
    }

    @Override
    public List<Object> convertToEntityAttribute(String s) {
        try {
            return mapper.readValue(s, new TypeReference<ArrayList<Object>>() {
            });
        } catch (JsonProcessingException exception) {
            throw new RuntimeException("数据格式错误");
        }
    }
}

然后应用到 JPA 当中:

@Entity
@Table(name = "tbl_event_info")
public class EventInfoModel {
    /**
     * 这里也是 Browse - Install - Register 归因主要字段
     * 内部采用 [ md5('xxx'),md5('yyy'),md5('zzz'),... ] 放置多个哈希特征值, 可能是 imei|idfa|androidId|IpAddress|UserAgent|finger等
     * 主要关键就是匹配出 Browse - Install - Register 整个流程跑完的用户, 确定归因点击广告等行为触发注册的玩家
     * 利用 MySQL8 之后的 JSON 特性匹配出 endTime < 当前事件 且 keyboards IN(xxx) 的关联数据
     */
    @Convert(converter = ConvertListAndJson.class)
    @Column(nullable = false, columnDefinition = "JSON COMMENT '归因字段,imei|idfa|androidId|IpAddress|UserAgent|finger之类做MD5关联, 然后检索指定32位字符串'")
    private List<String> keyboards = new ArrayList<>();
}

这时候这种解析看起来很完美对不对, 但是这里隐藏很大的坑点: 查询无法匹配自定义类型; 可以看到SQL注释指名了这个 JSON 数组是需要模糊匹配查询出指定数据, 但是在编写 CrudRepository 数据工厂会一直报错找不到对应类型匹配条件.

在这种 JSON 格式之中, 新版本数据库支持以下 JSON 查询:

# 最新版本查询
SELECT * FROM tbl_event_info WHERE JSON_CONTAINS(keyboards,'"5b42eb43695678968bc68e4e5a700a05"');

# 哪怕老版本可以把字段认为是TEXT模糊匹配
SELECT * FROM tbl_event_info WHERE keyboards LIKE "%5b42eb43695678968bc68e4e5a700a05%";

但是在 JPA 如果使用自定义转化器将会完全失效, 因为在调用 CrudRepositorylike 查询的时候会默认 keyboards 为字符串导致和自定义 converter 出现类型冲突直接报错.

当然可能会有人说用 @Query(native = true) 直接编写原生语句就可以了吧, 事实上也不行的.

/**
 * 事件仓库
 */
public interface EventInfoRepository extends CrudRepository<EventInfoModel, Long>,JpaSpecificationExecutor<EventInfoModel> {


    /**
     * Like只认准字段必须为String对象
     */
    List<EventInfoModel> findAllByKeyboardsLike(String keyboard);

    /**
     * 也是异常类型错误, 找不到对应数据
     */
    @Query(nativeQuery = true,value = "SELECT * FROM tbl_event_info WHERE keyboards LIKE \"%?1%\"")
    List<EventInfoModel> findKeyboard(String keyboard);
}

这种情况之下 keyboards 字段完全处于不可检索状态, 那么最后不得不将其改回 String 类型从而支持 LIKE 模糊匹配查询.

这个坑点在网上找到很多资料之后还是没办法处理, 最后只能转回原生的String才能完美实现功能.

包括之前的提供的 Date 自动转化情况, 在作为字段查询检索情况十分不理想, 同时还会导致结构体作为数据响应给客户端的情况默认转化为 1900 这个初始时间戳格式.

但是如果不参与查询的时候, converter 直接默认对数据做好转化, 不需要去编写多余工具代码从而让其直接取出数据库 JSON 就能映射 MAP|LIST 对象从而精简好用.

到这里就看出问题了, 如果自定义格式对于参与查询检索是十分受限的, 而且在项目初期是没办法预估是否字段会参与查询的, 所以直接盲目引入自定义转化器对于后期维护回带来大量问题.

也就是踩过这次坑之后, 对于 converter 如果真的迫不得已, 否则千万不要去优先采用自定义转化方案!