解决问题:PostgreSQL类型为 json, 但表达式的类型为 character varying

前言

我们在SpringBoot中使用Mybatis-plus操作Mysql是非常方便的,但使用PostgreSQL必须指定类型。
本文主要解决SpringBoot开发过程中使用PostgreSQL操作复杂对象,如Json、Array等数据的处理。

        <!-- PostgreSql -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

常见错误

类型为 json, 但表达式的类型为 character varying

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: org.postgresql.util.PSQLException: 错误: 字段 "json_data" 的类型为 json, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:245
### The error may exist in com/ray/mapper/DataTestMapper.java (best guess)
### The error may involve com.ray.mapper.DataTestMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO data_test ( id, json_data, array_data, create_by, create_time, update_by, update_time )  VALUES  ( ?, ?, ?, ?, ?, ?, ?)
### Cause: org.postgresql.util.PSQLException: 错误: 字段 "json_data" 的类型为 json, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:245
; bad SQL grammar []; nested exception is org.postgresql.util.PSQLException: 错误: 字段 "json_data" 的类型为 json, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:245
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:239)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:91)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
	at com.sun.proxy.$Proxy143.insert(Unknown Source)
	at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272)

自定义转换器

JsonTypeHandlerPg

package com.ruoyi.common.utils.pg;

import com.ruoyi.common.utils.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.postgresql.util.PGobject;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @ClassName: JsonTypeHandlerPg
 * @Description: json对象处理
 * @Author: Ray
 * @Date: 2022-10-17 10:34
 */
@MappedTypes(String.class)
public class JsonTypeHandlerPg extends BaseTypeHandler<String> {

    /**
     * 引入PGSQL提供的工具类PGobject
     */
    private static final PGobject JSON_OBJECT = new PGobject();
    public static final String JSON_TYPE = "json";

    /**
     * 关键位置!!!
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String param, JdbcType jdbcType) throws SQLException {
        JSON_OBJECT.setType(JSON_TYPE);
        JSON_OBJECT.setValue(param);
        ps.setObject(i, JSON_OBJECT);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String sqlJson = rs.getString(columnName);
        if (StringUtils.isNotBlank(sqlJson)) {
            return sqlJson;
        }
        return null;
    }

    //根据列索引,获取可以为空的结果
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String sqlJson = rs.getString(columnIndex);
        if (StringUtils.isNotBlank(sqlJson)) {
            return sqlJson;
        }
        return null;
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String sqlJson = cs.getString(columnIndex);
        if (StringUtils.isNotBlank(sqlJson)) {
            return sqlJson;
        }
        return null;
    }
}

使用

修改实体类

package com.ruoyi.dt.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.pg.JsonTypeHandlerPg;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 测试对象
 *
 * @author ray
 * @date 2022-10-18
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("data_test")
public class DataTest extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id")
    private Long id;
    
    /**
     * 关键位置!!!
     */
    @TableField(typeHandler = JsonTypeHandlerPg.class)
    private String jsonData;
    

}

修改mapper文件

<?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="com.ruoyi.dt.mapper.DataTestMapper">

    <resultMap type="com.ruoyi.dt.domain.DataTest" id="DataTestResult">
        <result property="id" column="id"/>
		<!-- 关键位置!!! -->
        <result property="jsonData" column="json_data" typeHandler="com.ruoyi.common.utils.pg.JsonTypeHandlerPg"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
        <result property="delTime" column="del_time"/>
    </resultMap>

   
</mapper>

业务代码

    /**
     * 新增数据
     */
    @Override
    public Boolean insertByAo(DataTestAo ao) {
        DataTest add = BeanUtil.toBean(ao, DataTest.class);
        // 关键位置!!!
		add.setJsonData(JSONUtil.toJsonStr(ao));
        validEntityBeforeSave(add);
        boolean flag = baseMapper.insert(add) > 0;
        if (flag) {
            ao.setId(add.getId());
        }
        return flag;
    }

ArrayTypeHandlerPg

数组类型同理

package com.ruoyi.common.utils.pg;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;

/**
 * @ClassName: ArrayTypeHandlerPg
 * @Description: array对象处理
 * @Author: Ray
 * @Date: 2022-10-17 10:34
 */
public class ArrayTypeHandlerPg extends BaseTypeHandler<Object[]> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ArrayTypeHandlerPg.class);

    private static final String TYPE_NAME_VARCHAR = "varchar";
    private static final String TYPE_NAME_INTEGER = "integer";
    private static final String TYPE_NAME_LONG = "long";
    private static final String TYPE_NAME_BOOLEAN = "boolean";
    private static final String TYPE_NAME_NUMERIC = "numeric";

    /**
     * 关键设置
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object[] parameter, JdbcType jdbcType)
        throws SQLException {
        String typename = null;
        if (parameter instanceof Integer[]) {
            typename = TYPE_NAME_INTEGER;
        } else if (parameter instanceof String[]) {
            typename = TYPE_NAME_VARCHAR;
        } else if (parameter instanceof Boolean[]) {
            typename = TYPE_NAME_BOOLEAN;
        } else if (parameter instanceof Double[]) {
            typename = TYPE_NAME_NUMERIC;
        } else if (parameter instanceof Long[]) {
            typename = TYPE_NAME_LONG;
        }

        if (typename == null) {
            throw new TypeException("arraytypehandler parameter typename error, your type is " + parameter.getClass().getName());
        }

        Array array = ps.getConnection().createArrayOf(typename, parameter);
        ps.setArray(i, array);
    }

    @Override
    public Object[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return getArray(rs.getArray(columnName));
    }


    @Override
    public Object[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return getArray(rs.getArray(columnIndex));
    }


    @Override
    public Object[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getArray(cs.getArray(columnIndex));
    }

    private Object[] getArray(Array array) {
        if (array == null) {
            return null;
        }
        try {
            return (Object[]) array.getArray();
        } catch (SQLException e) {
            LOGGER.error("ArrayTypeHandler getArray SQLException", e);
        }
        return null;
    }
}

使用

修改实体类

package com.ruoyi.dt.domain;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.pg.JsonTypeHandlerPg;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 测试对象
 *
 * @author ray
 * @date 2022-10-18
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("data_test")
public class DataTest extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id")
    private Long id;
    
    /**
     * 关键位置!!!
     */
    @TableField(typeHandler = ArrayTypeHandlerPg.class)
    private String[] arrayData;
    

}

修改mapper文件

<?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="com.ruoyi.dt.mapper.DataTestMapper">

    <resultMap type="com.ruoyi.dt.domain.DataTest" id="DataTestResult">
        <result property="id" column="id"/>
		<!-- 关键位置!!! -->
        <result property="arrayData" column="array_data" typeHandler="com.ruoyi.common.utils.pg.ArrayTypeHandlerPg"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
        <result property="delTime" column="del_time"/>
    </resultMap>

   
</mapper>