javamybatis

Mybatis how mapping Integer to Enum?


In the database, the column "status" is integer.

xml mybatis

<resultMap id="TaskStatus" type="ru....domain.Task$Status">
            <result typeHandler="org.apache.ibatis.type.EnumTypeHandler"
                    property="id" column="status"/>
</resultMap>
    
<select id="selectStatus" resultMap="TaskStatus">
            select id, status
            from task
            where id = #{id}
</select>

my enum class

public class Task{
    
        @Getter
        @AllArgsConstructor
        public enum Status {
            CREATED(1),
            RUNNING(2),
            PAUSED(3),
            FINISHED(4),
            ARCHIVED(5),
            MODERATION_READY(6),
            MODERATING(7),
            REJECTED(8);
    
    
            private final Integer id;
        }
    ....
    }

I want to put a column in enum class.

Error

Error querying database. Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'status' from result set. Cause: java.lang.IllegalArgumentException: No enum constant ru...domain.Task.Status.2


Solution

  • The default EnumTypeHandler maps enum's name (e.g. "CREATED", "RUNNING"), so the column type must be one of text types like VARCHAR [1].

    As MyBatis knows nothing about the id property, you have to write a custom type handler.
    Here is an example implementation.

    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.JdbcType;
    import org.apache.ibatis.type.MappedTypes;
    import org.apache.ibatis.type.TypeHandler;
    
    @MappedTypes(Status.class)
    public class StatusTypeHandler implements TypeHandler<Status> {
      @Override
      public void setParameter(PreparedStatement ps, 
           int i, Status parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {
          ps.setNull(i, Types.INTEGER);
        } else {
          ps.setInt(i, parameter.getId());
        }
      }
    
      @Override
      public Status getResult(ResultSet rs, String columnName) throws SQLException {
        return getStatus(rs.getInt(columnName));
      }
    
      @Override
      public Status getResult(ResultSet rs, int columnIndex) throws SQLException {
        return getStatus(rs.getInt(columnIndex));
      }
    
      @Override
      public Status getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getStatus(cs.getInt(columnIndex));
      }
    
      private static Status getStatus(int id) {
        if (id == 0) {
          return null;
        }
        for (Status status : Status.values()) {
          if (id == status.getId()) {
            return status;
          }
        }
        throw new IllegalArgumentException("Cannot convert " + id + " to Status");
      }
    }
    

    You should register this type handler globally.
    Then it's unnecessary to specify typeHandler explicitly in most cases.

    1. If you use mybatis-spring-boot, specifying mybatis.type-handlers-package in the config may be the easiest way to register type handlers globally.

    2. If you use XML config, add the following.

      <typeHandlers>
        <typeHandler
          handler="xxx.yyy.StatusTypeHandler" />
      </typeHandlers>
      

    If the Status is the only enum in your project, you can stop reading.

    But, what if there are many enums that have id property and you don't want to write a similar custom type handler for each of them?

    If your enums implement a common interface like below, you can write a type handler that can map all of them.

    public interface HasId {
      Integer getId();
    }
    

    Here is an example type handler implementation. Note that it has a constructor that takes java.lang.Class as its argument.

    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.JdbcType;
    import org.apache.ibatis.type.MappedTypes;
    import org.apache.ibatis.type.TypeHandler;
    
    @MappedTypes(HasId.class)
    public class HasIdTypeHandler<E extends Enum<E> & HasId> implements TypeHandler<E> {
      private Class<E> type;
      private final E[] enums;
    
      public HasIdTypeHandler(Class<E> type) {
        if (type == null)
          throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
        this.enums = type.getEnumConstants();
        if (!type.isInterface() && this.enums == null)
          throw new IllegalArgumentException(type.getSimpleName()
              + " does not represent an enum type.");
      }
    
      @Override
      public void setParameter(PreparedStatement ps, 
          int i, E parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {
          ps.setNull(i, Types.INTEGER);
        } else {
          ps.setInt(i, parameter.getId());
        }
      }
    
      @Override
      public E getResult(ResultSet rs, String columnName) throws SQLException {
        return getEnum(rs.getInt(columnName));
      }
    
      @Override
      public E getResult(ResultSet rs, int columnIndex) throws SQLException {
        return getEnum(rs.getInt(columnIndex));
      }
    
      @Override
      public E getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getEnum(cs.getInt(columnIndex));
      }
    
      private E getEnum(int id) {
        if (id == 0) {
          return null;
        }
        for (E e : enums) {
          if (id == e.getId()) {
            return e;
          }
        }
        throw new IllegalArgumentException("Cannot convert " +
          id + " to " + type.getSimpleName());
      }
    }
    

    Note that if you try to specify this type handler in a mapper, it might not work properly. There is a known issue.

    [1] FYI, there is another built-in type handler for enums :EnumOrdinalTypeHandler maps enum's ordinal.