利用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM