New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for an enum type handler which persists specified value rather than ordinal or name. #42

Closed
fengdh opened this Issue May 1, 2013 · 13 comments

Comments

Projects
None yet
5 participants
@fengdh

fengdh commented May 1, 2013

MyBatis provides only EnumOridinalTypeHandler and EnumTypeHandler, but application (especially enterprise application) tends to persist specified value for each enum literal. It is welcome to provide a generic "EnumValueTypeHandler" to ease the problem.

In my implementation including:

  • an EnumValue annotation which is used to specify persistable value for enum literal
  • IEnumAdapter interface which define how to convert int value between enum literal
  • a generic concrete EnumAdapter implementation which maintains a fast-index to enum literals since most int value of enum literal falls into a narrow range of [0..N] with few exception (i.e., -1 or 999) for unkonw/undefined one(s).
  • EnumValueTypeHandler which can resolve an IEnumAdapter to execute convertion.
@harawata

This comment has been minimized.

Member

harawata commented May 9, 2013

Thank you for the suggestion, but MyBatis would not support it out of the box because it's not part of the enum spec unlike ordinal() or name().
A common solution would be to create an interface and a custom TypeHandler.

public interface HasValue {
  int getValue();
}

public class HasValueEnumTypeHandler<E extends Enum<E> & HasValue> extends
    BaseTypeHandler<E> {
  private Class<E> type;
  private final E[] enums;

  public HasValueEnumTypeHandler(Class<E> type) {
    if (type == null)
      throw new IllegalArgumentException("Type argument cannot be null");
    this.type = type;
    this.enums = type.getEnumConstants();
    if (this.enums == null)
      throw new IllegalArgumentException(type.getSimpleName()
          + " does not represent an enum type.");
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, E parameter,
      JdbcType jdbcType) throws SQLException {
    ps.setInt(i, parameter.getValue());
  }

  @Override
  public E getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    int value = rs.getInt(columnName);
    if (rs.wasNull()) {
      return null;
    }
    for (E enm : enums) {
      if (value == enm.getValue()) {
        return enm;
      }
    }
    throw new IllegalArgumentException("Cannot convert "
      + value + " to " + type.getSimpleName());
  }

  @Override
  public E getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    // almost the same as above
  }

  @Override
  public E getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    // almost the same as above
  }

Then you can define your enums as follows.

public enum YourEnum implements HasValue {
    AAA(100), BBB(200), CCC(300);

    int value;

    YourEnum(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

Hope this helps,
Iwao

@Fayou

This comment has been minimized.

Fayou commented Mar 7, 2017

Hello @harawata

I did the following :

  • Create the HasValue interface
  • Implement it by a custom Enum (called com.fayou.DummyEnum in my case)
  • Finish the implementation of the HasValueEnumTypeHandler you started
  • Create a mapper providing a method which returns the DummyEnum selected from a table row

However, when executing the query, I get the following IllegalArgumentException :
No enum constant com.fayou.DummyEnum.3
at java.lang.Enum.valueOf(Enum.java:238)
at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:49)
at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:26)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:66)
... 42 common frames omitted

This tends to suggest that the custom TypeHandler is not called, and the mapping is handled by the generic EnumTypeHandler.

Did I forget something to have my DummyEnum converted by the custom HasValueEnumTypeHandler ?

I also tried to annotate the custom TypeHandler with @MappedValues(DummyEnum.class), but I get the same error...

Thank you for your time,

Fayou

@harawata

This comment has been minimized.

Member

harawata commented Mar 8, 2017

Hi @Fayou ,

To use HasValueEnumTypeHandler, you need to register it in the config.

<typeHandlers>
  <typeHandler handler="com.xxx.HasValueEnumTypeHandler" />
</typeHandlers>

And add @MappedTypes to HasValueEnumTypeHandler to specify your enums.

@MappedTypes({DummyEnum.class, AnotherEnum.class})

If it still does not work, please create a test case or a demo app that demonstrates the exception and upload it to a GitHub repo.

@Fayou

This comment has been minimized.

Fayou commented Mar 8, 2017

Thank you very much @harawata !

The problem was indeed in the registering of the TypeHandler : I'm using mybatis-spring, and I was assuming that the type handlers were automatically picked up by Spring at startup, which is actually not the case.

I manually registered the HasValueEnumTypeHandler (via SqlSessionFactoryBean.setTypeHandlersPackage), and it finally worked.

Thank you again for your quick answer !

Fayou

@harawata

This comment has been minimized.

Member

harawata commented Mar 8, 2017

@Fayou ,
Auto-detection does not work well with type handlers because there can be multiple candidates and only the developer can choose the right one.
Anyway, glad to know that it worked =)

@mollyxin

This comment has been minimized.

mollyxin commented Jul 16, 2017

Hi @harawata
I am new to mybatis and find this post. However, there is an error when I try to put your codes in my projects. Would you kindly help with it?

Thanks a lot

I copied the handler:

@MappedTypes({UserType.class, AccountType.class})
public class HasValueEnumTypeHandler<E extends Enum<E> & HasValue> extends BaseTypeHandler<E> {
	private Class<E> type;
	private final E[] enums;

	public HasValueEnumTypeHandler(Class<E> type) {
		if (type == null)
			throw new IllegalArgumentException("Type argument cannot be null");
		this.type = type;
		this.enums = type.getEnumConstants();
		if (this.enums == null)
			throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
	}
	

	@Override
	public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
		int value = rs.getInt(columnName);
		if (rs.wasNull()) {
			return null;
		}
		for (E enm : enums) {
			if (value == enm.getValue()) {
				return enm;
			}
		}
		throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getSimpleName());
	}

	@Override
	public E getNullableResult(ResultSet rs, int columnName) throws SQLException {
		int value = rs.getInt(columnName);
		if (rs.wasNull()) {
			return null;
		}
		for (E enm : enums) {
			if (value == enm.getValue()) {
				return enm;
			}
		}
		throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getSimpleName());
	}

	@Override
	public E getNullableResult(CallableStatement cs, int columnName) throws SQLException {
		int value = cs.getInt(columnName);
		if (cs.wasNull()) {
			return null;
		}
		for (E enm : enums) {
			if (value == enm.getValue()) {
				return enm;
			}
		}
		throw new IllegalArgumentException("Cannot convert " + value + " to " + type.getSimpleName());
	}

	@Override
	public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
		ps.setInt(i, parameter.getValue());
	}

}

The interface:

public interface HasValue {
	int getValue();
}

The enum:

public enum UserType implements HasValue {
	RMWORD(1);

	int value;

	UserType(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}
}

And register the handler:

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="/WEB-INF/configuration.xml" />
        <property name="typeHandlers" >
            <array>
                <bean class="com.rmword.utils.HasValueEnumTypeHandler" />
            </array>
        </property>
    </bean>

The error when start:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Cannot create inner bean 'com.rmword.utils.HasValueEnumTypeHandler#40df9eb1' of type [com.rmword.utils.HasValueEnumTypeHandler] while setting bean property 'typeHandlers' with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.rmword.utils.HasValueEnumTypeHandler#40df9eb1' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.rmword.utils.HasValueEnumTypeHandler]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.rmword.utils.HasValueEnumTypeHandler.<init>()
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:313)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:122)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveManagedArray(BeanDefinitionValueResolver.java:370)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:153)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1471)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1216)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:302)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:229)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:298)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:193)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1127)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1051)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:949)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:600)
	... 59 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 
@harawata

This comment has been minimized.

Member

harawata commented Jul 16, 2017

@mollyxin ,

HasValueEnumTypeHandler needs to be instantiated by MyBatis because Spring does not know how to instantiate it.
Either one of the following settings should work.

  1. Specify typeHandlersPackage property of SqlSessionFactoryBean.
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="configLocation" value="/WEB-INF/configuration.xml" />
  <property name="typeHandlersPackage" value="com.rmword.utils" />
</bean>
  1. Register type handler in MyBatis configuration file (i.e. /WEB-INF/configuration.xml).
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  ...
  <typeHandlers>
    <typeHandler handler="com.rmword.utils.HasValueEnumTypeHandler" />
  </typeHandlers>
  ...
@kazuki43zoo

This comment has been minimized.

Member

kazuki43zoo commented Jul 16, 2017

@harawata 👍

We might be consider for adding typeHandlerTypes as new property of SqlSessionFactoryBean .

@mollyxin

This comment has been minimized.

mollyxin commented Jul 18, 2017

@harawata
My apologies for last deleted comment. It is due to I configure a wrong handler. Now, inserting data is successfully. There is a problem in fetch data.

In my userModel, there are two enum value ("UserType" and "AccountType"):

package com.rmword.model;

import java.math.BigInteger;
import java.util.Date;

import com.rmword.enums.AccountType;
import com.rmword.enums.UserType;
import java.util.UUID;

public class UserModel {

	private String userId;
	private String password;
	private String token;
	private Date registerTime;
	private UserType userType;
	private AccountType accountType;

	public UserModel(){
		
	}
	
	public UserModel(String password, String token, Date registerTime, UserType userType,
			AccountType accountType) {
		this.userId = String.format("%040d", new BigInteger(UUID.randomUUID().toString().replace("-", ""), 16));
		this.password = password;
		this.token = token;
		this.registerTime = registerTime;
		this.userType = userType;
		this.accountType = accountType;
	}

	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getToken() {
		return token;
	}

	public void setToken(String token) {
		this.token = token;
	}

	public Date getRegisterTime() {
		return registerTime;
	}

	public void setRegisterTime(Date registerTime) {
		this.registerTime = registerTime;
	}

	public UserType getUserType() {
		return userType;
	}

	public void setUserType(UserType userType) {
		this.userType = userType;
	}

	public AccountType getAccountType() {
		return accountType;
	}

	public void setAccountType(AccountType accountType) {
		this.accountType = accountType;
	}
}

my mapper.xml is

<?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.rmword.mapper.UserModelMapper">

	<resultMap type="com.rmword.model.UserModel" id="usermap">
		<id column="user_id" property="userId" />
		<result column="password" property="password" />
		<result column="token" property="token" />
		<result column="register_time" property="registerTime" />
		<result column="user_type" property="userType"
			typeHandler="com.rmword.utils.HasValueEnumTypeHandler" />
		<result column="account_type" property="accountType"
			typeHandler="com.rmword.utils.HasValueEnumTypeHandler" />
	</resultMap>

	<insert id="insertUser">
		insert into user (user_id, password, token,
		register_time, user_type, account_type) values (
		#{userId},
		#{password}, #{token}, #{registerTime},
		#{userType, typeHandler=com.rmword.utils.HasValueEnumTypeHandler},
		#{accountType,
		typeHandler=com.rmword.utils.HasValueEnumTypeHandler}
		)
	</insert>

	<select id="selectUserById" parameterType="String" resultMap="usermap">
		select * from user where user_id = #{userId}
	</select>

</mapper>
public enum UserType implements HasValue {
	RMWORD(1);

	int value;

	UserType(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}
}

public enum AccountType implements HasValue {
	EMAIL(1),
	QQ(2),
	WEIBO(3);
	
	int value;

	AccountType(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}	
}

When select the user model, I get the exception as following:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Could not set property 'userType' of 'class com.rmword.model.UserModel' with value 'EMAIL' Cause: java.lang.IllegalArgumentException: argument type mismatch

I debug the handler. the "type" is "AccountType", However the column_name is "user_type". Do you know how to solve this?

@harawata

This comment has been minimized.

Member

harawata commented Jul 18, 2017

@mollyxin ,

It should work if you remove typeHandler from the mapper.
Please see #995 if you wonder why.

<?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.rmword.mapper.UserModelMapper">

	<resultMap type="com.rmword.model.UserModel" id="usermap">
		<id column="user_id" property="userId" />
		<result column="password" property="password" />
		<result column="token" property="token" />
		<result column="register_time" property="registerTime" />
		<result column="user_type" property="userType" />
		<result column="account_type" property="accountType" />
	</resultMap>

	<insert id="insertUser">
		insert into user (user_id, password, token,
		register_time, user_type, account_type) values (
		#{userId},
		#{password}, #{token}, #{registerTime},
		#{userType},
		#{accountType}
		)
	</insert>

	<select id="selectUserById" parameterType="String" resultMap="usermap">
		select * from user where user_id = #{userId}
	</select>

</mapper>

In case it still does not work, please create a small example project like these and upload it to your GitHub repo then I will look into it.

@kazuki43zoo ,

We might be consider for adding typeHandlerTypes as new property of SqlSessionFactoryBean .

Adding another similarly named property could be a little bit confusing.
And, in most cases, typeHandlersPackage may be sufficient.

@kazuki43zoo

This comment has been minimized.

Member

kazuki43zoo commented Jul 18, 2017

Adding another similarly named property could be a little bit confusing.
And, in most cases, typeHandlersPackage may be sufficient.

I agree !

@mollyxin

This comment has been minimized.

mollyxin commented Jul 19, 2017

@harawata
Thank you very much. Now, inserting and fetching are both successfully. Thanks again for you quick and detail answer.

@harawata

This comment has been minimized.

Member

harawata commented Jul 19, 2017

@mollyxin Glad to know it worked :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment