mybatis 處理陣列型別及使用Json格式儲存資料 JsonTypeHandler and ArrayTypeHandler
mybatis 比 ibatis 改進了很多,特別是支援了註解,支援了plugin inteceptor,也給開發者帶來了更多的靈活性,相比其他ORM,我還是挺喜歡mybatis的。
閒言碎語不要講,今天研究了下mybatis的typeHandler:
先看這樣一張表(postgresql)
create table user ( id serial not null name character varchar(100), age integer, emails character varchar[], -- varchar 陣列 表示可以多個email address character varchar(2000) -- 因為地址內容為非結構化的資料,我們希望儲存json格式描述的地址資訊,以增加靈活性 );
這個表有2個欄位值得我們注意:
1. emails 為 character varchar[] 陣列型別
2. address 我們希望儲存為json格式的資料,查詢時返回json字串,mybatis orm 之後可以還原為一個數據物件VO。
完成這2個需求,則需要我們標題中提到的 JsonTypeHandler & ArrayTypeHandler
先看第一個typHandler: ArrayTypeHandler
我們先準備VO的程式碼:
public class UserVO { private long id; private String name; private int age; private String[] emails; Private Object address; ...... }
其中 emails 對應資料庫的 emails,address 對應資料庫的 address,為什麼用Object型別呢,這是因為以後我們可能會有個 AddressVO 這樣的物件,也可能會有 AddressVO2 extends AddressVO 這樣的物件,但是資料庫的schame不會變。
接下來我們看一下 UserDao.xml 中的片段:
<pre name="code" class="html"><resultMap type="com.kylin.test.userVO" id="userVO"> <result property="id" column="id"/> <result property="name" column="name"/> <result property="age" column="age"/> <result property="emails" column="emails" typeHandler="com.kylin.test.util.mybatis.handler.ArrayTypeHandler"/> <result property="address" column="address" typeHandler="com.kylin.test.util.mybatis.handler.JsonTypeHandler"/> </resultMap>
上面的resultMap中配置了2個typeHandler,關於typeHandler的配置,mybatis有多種方法,這裡簡單示意一下。
再看UserDao.xml中的2個方法,需要使用到這2個handler
<insert id="addUser" parameterType="com.kylin.test.UserVO">
INSERT INTO user (
name,
age,
emails,
address)
VALUES (
#{name, jdbcType=VARCHAR},
#{age, jdbcType=INTEGER},
#{emails, jdbcType=ARRAY, typeHandler=com.kylin.test.util.mybatis.handler.ArrayTypeHandler},
#{address, jdbcType=VARCHAR, typeHandler=com.kylin.test.util.mybatis.handler.JsonTypeHandler})
</insert>
<select id="getUserById" resultMap="userVO">
SELECT *
FROM user
WHERE id = #{0}
</select>
上述的addUser方法,傳入了字串陣列,和Object物件,儲存到了資料庫中,見下面的程式碼:
UserVO user = new UserVO();
user.setName("kylin");
user.setAge(30);
user.setEmails(new String[] { "[email protected]", "[email protected]" });
Map<String, Object> address = new HashMap<String, Object>();
address.put("country", "china");
address.put("province", "guangdong");
address.put("city", "shenzhen");
user.setAddress(address);
// 呼叫dao.addUser方法
userDao.addUser(user);
上面這個方法,將emails 字串陣列儲存入資料庫,將Map address,以json字串的方式儲存到資料庫
select * from user;
id name age emails address
-------------------------------------------------------------------------------
1 kylin 30 ["[email protected]","[email protected]"] {"contry":"china","province":"guangdong","city":"shenzhen"}
看到輸入按期望的存入到了資料庫中,稍後我們看從資料庫讀取出後是什麼樣子,我們先看看ArrayTypeHandler.java
mybatis 已經實現了 BaseTypeHandler<T> 這個抽象類,並將公共的邏輯實現好了,我們只需要繼承BaseTypeHandler就好,只需要處理簡單資料即可
package com.kylin.test.util.mybatis.handler;
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
// 繼承自BaseTypeHandler<Object[]> 使用時傳入的引數一定要是Object[],例如 int[]是 Object, 不是Object[],所以傳入int[] 會報錯的
public class ArrayTypeHandler extends BaseTypeHandler<Object[]> {
private static final String TYPE_NAME_VARCHAR = "varchar";
private static final String TYPE_NAME_INTEGER = "integer";
private static final String TYPE_NAME_BOOLEAN = "boolean";
private static final String TYPE_NAME_NUMERIC = "numeric";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object[] parameter,
JdbcType jdbcType) throws SQLException {
/* 這是ibatis時的做法
StringBuilder arrayString = new StringBuilder("{");
for (int j = 0, l = parameter.length; j < l; j++) {
arrayString.append(parameter[j]);
if (j < l - 1) {
arrayString.append(",");
}
}
arrayString.append("}");
ps.setString(i, arrayString.toString());
*/
String typeName = null;
if (parameter instanceof Integer[]) {
typeName = TYPE_NAME_INTEGER;
} else if (parameter instanceof String[]) {
typeName = TYPE_NAME_VARCHAR;
} else if (parameter instanceof Boolean[]) {
typeName = TYPE_NAME_BOOLEAN;
} else if (parameter instanceof Double[]) {
typeName = TYPE_NAME_NUMERIC;
}
if (typeName == null) {
throw new TypeException("ArrayTypeHandler parameter typeName error, your type is " + parameter.getClass().getName());
}
// 這3行是關鍵的程式碼,建立Array,然後ps.setArray(i, array)就可以了
Connection conn = ps.getConnection();
Array array = conn.createArrayOf(typeName, parameter);
ps.setArray(i, array);
}
@Override
public Object[] getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return getArray(rs.getArray(columnName));
}
@Override
public Object[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return getArray(rs.getArray(columnIndex));
}
@Override
public Object[] getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return getArray(cs.getArray(columnIndex));
}
private Object[] getArray(Array array) {
if (array == null) {
return null;
}
try {
return (Object[]) array.getArray();
} catch (Exception e) {
}
return null;
}
}
JsonTypeHandler 我們需要用到處理Json的第三方包:jackson,這個包據說處理json是效率最快的,代價最小的。
先封裝一個JsonUtil,並提供JsonUtil.stringify(...) JsonUtil.parse(...) 這樣2個方法出來
package com.kylin.test.util.json;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonFilter;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.springframework.core.annotation.AnnotationUtils;
public class JsonUtil {
private static Log log = LogFactory.getLog(JsonUtil.class);
private static ObjectMapper objectMapper = null;
static {
objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat(FormatUtil.DATE_FORMAT_LONG));
objectMapper.disable(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.setFilters(new SimpleFilterProvider().setFailOnUnknownId(false));
}
/*
public static JsonUtil getInstance() {
if (instance == null) {
synchronized (JsonUtil.class) {
if (instance == null) {
instance = new JsonUtil();
}
}
}
return instance;
}
*/
public static String stringify(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
public static String stringify(Object object, String... properties) {
try {
return objectMapper
.writer(new SimpleFilterProvider().addFilter(
AnnotationUtils.getValue(
AnnotationUtils.findAnnotation(object.getClass(), JsonFilter.class)).toString(),
SimpleBeanPropertyFilter.filterOutAllExcept(properties)))
.writeValueAsString(object);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
public static void stringify(OutputStream out, Object object) {
try {
objectMapper.writeValue(out, object);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static void stringify(OutputStream out, Object object, String... properties) {
try {
objectMapper
.writer(new SimpleFilterProvider().addFilter(
AnnotationUtils.getValue(
AnnotationUtils.findAnnotation(object.getClass(), JsonFilter.class)).toString(),
SimpleBeanPropertyFilter.filterOutAllExcept(properties)))
.writeValue(out, object);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static <T> T parse(String json, Class<T> clazz) {
if (json == null || json.length() == 0) {
return null;
}
try {
return objectMapper.readValue(json, clazz);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
}
接著再看看JsonTypeHandler
package com.kylin.test.util.mybatis.handler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.kylin.test.util.json.JsonUtil;
// 繼承自BaseTypeHandler<Object> 使用Object是為了讓JsonUtil可以處理任意型別
public class JsonTypeHandler extends BaseTypeHandler<Object> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtil.stringify(parameter));
}
@Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return JsonUtil.parse(rs.getString(columnName), Object.class);
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return JsonUtil.parse(rs.getString(columnIndex), Object.class);
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return JsonUtil.parse(cs.getString(columnIndex), Object.class);
}
}
至此,JsonTypeHandler 和 ArrayTypeHandler 就分享介紹完了,
如前面的 resultMap的配置,當呼叫 getUserById方法時,會返回 String[], 和Map<String, Object>物件回來,
有了這2個基礎TypeHandler,接下來設計資料庫和資料結構就會方便和靈活很多了。