使用mybatis作为操作数据库的orm框架,操作基本数据类型时可以通过内置的类型处理器完成java数据类型和数据库类型的转换,但是对于扩展的数据类型要实现与数据库类型的转换就需要自定义类型转换器完成,比如某个实体类型存储到数据库,可以转换为json字符串存储,读取数据时再转换为对应的实体类。
在mybatis中可以有两种方式实现上面的方案:
一、直接继承mybatis框架提供的 org.apache.ibatis.type.BaseTypeHandler
完成数据类型转换;
二、如果项目引入了mybatis-plus,也可以继承 com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler
实现数据类型转换。
接下来分别介绍上面两种方案的实现方式。
首先在数据库中创建一个表用于测试数据存取:
CREATE TABLE `demo_data` (
`id` int NOT NULL AUTO_INCREMENT,
`detail` json NULL,
`create_time` datetime NULL,
PRIMARY KEY (`id`)
);
一、mybatis框架实现类型转换
使用mybatis实现类型转换,首先要自定义一个handler继承自基础的handler,再将自定义的handler注入到字段的typeHandler中就实现了类型转换:
自定义handler:
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @Author xingo
* @Date 2025/2/6
*/
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class DetailTypeHandler extends BaseTypeHandler<DemoData.DetailInfo> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, DemoData.DetailInfo parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JacksonUtils.toJSONString(parameter));
}
@Override
public DemoData.DetailInfo getNullableResult(ResultSet rs, String columnName) throws SQLException {
return JacksonUtils.parseObject(rs.getString(columnName), DemoData.DetailInfo.class);
}
@Override
public DemoData.DetailInfo getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return JacksonUtils.parseObject(rs.getString(columnIndex), DemoData.DetailInfo.class);
}
@Override
public DemoData.DetailInfo getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return JacksonUtils.parseObject(cs.getString(columnIndex), DemoData.DetailInfo.class);
}
}
上面就实现了java类型与数据库类型的对应关系,就是将实体类中的java对象与数据库中的字符串类型自动转换:
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @Author xingo
* @Date 2025/2/6
*/
@Data
public class DemoData implements Serializable {
private Integer id;
private DemoData.DetailInfo detail;
private LocalDateTime createTime;
@Data
public static class DetailInfo implements Serializable {
private String name;
private Integer age;
private LocalDateTime dateTime;
}
}
接下来就是在编写的xml文件中将刚刚自定义的handler和实体类信息完成对应关系:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xingo.demo.DemoDataMapper">
<resultMap id="demoData" type="org.xingo.demo.DemoData">
<id property="id" column="id"/>
<result property="detail" column="detail" typeHandler="org.xingo.demo.DetailTypeHandler"/>
<result property="createTime" column="create_time"/>
</resultMap>
<insert id="insertDemoData" useGeneratedKeys="true" keyProperty="id">
insert into demo_data(detail, create_time)
values (#{detail, typeHandler=org.xingo.demo.DetailTypeHandler}, #{createTime})
</insert>
<update id="updateDemoData">
update demo_data
set detail=#{detail, typeHandler=org.xingo.demo.DetailTypeHandler}
create_time=#{createTime}
where id=#{id}
</update>
<select id="findDemoDataById" resultMap="demoData">
select *
from demo_data
where id=#{id}
</select>
</mapper>
xml对应的接口:
import org.xingo.demo.DemoData;
/**
* @Author xingo
* @Date 2025/2/6
*/
public interface DemoDataMapper {
void insertDemoData(DemoData data);
void updateDemoData(DemoData data);
DemoData findDemoDataById(Integer id);
}
上面的几步就实现了自定义数据类型与数据库中字符串类型的转换,测试上面接口可以完成数据的存取:
二、mybatis-plus框架实现类型转换
使用mybatis实现自定义类型与数据库类型的转换相对来说还是有一点繁琐,如果在项目中引入了mybatis-plus,那么就可以减少xml文件的编写,直接在实体类的字段上添加注解完成xml文件的内容。
使用mybatis-plus实现类型转换首先也是自定义handler类:
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* @Author xingo
* @Date 2025/2/6
*/
@MappedTypes({DemoData.DetailInfo.class})
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class DetailTypeHandler extends AbstractJsonTypeHandler<DemoData.DetailInfo> {
@Override
protected DemoData.DetailInfo parse(String json) {
return JacksonUtils.parseObject(json, DemoData.DetailInfo.class);
}
@Override
protected String toJson(DemoData.DetailInfo detail) {
return JacksonUtils.toJSONString(detail);
}
}
映射主要是通过实体类的注解完成的:
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @Author xingo
* @Date 2025/2/6
*/
@Data
@TableName(value = "demo_data", autoResultMap = true)
public class DemoData implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(typeHandler = DetailTypeHandler.class)
private DemoData.DetailInfo detail;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Data
public static class DetailInfo implements Serializable {
private String name;
private Integer age;
private LocalDateTime dateTime;
}
}
mapper接口只需要继承mybatis-plus提供的基础mapper就可以:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Author xingo
* @Date 2025/2/6
*/
public interface DemoDataMapper extends BaseMapper<DemoData> {
}
通过上面的定义,所有基于mybatis-plus提供的增改查操作都可以完成字段类型转换。
测试上面的内容在数据库中产生的数据:
附:jackson工具类
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* json工具
*
* @Author xingo
* @Date 2025/2/6
*/
@Slf4j
public class JacksonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
// Long类型处理,避免前端处理长整型时精度丢失
SimpleModule module1 = new SimpleModule();
module1.addSerializer(Long.class, ToStringSerializer.instance);
module1.addSerializer(Long.TYPE, ToStringSerializer.instance);
JavaTimeModule module2 = new JavaTimeModule();
// java8日期处理
module2.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
module2.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module2.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
module2.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
module2.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module2.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
OBJECT_MAPPER
// 添加modules
.registerModules(module1, module2, new Jdk8Module())
// 日期类型不转换为时间戳
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
// 反序列化的时候如果多了其他属性,不抛出异常
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// 如果是空对象的时候,不抛异常
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
// 空对象不序列化
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
// 日期格式化
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
// 设置时区
.setTimeZone(TimeZone.getTimeZone("GMT+8"))
// 驼峰转下划线
// .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
// 语言
.setLocale(Locale.SIMPLIFIED_CHINESE);
}
/**
* 反序列化对象
*/
public static <T> T parseObject(String json, Class<T> clazz) {
if (json == null) {
return null;
}
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化对象
*/
public static JsonNode parseObject(String json) {
if (json == null) {
return null;
}
try {
return OBJECT_MAPPER.readTree(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化对象
*/
public static <T> T parseObject(String json, TypeReference<T> type) {
if (json == null) {
return null;
}
try {
return OBJECT_MAPPER.readValue(json, type);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化对象
*/
public static <T> T parseObject(byte[] bytes, TypeReference<T> type) {
if (bytes == null) {
return null;
}
try {
return OBJECT_MAPPER.readValue(bytes, type);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化对象
*/
public static <T> T parseObject(JsonNode jsonNode, Class<T> clazz) {
return jsonNode == null ? null : OBJECT_MAPPER.convertValue(jsonNode, clazz);
}
/**
* 反序列化列表
*/
public static <T> List<T> parseArray(String json, Class<T> clazz) {
if (json == null) {
return null;
}
try {
JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructParametricType(List.class, clazz);
return OBJECT_MAPPER.treeToValue(OBJECT_MAPPER.readTree(json), javaType);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化列表
*/
public static <T> List<T> parseArray(JsonNode json, Class<T> clazz) {
try {
JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructParametricType(List.class, clazz);
return json == null ? null : OBJECT_MAPPER.treeToValue(json, javaType);
} catch (JsonProcessingException e) {
log.warn(e.getLocalizedMessage());
return null;
}
}
/**
* 写为json串
*/
public static String toJSONString(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof String) {
return (String) obj;
}
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 写为字节数组
*/
public static byte[] toJSONBytes(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsBytes(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取jackson对象
*/
public static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
/**
* 美化输出json格式
*/
public static String pretty(String json) throws IOException {
return StringUtils.isBlank(json) ? json : pretty(JacksonUtils.getObjectMapper().readTree(json));
}
/**
* 美化输出json格式
*/
public static String pretty(JsonNode jsonNode) throws IOException {
return null == jsonNode ? "" : JacksonUtils.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
}
/**
* 对象转json
*/
public static JsonNode toJsonNode(Object obj) {
if (obj instanceof String) {
return parseObject((String) obj, JsonNode.class);
}
return obj == null ? null : OBJECT_MAPPER.convertValue(obj, JsonNode.class);
}
}