mybatis动态查询框架

1.mybatis通用查询框架

1.1.该框架的设计背景

在使用数据库时,我们经常需要根据用户的输入条件来查询内容,所以代码中各个业务模块都有着相同的逻辑代码,仅仅是业务不一样,所以我们怎么抽象出变与不变,形成一个基础组件,使得各个业务模块通用呢?

通过抽象,我们可以观察到,上面的核心问题在于不变,比如一个系统有50个模块,每个模块仅仅是对应的业务不一样(表不一样),但是查询的过程是不变的。

  • 变,指的是业务模块
  • 不变,指的是根据用户的输入,去数据库查询的过程

1.2.常用的解决方案

  • 使用mybatis的dynamic sql
  • 在业务层捕获param,然后构建where sql

1.3.如何做到动态扩展查询条件

在使用mybatis的dynamic sql时,有一个很明显的缺点,那就是mapper文件的中的查询条件是固定的,如果要动态的添加查询条件时,需要同时更新mapper.xml以及parameterType的查询对象
假设我们为下面的sql添加一个新的查询条件,age > 25

select * from student
<where>
    <if test="name != null and name != ''">
        name = #{name}
    </if>
    <if test="tel != null and tel != ''">
        and tel = #{tel}
    </if>
</where>

那么我们需要将上面的sql修改为如下的样子

select * from student
<where>
    <if test="name != null and name != ''">
        name = #{name}
    </if>
    <if test="tel != null and tel != ''">
        and tel = #{tel}
    </if>
    <if test="age != null">
        and age > #{age}
    </if>
</where>

2.mybatis # $的区别

  • #{} 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符
  • ${} 解析为一个存粹的字符串替换

比如下面的例子,我们假设name = 'luohong'

name = #{name},将会被解析为name = ?,然后jdbc PreparedStatement会动态的将luohong的值替换掉?,也就是name = 'luohong'
name = ${name},那么将会被直接解析为name = 'luohong'

2.1

既然mybatis的dynamic sql有两个占位符,那么我们该使用哪一个呢?

大部分的情况下,我们都是用#{},因为这种方式,可以避免一些sql注入的问题

3.利用mybatis的$特性来创建一个动态查询sql框架

还是上面的sql,假设我们将该sql修改为下面的格式

select * from student
<where>
    ${whereSql}
</where>

其中whereSql代表的是业务层已经处理好的查询条件sql,那么在mapper文件这一侧,我们就实现了动态增加、删除查询条件的特性了

  • 比如A模块:whereSql = "name = 'luohong'";
  • 比如B模块:whereSql = "name = 'luohong' or name = 'xiemeijiao'";
  • 比如C模块:whereSql = "name = 'luohong' and tel = '15013338888'";
  • 比如D模块:whereSql = "name = 'luohong' and age > 25";

4.设计思路与实现

sql常用的比较操作

  • 等于
  • 不等于
  • 大于
  • 大于等于
  • 小于
  • 小于等于
  • between ... and ...
  • LIKE
  • IN

支持数据类型,后续感兴趣的同学可以自行扩展,比如TimeStamp等

  • String
  • Date
  • Number

5.核心代码

先看一个类图


20180823191055.png

ICondition,抽象出一个条件表达式,比如:name = 'luohong', age > 15

package luohong;

/**
 * 抽象的查询条件
 * 这里面使用模板设计模式,目前支持Date,Number,String三种数据类型
 * @author luohong
 */
public interface ICondition {
    /**
     * 查询条件的名字
     * @return
     */
    String name();

    /**
     * 查询条件的比较类型
     * @return
     */
    OperationEnum operation();

    /**
     * 查询条件的值
     * @return
     */
    Object value();

    /**
     * 查询条件生成的sql
     * @return
     */
    String sql();
}

OperationEnum的抽象,这里面使用枚举

package luohong;

public enum OperationEnum {
    EQ("="),
    NEQ("!="),
    GTE(">="),
    GT(">"),
    LTE("<="),
    LT("<"),
    LIKE("LIKE"),
    IN("IN");

    private String value;

    /**
     * 比较类型
     * @param operation
     */
    OperationEnum(String operation){
        value = operation;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

DateCondition的实现

package luohong;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

public class DateCondition implements ICondition {
    private String name;
    private Date value;
    private List<Date> values;
    private OperationEnum operation;

    public DateCondition(String name, OperationEnum operation, Date value){
        if(operation == OperationEnum.IN){
            throw new RuntimeException("Not support IN operation for single value, please use new StringCondition(String name, List<String> values)");
        }

        if(operation == OperationEnum.LIKE){
            throw new RuntimeException("Not supper LIKE operation for date condition");
        }

        this.name = name;
        this.operation = operation;
        this.value = value;
    }

    public DateCondition(String name, List<Date> values){
        this.name = name;
        this.operation = OperationEnum.IN;
        this.values = values;
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public OperationEnum operation() {
        return operation;
    }

    @Override
    public Object value() {
        return value;
    }

    @Override
    public String sql() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        switch (operation()){
            case EQ:
            case NEQ:
            case GT:
            case GTE:
            case LT:
            case LTE:
                return name + " " +  operation.getValue() +  " '" + sdf.format(value) + "'";
            case IN:
                StringBuilder sb = new StringBuilder();
                sb.append(name() + " " + operation().getValue() + " (");
                for(int i=0; i<values.size(); i++){
                    if(i == values.size() - 1){
                        sb.append("'" + sdf.format(values.get(i)) + "'");
                    }else{
                        sb.append("'" + sdf.format(values.get(i)) + "',");
                    }
                }
                sb.append(")");
                return sb.toString();
        }
        throw new RuntimeException(name() + operation() + value() + " generate sql fail");
    }

    public static void main(String[] args) {
        System.out.println(new DateCondition("name", OperationEnum.EQ, new Date()).sql());
        System.out.println(new DateCondition("name", OperationEnum.NEQ, new Date()).sql());
        System.out.println(new DateCondition("name", OperationEnum.LT, new Date()).sql());
        System.out.println(new DateCondition("name", OperationEnum.LTE, new Date()).sql());
        System.out.println(new DateCondition("name", OperationEnum.GT, new Date()).sql());
        System.out.println(new DateCondition("name", OperationEnum.GTE, new Date()).sql());
        System.out.println(new DateCondition("name", Arrays.asList(new Date(), new Date())).sql());
    }
}

NumberCondition的实现

package luohong;
import java.util.Arrays;
import java.util.List;

public class NumberCondition implements ICondition {
    private String name;
    private Number value;
    private List<Number> values;
    private OperationEnum operation;

    public NumberCondition(String name, OperationEnum operation, Number value){
        if(operation == OperationEnum.IN){
            throw new RuntimeException("Not support IN operation for single value, please use new StringCondition(String name, List<String> values)");
        }
        if(operation == OperationEnum.LIKE){
            throw new RuntimeException("Not support LIKE operation for number condition");
        }

        this.name = name;
        this.operation = operation;
        this.value = value;
    }

    public NumberCondition(String name, List<Number> values){
        this.name = name;
        this.operation = OperationEnum.IN;
        this.values = values;
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public OperationEnum operation() {
        return operation;
    }

    @Override
    public Object value() {
        return value;
    }

    @Override
    public String sql() {
        switch (operation()){
            case EQ:
            case NEQ:
            case GT:
            case GTE:
            case LT:
            case LTE:
                return name + " " +  operation.getValue() + " " + value;
            case IN:
                StringBuilder sb = new StringBuilder();
                sb.append(name() + " " + operation().getValue() + " (");
                for(int i=0; i<values.size(); i++){
                    if(i == values.size() - 1){
                        sb.append("" + values.get(i) + "");
                    }else{
                        sb.append("" + values.get(i) + ",");
                    }
                }
                sb.append(")");
                return sb.toString();
        }
        throw new RuntimeException(name() + operation() + value() + " generate sql fail");
    }

    public static void main(String[] args) {
        System.out.println(new NumberCondition("name", OperationEnum.EQ, 10).sql());
        System.out.println(new NumberCondition("name", OperationEnum.NEQ, 100L).sql());
        System.out.println(new NumberCondition("name", OperationEnum.LT, 1).sql());
        System.out.println(new NumberCondition("name", OperationEnum.LTE, 100.2).sql());
        System.out.println(new NumberCondition("name", OperationEnum.GT, 1000).sql());
        System.out.println(new NumberCondition("name", OperationEnum.GTE, 100F).sql());
        System.out.println(new NumberCondition("name", Arrays.asList(1, 2, 3, 4, 5)).sql());
    }
}

StringCondition的实现

package luohong;

import java.util.Arrays;
import java.util.List;

public class StringCondition implements ICondition {
    private String name;
    private String value;
    private List<String> values;
    private OperationEnum operation;

    public StringCondition(String name, OperationEnum operation, String value){
        if(operation == OperationEnum.IN){
            throw new RuntimeException("Not support IN operation for single value, please use new StringCondition(String name, List<String> values)");
        }

        this.name = name;
        this.operation = operation;
        this.value = value;
    }

    public StringCondition(String name, List<String> values){
        this.name = name;
        this.operation = OperationEnum.IN;
        this.values = values;
        this.value = values.toString();
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public OperationEnum operation() {
        return operation;
    }

    @Override
    public Object value() {
        return value;
    }

    @Override
    public String sql() {
        switch (operation()){
            case EQ:
            case NEQ:
            case GT:
            case GTE:
            case LT:
            case LTE:
                return name + " " +  operation.getValue() +  " '" + value + "'";
            case LIKE:
                return name + " " + operation.getValue() +  " '%" + value + "%'";
            case IN:
                StringBuilder sb = new StringBuilder();
                sb.append(name() + " " + operation().getValue() + " (");
                for(int i=0; i<values.size(); i++){
                    if(i == values.size() - 1){
                        sb.append("'" + values.get(i) + "'");
                    }else{
                        sb.append("'" + values.get(i) + "',");
                    }
                }
                sb.append(")");
                return sb.toString();
        }
        throw new RuntimeException(name() + operation() + value() + " generate sql fail");
    }

    public static void main(String[] args) {
        System.out.println(new StringCondition("name", OperationEnum.EQ, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.NEQ, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.LT, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.LTE, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.GT, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.GTE, "luohong").sql());
        System.out.println(new StringCondition("name", OperationEnum.LIKE, "luohong").sql());
        System.out.println(new StringCondition("name", Arrays.asList("luohong", "luoyan")).sql());
    }
}

为了简化调用,我们使用一个工厂类,ConditionFactory

package luohong;

import java.util.Date;
import java.util.List;

/**
 * 工厂模式
 * 用于创建condition
 */
public class ConditionFactory {
    public static ICondition newInstance(String name, OperationEnum operation, String value){
        return new StringCondition(name, operation, value);
    }

    public static ICondition newInstance(String name, OperationEnum operation, Number value){
        return new NumberCondition(name, operation, value);
    }

    public static ICondition newInstance(String name, OperationEnum operation, Date value){
        return new DateCondition(name, operation, value);
    }

    public static ICondition newInstanceDates(String name, List<Date> values){
        return new DateCondition(name, values);
    }

    public static ICondition newInstanceStrings(String name, List<String> values){
        return new StringCondition(name, values);
    }

    public static ICondition newInstanceNumbers(String name, List<Number> values){
        return new NumberCondition(name, values);
    }
}

最后,我们提供一个聚合类,并且提供用户自定义扩展sql的方式,比如用户可以直接添加age = 15这种扩展,无需了解condition的设计,这个特性是开闭原则的应用点

package luohong;

import java.util.*;

/**
 * 用于解析condition,然后生成whereSql
 */
public class ConditionHandler {
    private Set<ICondition> conditions = new LinkedHashSet<>();
    /**
     * 用户自定义的sql片段,提供一个扩展点,由用户注入动态的condtion条件
     */
    private Set<String> conditionSqls = new LinkedHashSet<>();

    public Set<ICondition> getConditions(){
        return conditions;
    }

    public void addCondition(ICondition condition){
        this.conditions.add(condition);
    }

    public void addConditionSql(String sql){
        this.conditionSqls.add(sql);
    }

    /**
     * 便利所有的condition,然后生成所有一个sql片段
     * @return
     */
    public String genWhereSql(){
        StringBuilder sb = new StringBuilder();
        for(ICondition condition: conditions){
            sb.append(condition.sql());
            sb.append(" AND ");
        }

        for(String conditionSql: conditionSqls){
            sb.append(conditionSql);
            sb.append(" AND ");
        }

        String whereSql = sb.toString();
        if(whereSql != null && !whereSql.equals("")){
            whereSql = whereSql.substring(0, whereSql.length() - 4);
        }

        return whereSql;
    }

    public static void main(String[] args) {
        ConditionHandler requestParamSqlHandler = new ConditionHandler();
        requestParamSqlHandler.addConditionSql("age > 15");
        requestParamSqlHandler.addConditionSql("name = 'luohong'");
        requestParamSqlHandler.addConditionSql("tel = '15013336884'");
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.EQ, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.NEQ, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LT, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LTE, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.GT, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.GTE, "luohong"));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LIKE, "luohong"));

        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.EQ, 100));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LT, 100));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LTE, 100));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.GTE, 100));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.NEQ, 100));

        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.EQ, new Date()));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.NEQ, new Date()));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.GT, new Date()));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.GTE, new Date()));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LT, new Date()));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstance("name", OperationEnum.LTE, 100));

        requestParamSqlHandler.addCondition(ConditionFactory.newInstanceDates("name", Arrays.asList(new Date())));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstanceStrings("name", Arrays.asList("luohong", "xiemeijiao")));
        requestParamSqlHandler.addCondition(ConditionFactory.newInstanceNumbers("name", Arrays.asList(1, 2, 3, 4)));
        System.out.println(requestParamSqlHandler.genWhereSql());
    }
}

程序输出结果

name = 'luohong' 
AND name != 'luohong' 
AND name < 'luohong' 
AND name <= 'luohong' 
AND name > 'luohong' 
AND name >= 'luohong' 
AND name LIKE '%luohong%' 
AND name = 100 
AND name < 100 
AND name <= 100 
AND name >= 100 
AND name != 100 
AND name = '2018-08-23' 
AND name != '2018-08-23' 
AND name > '2018-08-23' 
AND name >= '2018-08-23' 
AND name < '2018-08-23' 
AND name <= 100 
AND name IN ('2018-08-23') 
AND name IN ('luohong','xiemeijiao') 
AND name IN (1,2,3,4) 
AND age > 15 
AND name = 'luohong' 
AND tel = '15013336884'

6.总结

上面的代码虽然简单,但是却使用了<b>策略设计模式</b>,<b>工厂设计模式</b>,以及应用了开闭原则使得扩展性大大提高。

7.扩展性

由于上面的demo仅仅是花了一小点时间制作,还有些不完善的地方,大家有兴趣的可以进行这些方面的思考

  • 支持更多的数据类型扩展,比如Timestame,boolean等
  • 比较类型的扩展,比如between ... and ...
  • AND OR扩展,上面默认都是AND规则,这里面可以继续加入OR的规则
  • sql注入的规避
  • ...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容