利用MyBatis的动态SQL特性抽象统一SQL查询接口
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
利用MyBatis的动态SQL特性抽象统一SQL查询接口的更多相关文章
- Java利用Mybatis进行数据权限控制
权限控制主要分为两块,认证(Authentication)与授权(Authorization).认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based ...
- MyBatis的动态SQL详解
MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑,本文详解mybatis的动态sql,需要的朋友可以参考下 MyBatis 的一个强大的特性之一通常是它 ...
- MyBatis框架——动态SQL、缓存机制、逆向工程
MyBatis框架--动态SQL.缓存机制.逆向工程 一.Dynamic SQL 为什么需要动态SQL?有时候需要根据实际传入的参数来动态的拼接SQL语句.最常用的就是:where和if标签 1.参考 ...
- Mybatis的动态SQL实现
一.动态SQL简介 MyBatis的强大特性之一便是它的动态 SQL.如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦.拼接的时候要确保不能忘了必要的 ...
- Java-MyBatis:MyBatis 3 动态 SQL
ylbtech-Java-MyBatis:MyBatis 3 动态 SQL 1.返回顶部 1. 动态 SQL MyBatis 的强大特性之一便是它的动态 SQL.如果你有使用 JDBC 或其它类似框架 ...
- 一分钟带你了解下MyBatis的动态SQL!
MyBatis的强大特性之一便是它的动态SQL,以前拼接的时候需要注意的空格.列表最后的逗号等,现在都可以不用手动处理了,MyBatis采用功能强大的基于OGNL的表达式来实现,下面主要介绍下. 一. ...
- MyBatis 实践 -动态SQL/关联查询
MyBatis 实践 标签: Java与存储 动态SQL 动态SQL提供了对SQL语句的灵活操作,通过表达式进行判断,对SQL进行拼接/组装. if 对查询条件进行判断,如果输入参数不为空才进行查询条 ...
- Mybatis.net与MVC入门配置及联合查询动态SQL拼接和简单事务
第一次学习Mybatis.net,在博客园也找到好多资料,但是在配置成功之后也遇到了一些问题,尤其是在动态SQl拼接时候,这里把遇到的问题还有自己写的一个Demo贴出来,希望能帮到新手,有不适合的地方 ...
- Mybatis之动态构建SQL语句
今天一个新同事问我,我知道如何利用XML的方式来构建动态SQL,可是Mybatis是否能够利用注解完成动态SQL的构建呢?!!答案是肯定的,MyBatis 提供了注解,@InsertProvider, ...
随机推荐
- USACO 6.1 Postal Vans(一道神奇的dp)
Postal Vans ACM South Pacific Region -- 2003 Tiring of their idyllic fields, the cows have moved to ...
- C#实现盛大盛付通充值卡状态查询
今天有这样一需求,要求能够查询盛付通卡的状态,官网如下 http://www.801335.com/status/index.htm 刚一打开网址,发现两个输入框加一个验证码,心中一喜不是小 cas ...
- HDU - 4458 计算几何判断点是否在多边形内
思路:将飞机看成不动的,然后枚举时间看点是否在多边形内部. #include<bits/stdc++.h> #define LL long long #define fi first #d ...
- PHP phpexcel 导入时间/日期转换时间戳
strtotime(gmdate('Y-m-d H:i',\PHPExcel_Shared_Date::ExcelToPHP($importtime))); /** * 判断字符串是否是日期格式 * ...
- php 生成二维码(qrcode)
可以用composer安装 https://packagist.org/packages/endroid/qrcode
- TradingView 自定义指标
TradingView 支持自定义指标,不过是把你要定义的指标写成一个 JS 源文件(customIndex.js),放在图表库 static 文件夹下.自定义指标 JS 源代码模板如下: __cus ...
- 20169211《Linux内核原理与分析》 第九周作业
一.Linux内核虚拟文件系统学习总结 Linux支持各种文件系统,Linux内核通过虚拟文件系统了对各种文件系统共性的进行抽象,并对外提供统一接口,从面向对象编程的角度来看,称为抽象文件系统更为合适 ...
- NetworkX 使用(二)
官方教程 博客:NetworkX %pylab inline import networkx as nx Populating the interactive namespace from numpy ...
- ubuntu下安装和破解navicat的方法
ubuntu下安装和破解navicat的方法 之前我也在苦苦搜寻ubuntu完美破解navicat的方法,但是大家都说是删除掉~/.Navicat,就可以续用,的确是这样,但是很麻烦. 于是我找到了一 ...
- Little Elephant and Array 线段树
题目:http://codeforces.com/problemset/problem/220/B 题意 给定一组数据,多次询问区间内某数字出现次数与该数字数值相同的数的个数 思路 一看到区间查询,就 ...