1. SQL查詢的統一抽象
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