skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.


  2. To specify a custom type handler for INSERT, you need to specify typeHandler in each parameter reference.

    insert into ... values (..., #{role,typeHandler=pkg.BitByteaTypeHandler}, ...
    

    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.

    import java.sql.CallableStatement;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Types;
    
    import org.apache.ibatis.type.BaseTypeHandler;
    import org.apache.ibatis.type.JdbcType;
    
    public class BitByteaTypeHandler extends BaseTypeHandler<byte[]>{
    
      @Override
      public void setNonNullParameter(PreparedStatement ps, int i, byte[] parameter, JdbcType jdbcType)
          throws SQLException {
        ps.setObject(i, new String(parameter), Types.OTHER);
      }
    
      @Override
      public byte[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getBytes(columnName);
      }
    
      @Override
      public byte[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getBytes(columnIndex);
      }
    
      @Override
      public byte[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getBytes(columnIndex);
      }
    }
    

    The getNullableResult() methods may not be used because the built-in ByteArrayTypeHandler 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search