MyBatis制動動態SQL的構造,利用動態SQL和自定義的參數Bean抽象,可以將絕大部分SQL查詢抽象爲一個統一接口,查詢參數使用一個自定義bean繼承Map,使用映射的方法構造多查詢參數.在遇到多屬性參數(例如order by,其參數包括列名,升序降序類型,以及可以多個列及升降序類型憑藉在order by之後)無法使用簡單的key-value表示時,可以將參數單獨抽象爲一個類.
將要用到的bean
package com.xxx.mybatistask.bean;
import com.xxx.mybatistask.support.jsonSerializer.JsonDateDeserializer;
import com.xxx.mybatistask.support.jsonSerializer.JsonDateSerializer;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import java.util.Date;
public class Post {
private int id;
private String title;
private String content;
private String author;
private PostStatus status;
private Date created;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public PostStatus getStatus() {
return status;
}
public void setStatus(PostStatus status) {
this.status = status;
}
@JsonSerialize(using = JsonDateSerializer.class)
public Date getCreated() {
return created;
}
@JsonDeserialize(using = JsonDateDeserializer.class)
public void setCreated(Date created) {
this.created = created;
}
}
1)參數Bean設計
總的參數Map抽象接口設計
package com.xxx.mybatistask.bean.query;
import java.util.Map;
public interface QueryParam extends Map<String, Object> {
/**
* 新增查詢參數
*
* @param key 參數名
* @param value 參數值
* @return
*/
QueryParam fill(String key, Object value);
}
列表查詢參數接口
package com.xxx.mybatistask.bean.query;
import java.util.List;
public interface ListQueryParam extends QueryParam {
/**
* 獲取排序條件集合
*
* @return
*/
List<SortCond> getSortCond();
/**
* 添加排序條件
*
* @param sortCond
*/
void addSortCond(SortCond sortCond);
void addSortCond(List<SortCond> sortCondList);
/**
* 獲取當前頁數
*
* @return
*/
Integer getPage();
/**
* 獲取每頁查詢記錄數
*
* @return
*/
Integer getPageSize();
/**
* 設置當前頁數
*/
void setPage(Integer page);
/**
* 設置每頁查詢記錄數
*/
void setPageSize(Integer pageSize);
}
列表查詢參數接口實現
package com.xxx.mybatistask.bean.query;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
public class GenericQueryParam extends LinkedHashMap<String, Object> implements ListQueryParam {
/**
* 最大單頁記錄數
*/
public final static int MAX_PAGE_SIZE = 100;
/**
* 當前頁面key
*/
private final static String PAGE_KEY = "__page";
/**
* 單頁記錄數key
*/
private final static String PAGESIZE_KEY = "__pagesize";
/**
* 排序參數List key
*/
private final static String SORTCOND_KEY = "__sortcond";
public GenericQueryParam() {
this(1, 10);
}
public GenericQueryParam(
Integer page,
Integer pageSize
) {
setPage(page);
setPageSize(pageSize);
}
@Override
public Integer getPage() {
return (Integer) get(PAGE_KEY);
}
@Override
public Integer getPageSize() {
return (Integer) get(PAGESIZE_KEY);
}
@Override
public void setPage(Integer page) {
put(PAGE_KEY, page);
}
@Override
public void setPageSize(Integer pageSize) {
put(PAGESIZE_KEY, pageSize);
}
@Override
@SuppressWarnings("unchecked")
public List<SortCond> getSortCond() {
List<SortCond> sortCondList = (List<SortCond>) get(SORTCOND_KEY);
if (sortCondList == null) {
sortCondList = new LinkedList<SortCond>();
put(SORTCOND_KEY, sortCondList);
}
return sortCondList;
}
@Override
@SuppressWarnings("unchecked")
public void addSortCond(SortCond sortCond) {
List<SortCond> sortCondList = (List<SortCond>) get(SORTCOND_KEY);
if (sortCondList == null) {
sortCondList = new LinkedList<SortCond>();
put(SORTCOND_KEY, sortCondList);
}
sortCondList.add(sortCond);
}
@Override
public void addSortCond(List<SortCond> sortCondList) {
for (SortCond sortCond : sortCondList) addSortCond(sortCond);
}
@Override
public QueryParam fill(String key, Object value) {
put(key, value);
return this;
}
}
排序參數的抽象
package com.xxx.mybatistask.bean.query;
public class SortCond {
/**
* 排序類型枚舉
*/
public enum Order {
ASC, DESC
}
/**
* 排序類型
*/
private String column;
/**
* 排序類型
*/
private Order order;
public SortCond(String column) {
this(column, Order.DESC);
}
public SortCond(String column, Order order) {
this.column = column;
this.order = order;
}
public String getColumn() {
return column;
}
public Order getOrder() {
return order;
}
}
2)Service查詢接口設計
package com.xxx.mybatistask.service;
import com.xxx.mybatistask.bean.query.GenericQueryParam;
import org.apache.ibatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
public abstract class AbstractService {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
protected SqlSession sqlSession;
/**
* 分頁參數校驗
*
* @param params
* @param rowCount
* @return
*/
protected void pageParamValidate(GenericQueryParam params, int rowCount) {
int page = params.getPage();
int pageSize = params.getPageSize();
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 1;
if (pageSize > GenericQueryParam.MAX_PAGE_SIZE)
pageSize = GenericQueryParam.MAX_PAGE_SIZE;
int maxPage = (int) Math.ceil((double) rowCount / pageSize);
if (page > maxPage) page = maxPage;
params.setPage(page);
params.setPageSize(pageSize);
}
}
package com.xxx.mybatistask.service;
import com.xxx.mybatistask.bean.Post;
import com.xxx.mybatistask.bean.query.GenericQueryParam;
import com.xxx.mybatistask.bean.query.ListResult;
public interface PostService {
/**
* 查詢參數列名枚舉
*/
public enum PostQueryPram {
title, content, author, status, created
}
void create(Post post);
/**
* 翻頁查詢
*
* @param param
* @return
*/
ListResult<Post> select(GenericQueryParam param);
void update(Post post);
}
package com.xxx.mybatistask.service.impl;
import com.xxx.mybatistask.bean.Post;
import com.xxx.mybatistask.bean.query.GenericQueryParam;
import com.xxx.mybatistask.bean.query.ListResult;
import com.xxx.mybatistask.service.AbstractService;
import com.xxx.mybatistask.service.PostService;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Service;
import java.util.LinkedList;
import java.util.List;
@Service
public class PostServiceImpl extends AbstractService implements PostService {
@Override
public void create(Post post) {
sqlSession.insert("post.insert", post);
}
@Override
public ListResult<Post> select(GenericQueryParam params) {
Integer rowCount = sqlSession.selectOne("post.selectCount", params);
if (rowCount == 0) {
return new ListResult<Post>(new LinkedList<Post>(), 0);
}
// 分頁參數檢查
pageParamValidate(params, rowCount);
int page = params.getPage();
int pageSize = params.getPageSize();
int offset = (page - 1) * pageSize;
RowBounds rowBounds = new RowBounds(offset, pageSize);
List<Post> postList = sqlSession.selectList("post.select", params, rowBounds);
return new ListResult<Post>(postList, rowCount);
}
@Override
public void update(Post post) {
sqlSession.update("post.update", post);
}
}
3)自定義參數bean的解析與轉換
以SortCond爲例,由於是多屬性查詢參數,所以我們需要自己定義參數在客戶端的文本格式,從客戶端傳入後再使用自定義的Paser來將其包裝成SortCond
例如此處我們定義的排序參數在url中的格式爲
/api/post/query/title/an?page=3&pageSize=200&sorts=created:DESC|author:ASC
其中排序參數爲 "created:DESC|author:ASC" , 解析類如下
package com.xxx.mybatistask.support.stringparser;
import java.util.List;
public interface Parser<T> {
/**
* 字符串轉對象
*
* @param parseString 待轉換字符串
* @return List<T> 轉換完成的對象List
*/
List<T> parseList(String parseString);
}
package com.xxx.mybatistask.support.stringparser;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.xxx.mybatistask.bean.query.SortCond;
import java.util.List;
import java.util.Map;
public class SortCondParser implements Parser<SortCond> {
/**
* 排序列分隔符
*/
private static final String COL_SPLITTER = "|";
/**
* 順序類型分隔符
*/
private static final String ORDER_SPLITTER = ":";
/**
* 列名檢查
*/
private Class<? extends Enum> columnEnumCls;
public SortCondParser(Class<? extends Enum> columnEnumCls) {
this.columnEnumCls = columnEnumCls;
}
/**
* 將字符串轉換爲SortCond
* 字符串的標準格式爲
* title:ASC|created:DESC
*
* @param parseString 待轉換字符串
* @return
*/
@Override
public List<SortCond> parseList(String parseString) {
List<SortCond> sortCondList = Lists.newArrayList();
// 將字符串切分爲 {"column" => "order"} 的形式
Map<String, String> sortOrderMap =
Splitter.on(COL_SPLITTER)
.trimResults()
.omitEmptyStrings()
.withKeyValueSeparator(ORDER_SPLITTER)
.split(parseString);
String column = null;
String order = null;
for (Map.Entry<String, String> entry : sortOrderMap.entrySet()) {
// 驗證column合法性
column = entry.getKey();
if (column != null && !column.equals("")) {
Enum.valueOf(columnEnumCls, column);
} else {
break;
}
// 驗證order合法性
order = entry.getValue();
if (order != null && !order.equals("")) {
Enum.valueOf(SortCond.Order.class, order);
} else {
order = SortCond.Order.DESC.name();
}
sortCondList.add(new SortCond(column, SortCond.Order.valueOf(order)));
}
return sortCondList;
}
}
4) 動態查詢SQL的編寫
<select id="select"
parameterType="com.xxx.mybatistask.bean.query.GenericQueryParam"
resultType="com.xxx.mybatistask.bean.Post">
<![CDATA[
select
id,
title,
content,
author,
status,
created
from
post
]]>
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title like concat('%', #{title}, '%')
</if>
<if test="author != null and author != ''">
and author like concat('%', #{author}, '%')
</if>
<if test="content != null and content != ''">
and match(content) against(#{content})
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="created != null and created != ''">
and created = #{created}
</if>
</where>
<if test="_parameter.getSortCond().size() != 0">
order by
<foreach collection="_parameter.getSortCond()" item="sortCond" separator=",">
${sortCond.column} ${sortCond.order}
</foreach>
</if>
</select>
至此SQL抽象接口以及完成,結合SortCond類,動態SQL和OGNL動態生成了order by參數,而類似的像 JOIN ... ON (USING) 或者 GROUP BY ... HAVING 等查詢參數條件,也可以將其抽象成bean,通過GenericQueryParam成員變量的形式拼接到SQL查詢語句中來
另外代碼中並沒有對參數進行過多的檢查,原因是:
1. MyBatis SQL查詢使用prepareStatement,對於注入問題相對安全
2. 動態SQL查詢使用<if>判斷where查詢條件,如果參數中的map key不是有效列名,將不會拼接到SQL語句中
3. 即使由於惡意用戶篡改參數格式造成不規範參數的SQL查詢異常,對於這種異常只需要重定向到全局error頁面即可
5) Controller調用示例
@RequestMapping(value = "/query/{colKey}/{colVal}", method = RequestMethod.GET)
public
@ResponseBody
Object query(
@PathVariable String colKey,
@PathVariable String colVal,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(value = "sorts", required = false, defaultValue = "") String sorts
) {
// page and col
GenericQueryParam params = new GenericQueryParam(page, pageSize);
params.fill(colKey, colVal)
.fill(
PostService.PostQueryPram.status.name(),
PostStatus.valueOf(status)
);
// sorts
SortCondParser sortCondParser = new SortCondParser(PostService.PostQueryPram.class);
params.addSortCond(sortCondParser.parseList(sorts));
ListResult<Post> postList = postService.select(params);
return dataJson(postList);
}
2. TypeHandler設計
上文中的bean Post類中status屬性類型是enum類,如下
package com.xxx.mybatistask.bean;
public enum PostStatus {
NORMAL(0, "正常"), LOCKED(1, "鎖定");
private int code;
private String text;
private PostStatus(int code, String text) {
this.code = code;
this.text = text;
}
public int code() {
return code;
}
public String text() {
return text;
}
public static PostStatus codeOf(int code) {
for (PostStatus postStatus : PostStatus.values()) {
if (postStatus.code == code) {
return postStatus;
}
}
throw new IllegalArgumentException("invalid code");
}
public static boolean contains(String text) {
for (PostStatus postStatus : PostStatus.values()) {
if (postStatus.toString().equals(text)) {
return true;
}
}
return false;
}
}
而這個屬性在數據庫中的類型實際上市一個tinyint表示的標記位,爲了讓mybatis jdbc自動轉換這個tinyint標記位爲enum(查詢時)和轉換enum爲tinyint(插入更新時),需要編寫mybatis typehandler
package com.xxx.mybatistask.support.typehandler;
import com.xxx.mybatistask.bean.PostStatus;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PostStatusTypeHandler implements TypeHandler<PostStatus> {
/**
* PostStatus插入數據庫時轉換的方法
* 將使用PostStatus的code插入數據庫
*
* @param preparedStatement
* @param index
* @param postStatus
* @param jdbcType
* @throws SQLException
*/
@Override
public void setParameter(PreparedStatement preparedStatement, int index, PostStatus postStatus, JdbcType jdbcType) throws SQLException {
preparedStatement.setInt(index, postStatus.code());
}
/**
* status查詢出來時轉爲PostStatus的方法
*
* @param resultSet
* @param colName
* @return
* @throws SQLException
*/
@Override
public PostStatus getResult(ResultSet resultSet, String colName) throws SQLException {
return PostStatus.codeOf(resultSet.getInt(colName));
}
@Override
public PostStatus getResult(ResultSet resultSet, int colIndex) throws SQLException {
return PostStatus.codeOf(resultSet.getInt(colIndex));
}
@Override
public PostStatus getResult(CallableStatement callableStatement, int colIndex) throws SQLException {
return PostStatus.codeOf(callableStatement.getInt(colIndex));
}
}
在MyBatis配置文件中配置這個TypeHandler是其對PostStatus參數生效
<typeHandlers>
<typeHandler handler="com.xxx.mybatistask.support.typehandler.PostStatusTypeHandler"
javaType="com.xxx.mybatistask.bean.PostStatus"/>
</typeHandlers>
3. 特殊參數的序列化與反序列化
由於需要實現接收和響應JSON數據,自動將JSON數據包裝爲具體對象類,此處使用了Spring的@ResponseBody以及@RequestBody標籤,JSON的轉換器爲org.codehaus.jackson
但是對於某些特殊屬性,例如此處的Post裏的created屬性,在bean中表現爲Date類型,而在數據庫中爲TIMESTAMP類型,如果直接輸出到JSON響應中,將會輸出timestamp的毫秒數,爲了格式化爲自定義的格式,我們需要自定義一個JSON序列化(轉爲響應文本時)與反序列化(接收請求參數轉爲POST類時)的類.如下
序列化類
package com.xxx.mybatistask.support.jsonSerializer;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class JsonDateSerializer extends JsonSerializer<Date> {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(sdf.format(date));
}
}
反序列化類
package com.xxx.mybatistask.support.jsonSerializer;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.ObjectCodec;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class JsonDateDeserializer extends JsonDeserializer<Date> {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public Date deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
try {
return sdf.parse(node.getTextValue());
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
然後注意在Post類中標明,當Jackson序列化Post類爲JSON串或將JSON串反序列化成Post類時,將調用這兩個類,Post類的代碼片段
@JsonSerialize(using = JsonDateSerializer.class)
public Date getCreated() {
return created;
}
@JsonDeserialize(using = JsonDateDeserializer.class)
public void setCreated(Date created) {
this.created = created;
}
THE END
轉載請註明本文地址:利用MyBatis的動態SQL特性抽象統一SQL查詢接口