Mybatis 框架如何实现 动态 SQL 呢?

# Mybatis 框架如何实现 动态 SQL 呢?

MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。它借助ognl(类似于jsp里面的el表达式)表达式来完成动态sql的拼接使得非常简便。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WA1yIqE7-1602664694026)(https://imgkr.cn-bj.ufileos.com/768074eb-5e03-4ad1-81ee-5432b6614e4a.jpg)]

## 实习 动态 SQL 的方式

- if条件判断

- choose, when, otherwise 选择器使用

- trim, where, set

- foreach

- 使用Ognl表达式


## 案例实操

### if条件判断

动态 SQL 通常要做的事情是有条件地包含 where 子句的一部分。比如:

~~~ xml

<!-- 模糊匹配 -->   

<select id="queryUserByUserName" parameterType="string" resultType="user">   

    select id,userName,userPwd from user  where 1=1 

    <if test="userName!=null and userName!=''">     

        and userName like '%#{userName}%'     

    </if>

</select>

~~~

使用if标签就是加一个test属性作为判断, 如果有多个条件组合判断的话用and, or连接

实现方法

~~~ java

@Override   

public List<User> queryUserByUserName(String userName) {

    List<User> users=null;     

    SqlSession session=null;   

    try {         

        session=sqlSessionFactory.openSession();     

        Map map=new HashMap();

        //map 参数         

        map.put("userName",userName);

        users=session.selectList("com.xxx.mapper.UserMapper.queryUserByUserName", map);       

    } catch (Exception e) {     

        e.printStackTrace();   

    }finally{           

        if(null!=session){   

            session.close();     

        }           

    }     

    return users; 

}

~~~

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wogGoZ1q-1602664694031)(https://imgkr2.cn-bj.ufileos.com/6c5fe387-492c-4721-89aa-c8560f6a528f.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=bC8cFRwCZAEavYH%252FBpq18iDwJ1Y%253D&Expires=1596378864)]

运行结果, sql自动判断并且拼接上了

### choose, when, otherwise 选择器使用

我们不想用到所有的条件语句,而只想从中择其一二。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句

~~~ xml

<select id="queryUserByParams" parameterType="map" resultType="user">       

    select id,userPwd     

    <choose>         

        <when test="nation!=null and nation!=''">     

            ,userName     

        </when>       

        <otherwise>   

            ,realName   

        </otherwise>     

    </choose>       

    from user     

    where userName like '%${userName}%' 

    <if test="phone!=null and phone!=''"> 

        and phone like '%${phone}%'   

    </if>

</select>

~~~

这条语句的意思就是说 如果我传进nation不为空就查userName的值, 否则是realName的值

~~~ java

@Test   

public void test16(){ 

    UserDao userDao=new UserDaoImpl(sqlSessionFactory);   

    List<User> list=userDao.queryUserByParams("", null, "xxx");

    for(User u:list){   

        System.out.println(u); 

    } 

}

~~~

### trim, where, set

前面几个例子已经适宜地解决了一个臭名昭著的动态 SQL 问题, 然后我们再来看第一条的配置

~~~ xml

<select id="findUserByUserName" resultMap="RM_User" >

    select

    userId, userName, password

    from

  user

    where

    userName like '%${userName}%'

    <if test="phone != null and phone != ''" >

        and phone like '%${phone}%'

    </if>

</select>

~~~

如果我把 userName like '%${userName}%'这个语句也用if做个判断

~~~ xml

<select id="findUserByUserName" resultMap="RM_User" >

    select

    userId, userName, password

    from

    user       

    where  

    <if test="userName != null and userName != ''" >

        userName like '%${userName}%'  

    </if>    

    <if test="phone != null and phone != ''" >

        and phone like '%${phone}%'

    </if>

</select>

~~~

这样的话我们预测一下 打印的sql应该是

~~~ sql

select userId, userName, password from user where

~~~

很明显这条sql会报错

那为了解决这个问题呢, 我们使用\<where>\</where>标签

~~~ xml

<select id="queryUserByParams" parameterType="map" resultType="user">

    select

    id,userPwd,phone

    <choose> 

        <when test="nation!=null and nation!=''"> 

            ,userName 

        </when> 

        <otherwise>   

            ,realName 

        </otherwise> 

    </choose>from user<where> 

    <if test="userName !=null and userName !=''">   

        userName like '%${userName}%' 

    </if> 

    <if test="phone!=null and phone!=''">   

        and phone like '%${phone}%'   

    </if>

    </where>

</select>

~~~

编写测试类

~~~ java

@Test 

public void test16(){   

    UserDao userDao=new UserDaoImpl(sqlSessionFactory); 

    List<User> list=userDao.queryUserByParams("", "", "");   

    for(User u:list){     

        System.out.println(u); 

    } 

}

~~~

where 元素知道只有在一个以上的if条件有值的情况下才去插入“WHERE”子句。而且,若最后的内容是“AND”或“OR”开头的,where 元素也知道如何将他们去除。就像上面的配置如果我phone有值, userName没值的话 where也知道要将phone 前面的and去掉

但如果 where 元素没有按正常套路出牌,我们还是可以通过自定义 trim 元素来定制我们想要的功能。比如,和 where 元素等价的自定义 trim 元素为:

~~~ xml

<select id="queryUserByParams" parameterType="map" resultType="user">   

    select

    id,userPwd,phone     

    <choose>     

        <when test="nation!=null and nation!=''">     

            ,userName   

        </when>     

        <otherwise>         

            ,realName       

        </otherwise>         

    </choose>     

    from user     

    <trim prefix="where" prefixOverrides="and |or" >   

        <if test="userName !=null and userName !=''"> 

            userName like '%${userName}%' 

        </if>       

        <if test="phone!=null and phone!=''">   

            and phone like '%${phone}%'       

        </if>                 

    </trim>           

</select>

~~~

这样的效果跟\<where>\</where>效果是一样的

prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它带来的结果就是所有在 prefixOverrides 属性中指定的内容将被移除,并且插入 prefix 属性中指定的内容。

对于update语句, 我们采用\<set>\</set>去设置值

~~~ xml

<update id="updateUserById" parameterType="user">   

    update user         

    <set>     

        <if test="userName!=null"> 

            userName=#{userName},     

        </if>     

        <if test="userPwd!=null"> 

            userPwd=#{userPwd},   

        </if>     

    </set>   

    where id=#{id}

</update>

~~~

编写测试方法

~~~ java

@Test 

public void test17(){ 

    UserDao userDao=new UserDaoImpl(sqlSessionFactory);

    User user=userDao.queryUserById(6);     

    user.setUserPwd(null);   

    user.setUserName("xxx06"); 

    userDao.updateUserById(user);

}

~~~

若你对等价的自定义 trim 元素的样子感兴趣,那这就应该是它的真面目:

~~~ xml

<update id="updateUserById" parameterType="user">     

    update user     

    <trim prefix="set" suffixOverrides="," > <!-- 此时使用后缀消除, -->     

        <if test="userName!=null">     

            userName=#{userName},     

        </if>   

        <if test="userPwd!=null">   

            userPwd=#{userPwd},   

        </if>       

    </trim>       

    where id=#{id}

</update>

~~~

这个效果和set是一致的

### foreach

动态 SQL 的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建 IN 条件语句或者是批量插入。比如:

~~~ xml

<select id="findUserByUserName" resultMap="RM_User" >

    select

    userId, userName, password

    from

    user

    <where>

        <if test="userNameList != null" >

            userName in

            <foreach item="item" index="index" collection="userNameList"open="(" separator="," close=")">

                #{item}

            </foreach>

        </if>

    </where>

</select>

~~~

编写测试方法

~~~ java

@Test

public void testFindUserByUserName() {

    InputStream is = MybatisSecondaryCacheTest.class.getClassLoader().getResourceAsStream("mybatis.xml"); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);

    SqlSession session = sessionFactory.openSession();

    // 创建参数

    Map<String, Object> params = new HashMap<>();

    // 创建以string数组然后转化成list

    String[] userName = new String[]{"Tonygogo", "hello", "哈哈哈"};     params.put("userNameList", Arrays.asList(userName));

    // string数组转list, key的名称要与映射文件中的变量名要一直

    List<User> users = session.selectList("findUserByUserName", params); System.out.println("查询结果: " + users.toString());

}

~~~

### 使用Ognl表达式

我们在上面的映射中, 如果用if去判断一个值是否为空或者是空字符串时我们是这样做的test="userName != null and userName !='' "这样写起来比较复杂, 为此我们采用Ognl表达式*@Ognl@isNotEmpty(**userName**)*去判断。

使用ognl表达式时我们要在根目录的包下面加上Ognl的一个Java类, 这里面会有各种各样的判断比如为空判断*@Ognl@isEmpty(**userName**)*,不为空判断 *@Ognl@is**Not**Empty(**userName**)**,* 是否是空字符串*@Ognl@isBlank(**userName**)**,* 不为空字符串*@Ognl@is**Not**Blank(**userName**)**等等*

我们常用的可能就是这四个,它只是方便我们去做一些操作,实际中也会用到

~~~ java

import java.lang.reflect.Array;

import java.util.Collection;import java.util.Map; 

/**

* Ognl工具类,主要是为了在ognl表达式访问静态方法时可以减少长长的类名称编写

* Ognl访问静态方法的表达式为: @class@method(args)

* 示例使用: 

* <pre>

*  <if test="@Ognl@isNotEmpty(userId)">

*      and user_id = #{userId}

*  </if>

* </pre>

*

*/

public class Ognl {       

    /**   

    * 可以用于判断String,Map,Collection,Array是否为空   

    * @param o   

    * @return   

    */   

    @SuppressWarnings("rawtypes")   

    public static boolean isEmpty(Object o) throws IllegalArgumentException {       

        if(o == null) return true;       

        if(o instanceof String) {           

            if(((String)o).length() == 0){         

                return true;         

            }     

        } else if(o instanceof Collection) {   

            if(((Collection)o).isEmpty()){   

                return true;     

            }       

        } else if(o.getClass().isArray()) { 

            if(Array.getLength(o) == 0){     

                return true;       

            }     

        } else if(o instanceof Map) { 

            if(((Map)o).isEmpty()){   

                return true;     

            }     

        }else {   

            return false;

            //          throw new IllegalArgumentException("Illegal argument type,must be : Map,Collection,Array,String. but was:"+o.getClass());     

        }     

        return false; 

    }       

    /**   

    * 可以用于判断 Map,Collection,String,Array是否不为空   

    * @param c 

    * @return 

    */   

    public static boolean isNotEmpty(Object o) {   

        return !isEmpty(o); 

    }     

    public static boolean isNotBlank(Object o) { 

        return !isBlank(o); 

    }   

    public static boolean isBlank(Object o) {   

        if(o == null)     

            return true;   

        if(o instanceof String) {   

            String str = (String)o;     

            return isBlank(str);   

        }     

        return false;

    }   

    public static boolean isBlank(String str) {   

        if(str == null || str.length() == 0) { 

            return true;     

        }         

        for (int i = 0; i < str.length(); i++) {   

            if (!Character.isWhitespace(str.charAt(i))) { 

                return false;     

            }   

        }   

        return true;

    }

}

~~~

## 扩展

### 注解形式动态sql

除了xml 配置能够支持动态 sql 外,MyBatis提供了各种注解如@InsertProvider,@UpdateProvider,@DeleteProvider和@SelectProvider,来帮助构建动态SQL语句,然后让MyBatis执行这些SQL语句。

~~~ java

public interface AccountDao {

​    /**

​    \* 添加账户记录 

​    \*  添加字符串sql由AccountProvider 类addAccount方法提供

​    \*  返回影响行数

​    \* @param account

​    \* @return

​    */

​    @InsertProvider(method="addAccount",type=AccountProvider.class)

​    public int  addAcccount(Account account);

​   

​    /**

​    \* 添加账户记录 

​    \*  添加字符串sql由AccountProvider 类addAccount方法提供

​    \* 返回主键

​    \* @param account

​    \* @return

​    */

​    @InsertProvider(method="addAccount",type=AccountProvider.class)

​    @Options(useGeneratedKeys=true,keyColumn="id")

​    public int  addAcccount02(Account account);

​   

​    /**

​    \* 根据id查询账户记录 

​    \*  查询字符串sql由AccountProvider 类queryAccountById方法提供

​    \* @param id

​    \* @return

​    */

​    @SelectProvider(method="queryAccountById",type=AccountProvider.class)

​    public Account queryAccountById(@Param("id")int id);

​   

​    /**

​    \* 多条件查询账户记录

​    \*  查询字符串sql由AccountProvider 类queryAccountByParams方法提供

​    \* @param aname

​    \* @param type

​    \* @param time

​    \* @return

​    */

​    @SelectProvider(method="queryAccountByParams",type=AccountProvider.class)

​    public List<Account> queryAccountByParams(@Param("aname")String aname,@Param("type")String type,@Param("time")String time);

​   

​    /**

​    \* 更新账户记录

​    \*  更新字符串sql由AccountProvider 类updateAccountById方法提供

​    \* @param account

​    \* @return

​    */

​    @UpdateProvider(method="updateAccount",type=AccountProvider.class)

​    public int updateAccountById(Account account);

​   

​    /**

​    \* 根据id删除账户记录

​    \*  删除字符串sql由AccountProvider 类deleteAccount方法提供

​    \* @param id

​    \* @return

​    */

​    @DeleteProvider(method="deleteAccount",type=AccountProvider.class)

​    public int deleteAccountById(@Param("id")int id);

}

public class AccountProvider {

​    /**

​    \* 返回添加账户记录sql字符串

​    \* @param account

​    \* @return

​    */

​    public String addAccount(final Account account){

​        return new SQL(){{

​            INSERT_INTO("account");

​            VALUES("aname","#{aname}");

​            VALUES("type", "#{type}");

​            VALUES("remark","#{remark}");

​            VALUES("money", "#{money}");

​            VALUES("user_id", "#{userId}");

​            VALUES("create_time","#{createTime}");

​            VALUES("update_time", "#{updateTime}");

​        }}.toString();

​    }

​   

​    /**

​    \* 返回根据id查询账户记录sql字符串

​    \* @param id

​    \* @return

​    */

​    public String queryAccountById(@Param("id")int id){

​        return new SQL(){{

​            SELECT("id,aname,type,remark,create_time as createTime,update_time as updateTime,user_id as userId");

​            FROM("account");

​            WHERE(" id=#{id} ");

​        }}.toString();

​    }

​   

​    /**

​    \* 返回多条件查询sql字符串

​    \* @param aname

​    \* @param type

​    \* @param time

​    \* @return

​    */

​    public String queryAccountByParams(@Param("aname") final String aname,@Param("type")final String type,@Param("time")final String time){

​        String sql= new SQL(){{

​            SELECT("id,aname,type,remark,create_time as createTime,update_time as updateTime,user_id as userId");

​            FROM("account");

​            WHERE(" 1=1 ");

​            if(!StringUtils.isNullOrEmpty(aname)){

​                AND();

​                WHERE(" aname like concat('%',#{aname},'%') ");

​            }

​            if(!StringUtils.isNullOrEmpty(type)){

​                AND();

​                WHERE(" type =#{type}");

​            }

​            if(!StringUtils.isNullOrEmpty(time)){

​                AND();

​                WHERE(" create_time <=#{time}");

​            }

​        }}.toString();

​        return sql;

​    }

​   

​    /**

​    \* 返回更新账户记录sql字符串

​    \* @param account

​    \* @return

​    */

​    public String updateAccount(Account account){

​        return new SQL(){{

​              UPDATE(" account");

​              SET("aname=#{aname}");

​              SET("type=#{type}");

​              WHERE("id=#{id}");

​        }}.toString();

​    }

​   

​    /**

​    \* 返回删除账户记录sql字符串

​    \* @param id

​    \* @return

​    */

​    public String deleteAccount(@Param("id")int id){

​        return new SQL(){{

​            DELETE_FROM("account");

​            WHERE("id=#{id}");

​        }}.toString();

​    }

}

~~~

;

​    }

​   

​    /**

​    \* 返回更新账户记录sql字符串

​    \* @param account

​    \* @return

​    */

​    public String updateAccount(Account account){

​        return new SQL(){{

​              UPDATE(" account");

​              SET("aname=#{aname}");

​              SET("type=#{type}");

​              WHERE("id=#{id}");

​        }}.toString();

​    }

​   

​    /**

​    \* 返回删除账户记录sql字符串

​    \* @param id

​    \* @return

​    */

​    public String deleteAccount(@Param("id")int id){

​        return new SQL(){{

​            DELETE_FROM("account");

​            WHERE("id=#{id}");

​        }}.toString();

​    }

}

~~~

需要视频配套文档或更多资料+我们程序员小姐姐v:lezijie007(加好友时备注:b站-LT,不备注拒绝添加哟)

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