Mybatis how mapping Integer to Enum?

Viewed 613

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

1 Answers

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.

Related