I want to solve the following Postgresql error when insert a record into database.
org.postgresql.util.PSQLException: error: column "role" type is bit, but type of expression is bytea.
The structure of DO is as follows:
@TableName("agri_person_relation")
@KeySequence("agri_person_relation_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PersonRelationDO extends BaseDO {
@TableId
private Long id;
private Long relatedTableId;
private byte[] role;
private byte[] module;
private Long personId;
}
The structure of the table is as follows:
PostgreSQL
DROP TABLE IF EXISTS "public"."agri_person_relation";
CREATE TABLE "public"."agri_person_relation" (
"creator" varchar(64) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" int2 NOT NULL DEFAULT 0,
"id" int8 NOT NULL,
"related_table_id" int8,
"role" bit(2),
"module" bit(3),
"person_id" int8
)
I have tried using TypeHandler as in the pic but it doesn’t work.
import org.apache.ibatis.type.*;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
//convert byte[] to BIT in postgresql
@MappedJdbcTypes(JdbcType.BIT)
@MappedTypes(byte[].class)
public class BitByteaTypeHandler extends BaseTypeHandler<byte[]> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, byte[] parameter, JdbcType jdbcType) throws SQLException {
StringBuffer sb = new StringBuffer();
for(byte aByte: parameter) {
sb.append(Integer.toBinaryString(aByte-48));
}
ps.setString(i,sb.toString());
}
@Override
public byte[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
return getBytes(rs.getString(columnName));
}
@Override
public byte[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return getBytes(rs.getString(columnIndex));
}
@Override
public byte[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return getBytes(cs.getString(columnIndex));
}
private byte[] getBytes(String str){
byte [] bytes = new byte[str.length()];
for(int i=0;i < str.length();i++) {
if (str.charAt(i)=='1'){
bytes[i] = (byte)49;
}
else{
bytes[i] = (byte)48;
}
}
return bytes;
}
}
I used it in a mapper file like this:
@Results({
@Result(column = "role", property = "role", typeHandler = BitByteaTypeHandler.class),
@Result(column = "module", property = "module", typeHandler = BitByteaTypeHandler.class)})
default int insertN(PersonRelationDO personRelationDO){
return insert(personRelationDO);};
The way I use it is kind of strange and that is because the insert method is provided by Mybatis Plus in its BaseMapper class. I cannot change it, the only way I can come up with is to wrap it by defining insertN().
I will post its code below:
package com.baomidou.mybatisplus.core.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.exceptions.TooManyResultsException;
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);
int deleteBatchIds(@Param("coll") Collection<?> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
List<T> list = this.selectList(queryWrapper);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0L;
}
Long selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}
But the error remains the same: org.postgresql.util.PSQLException: error: column "role" type is bit, but the type of expression is bytea.
As ave said, that way of using @result probably won’t work at all. I tried to configure mybatis-plus in the .yaml file:
mybatis-plus:
type-handlers-package: cn.iocoder.yudao.module.agri.utils.BitByteaTypeHandler
and added the annotation in DO class:
@TableField(value = "role", typeHandler = BitByteaTypeHandler.class)
private byte[] role;
@TableField(value = "module", typeHandler = BitByteaTypeHandler.class)
private byte[] module;
I am pretty convinced that this way actually worked as the error turned into
org.postgresql.util.PSQLException: error: column "role" type is bit, but type of expression is character varying.
So I futher explored the PreparedStatement class and its set methods, it seems it doesn’t support PostgreSQL bit(n) type, it surely offers a method called setBoolean:
https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setBoolean-int-boolean-
void setBoolean(int parameterIndex, boolean x) throws SQLException;
and its comments:
Sets the designated parameter to the given Java boolean value. The driver converts this to an SQL BIT or BOOLEAN value when it sends it to the database.
Params: parameterlndex -the first parameter is 1, the second is 2, …
x-the parameter value
Throws: SQLException -if parameterlndex does not correspond to a parameter marker in the SQL
statement: if a database access error occurs or this method is called on a closed Preparedstatement
The method works for bit(1) but can not process bit(n) in PostgreSQL.
And the method setBinaryStream seems also won’t suit as its unit is byte.
https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setBinaryStream-int-java.io.InputStream-
2
Answers
After some surveys, I am pretty much sure that the problem is not solvable as Java JDBC doesn't offer a set method for bit(n) type in Postgresql. The JDBC type BIT actually refers to bit(1) in Postgresql which can be mapped to Boolean in Java.
To specify a custom type handler for INSERT, you need to specify
typeHandler
in each parameter reference.Regarding
BIT(n)
support, there is an ongoing discussion.https://github.com/pgjdbc/pgjdbc/issues/908
With pgjdbc 42.6.0, the following type handler should do the job.
The
getNullableResult()
methods may not be used because the built-inByteArrayTypeHandler
will be used by default.As I am not familiar with some of the annotations in the question, I wrote a simple executable demo.
https://github.com/harawata/mybatis-issues/tree/master/so-76234163