MyBatis(六) 动态SQL案例、模糊查询|where、if、trim、foreach标签使用|#{}和${}的区别

十二、动态SQL

交友网站:珍爱网、百合网 筛选心仪对象 性别 年龄 城市 …

电商平台:淘宝、京东 筛选商品(羽毛球拍) 品牌 价格 …

​ 张三的筛选:性别 女 select * from members where gender=‘女’

​ 李四的筛选:性别 女;年龄 18-23 select * from members where gender=‘女’ and age>=18 and age<=23

​ 张三的筛选:年龄;城市 select * from members where and age>=18 and age<=23 and city=’’

​ … …

​ 我仅仅有三个筛选条件,我的不同组合的SQL语句就有7种。那我如果有四个筛选条件呢,就数不清了。

​ 我不可能根据用户的每一种筛选情况,去写一种查询。有没有一种操作,只要一个查询语句,可以满足所有选择条件的查询呢?我的SQL语句,是根据你的筛选条件,来进行动态地改变。这就叫动态SQL。

筛选(不同的人筛选条件不一样,指的是条件的个数不一样,而不是条件的内容不一样)

用户的筛选条件不同,我们完成筛选执行的SQL也不一样。

我们可以通过穷举法来一一的完成不同条件的筛选,但是这种实现思路过于繁琐。

MyBatis就提供了动态SQL的配置方式来实现多条件查询。

12.1 什么是动态SQL

根据查询条件动态完成SQL的拼接

12.2 where、if标签使用案例

案例:心仪对象搜索

​ (性别、年龄范围、所在城市)

12.2.1 创建数据表
create table members(
	member_id int primary key auto_increment,
    member_nick varchar(20) not null unique,
    member_gender char(2) not null,
    member_age int not null,
    member_city varchar(30) not null
);
12.2.2 创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Member {
    private int memberId;
    private String memberNick;
    private String memberGender;
    private int memberAge;
    private String memberCity;
}
12.2.3 创建DAO接口

在DAO接口中定义一个多条件查询的方法

根据前面的学习,我们如果想写一个例如“按昵称查询”的方法,是很简单的

public interface MemberDAO {
    public Member queryMemberByNick(String nick);
}

但是现在,我对member的检索,可能只需要一个参数,也可能需要多个参数,而且多个参数的组合也是多种多样的。

public interface MemberDAO {
    public ??? searchMember( ??? );
}

在多条件查询中,如果查询条件不确定,可以直接使用HashMap作为参数

public interface MemberDAO {
    public List<Member> searchMember(HashMap<String,Object> params);
}

到此,先不写mapper配置文件,先写测试类去调用一下这个方法,直观地看一下是怎么传参的:

    @Test
    public void testSearchMember() {
        HashMap<String,Object> params = new HashMap<String,Object>();
        //我需要哪些条件就写上,不需要的不写就可以了。这里面的条件是可以任意地装的。
        //params.put("gender","女");
        params.put("minAge",18);
        //params.put("maxAge",23);
        params.put("city","郑州");
        
        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMember(params);

        System.out.println(members);
    }

除了使用HashMap作为参数,也可以定义专门用于存放查询条件的实体类作为封装参数

public interface MemberDAO {
    public List<Member> searchMember(Member member);
}

如果我这样写,我传入的member里面包含哪些条件,传入的查询就查询哪些条件,可以。

但是这样写会有一个问题,比如对年龄的筛选,Member类里面只有一个age,这样的话就无法实现“年龄区间”,即minAge和maxAge的筛选了。

那么,我创建一个实体类,MemberSearchCondition,成员查询参数类,在传参的时候传递这个对象的参数,也是可以的:

@Data
public class MemberSearchCondition {
    private String gender;
    private int minAge;
    private int maxAge;
    private String city;
}
public interface MemberDAO {
    public List<Member> searchMember(MemberSearchCondition params);
}

那么使用这种方法时,测试类中该怎么调用:

    @Test
    public void testSearchMember() {
        MemberSearchCondition params2 = new MemberSearchCondition();
        params2.setGender("女");
        params2.setMinAge(18);
        params2.setMaxAge(23);
        params2.setCity("武汉");

        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMember(params2);

        System.out.println(members);
    }

这两种方法各有优缺点

使用HashMap<>

​ 优点:无需单独定义传递查询条件的类

​ 缺点:当向Map中存放参数时,key必须与动态sql保持一致(不能一个是gender,一个是gender1)

创建存放查询条件的实体类

​ 优点:设置参数时无需关注属性名,就用set方法往里面存内容即可,不会有不一致的问题

​ 缺点:需要单独定义一个类来封装参数

12.2.4 映射文件
<mapper namespace="com.wyl.dao.MemberDAO">
    <resultMap id="memberMap" type="Member">
        <id column="member_id" property="memberId"/>
        <result column="member_nick" property="memberNick"/>
        <result column="member_gender" property="memberGender"/>
        <result column="member_age" property="memberAge"/>
        <result column="member_city" property="memberCity"/>
    </resultMap>
    <select id="searchMember" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where ???
    </select>
</mapper>

问题来了,条件查询语句的where后面该怎么写。

我有可能只有一个gender条件,也可能有多个条件,也可能没有gender条件。

我需要从传入的param参数(无论是通过HashMap还是条件查询类)中,判断某个条件是否为空,来判断是否需要查询这个条件。但是在这个地方显然不能写java代码中的if…else…等语句。

但是没关系,我们的映射文件中还有一个标签,标签。

    <select id="searchMember" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where
        <if test="gender != null">
            member_gender=#{gender}
        </if>
    </select>

这里的gender != null中的gender,它是来自于你searchMember(param)方法传入的参数param。如果你的param是HashMap定义的,那么它就是其中的某个key;如果是通过查询条件参数类定义的,那么就是这个类中的某一个属性。

判断其不为空的话,则将标签中的语句,拼接到后面。

注意,member_gender为表中字段名,而等号右边的#{ }中,可以直接写gender,因为这个gender,要么是HashMap中的那个key,要么是条件参数类中的那个属性。所以直接#{gender}就可以获取到。

        where
        <if test="gender != null">
            member_gender = #{gender}
        </if>
        <if test="minAge != null">
            member_age >= #{minAge}
        </if>

同理,我们再加一个条件minAge。

但是出现了一个问题:如果gender不为空,那么这两个语句在拼接的时候,中间要加一个AND。

        where
        <if test="gender != null">
            member_gender = #{gender}
        </if>
        <if test="minAge != null">
            and member_age >= #{minAge}
        </if>

但是,加上and,又会出现一个问题。如果gender为空,你加了and,SQL语句就错了。

对于这个问题的解决策略:

①方法一:

在where后面先加一个恒成立的条件1=1,再在每个需要拼接的语句上都加上and

(即使后面所有条件都没有,你where 1=1的话就是查询所有呗)

        where 1=1
        <if test="gender != null">
            and member_gender = #{gender}
        </if>
        <if test="minAge != null">
            and member_age >= #{minAge}
        </if>

②方法二:

使用标签,你就不用写1=1了

        from members
        <where>
            <if test="gender != null">
                and member_gender = #{gender}
            </if>
            <if test="minAge != null">
                and member_age &gt;= #{minAge}
            </if>
        </where>

标签起到一个功能:如果你这个要拼接的语句本身就是第一个条件,则会自动的把你的and帮你去掉,and就不碍事了。

③方法三:

使用标签,给里面的要拼接的语句加上一个前缀where。

        <trim prefix="where">
            <if test="gender != null">
                and member_gender = #{gender}
            </if>
            <if test="minAge != null">
                and member_age &gt;= #{minAge}
            </if>
            <if test="maxAge != null">
                and member_age &lt;= #{maxAge}
            </if>
            <if test="city != null">
                and member_city = #{city}
            </if>
        </trim>

但是这种写法,并不会自动地把作为第一个条件的拼接语句的and自动去掉,会报错。

我们可以加上一个属性,此处的例子表示,如果你的语句作为第一个条件,它会自动把里面的and或or去掉。

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

到此,有人就会问,你这样写,还不如直接写标签。但是,它有一个好处,就是,它还可以加后缀。

<trim prefix="where" prefixOverrides="and | or" suffix="order by member_age">

当然,你在标签后面写order by member_age也是一样的效果。

对于拼接SQL语句时,小于等于(<=)和大于等于(>=)号会出现的问题:

由于<=和>=会和我们的标签中的尖括号搞混,因此可能会存有隐患,所以我们在写SQL语句的时候,一般不用尖括号的方式去表示大于、小于,而是用这两个符号:

        <if test="maxAge != null">
            and member_age <= #{maxAge}
        </if>
        <if test="minAge != null">
            and member_age >= #{minAge}
        </if>

改为:

        <if test="maxAge != null">
            and member_age &gt;= #{maxAge}	//&gt; 表示大于
        </if>
        <if test="minAge != null">
            and member_age &lt;= #{minAge}	//&lt; 表示小于
        </if>

最终版本:

    <select id="searchMember" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where 1=1
        <if test="gender != null">
            and member_gender = #{gender}
        </if>
        <if test="minAge != null">
            and member_age &gt;= #{minAge}
        </if>
        <if test="maxAge != null">
            and member_age &lt;= #{maxAge}
        </if>
        <if test="city != null">
            and member_city = #{city}
        </if>
    </select>
12.2.5 测试

先在members表中添加若干条数据:

在这里插入图片描述

写测试类:

    @Test
    public void testSearchMember() {
        MemberSearchCondition params = new MemberSearchCondition();
        //params.setGender("女");
        //params.setMinAge(18);
        //params.setMaxAge(23);
        //params.setCity("武汉");

        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMember(params);

        System.out.println(members);
    }

无条件筛选,即查询所有,运行结果:

[]

Process finished with exit code 0

为什么?

因为我的“条件查询参数配置类”,即MemberSearchCondition类中,

@Data
public class MemberSearchCondition {
    private String gender;
    private int minAge;
    private int maxAge;
    private String city;
}

minAge、maxAge均为int类型,它不可能为空。若不赋值,则默认为0。而不存在年龄为0的记录。

我们把int改为Integer,让它作为一个对象类型。

@Data
public class MemberSearchCondition {
    private String gender;
    private Integer minAge;
    private Integer maxAge;
    private String city;
}

当然,你如果不想改为Integer,而仍使用int的话。你把SQL语句中的minAge != null改为minAge != 0就可以了。

改过之后,正常了。

1、查询所有性别为女的

    @Test
    public void testSearchMember() {
        MemberSearchCondition params = new MemberSearchCondition();
        params.setGender("女");
        //params.setMinAge(18);
        //params.setMaxAge(23);
        //params.setCity("武汉");

        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMember(params);

        for (Member m:members){
            System.out.println(m);
        }
    }

运行结果:

Member(memberId=1, memberNick=小丽, memberGender=女, memberAge=19, memberCity=武汉)
Member(memberId=3, memberNick=王阿姨, memberGender=女, memberAge=48, memberCity=武汉)
Member(memberId=4, memberNick=翠花, memberGender=女, memberAge=22, memberCity=宜昌)
Member(memberId=5, memberNick=小花, memberGender=女, memberAge=21, memberCity=宜昌)

Process finished with exit code 0

2、性别为女,最小年龄为18的

        params.setGender("女");
        params.setMinAge(18);

运行结果:

Member(memberId=1, memberNick=小丽, memberGender=女, memberAge=19, memberCity=武汉)
Member(memberId=3, memberNick=王阿姨, memberGender=女, memberAge=48, memberCity=武汉)
Member(memberId=4, memberNick=翠花, memberGender=女, memberAge=22, memberCity=宜昌)
Member(memberId=5, memberNick=小花, memberGender=女, memberAge=21, memberCity=宜昌)

Process finished with exit code 0

3、性别为女,最小年龄为22(大于等于22)的

    params.setGender("女");
    params.setMinAge(22);

运行结果:

Member(memberId=3, memberNick=王阿姨, memberGender=女, memberAge=48, memberCity=武汉)
Member(memberId=4, memberNick=翠花, memberGender=女, memberAge=22, memberCity=宜昌)

Process finished with exit code 0

更多的就不一一测试了。

12.3 foreach标签使用案例

还是刚才的案例,但是现在的需求是:

​ 筛选城市为武汉宜昌的,或者年龄为181921的。

对数据表稍作调整吧:

在这里插入图片描述

12.3.1 在DAO中添加方法
public interface MemberDAO {
    //public List<Member> searchMember(HashMap<String,Object> params);
    public List<Member> searchMember(MemberSearchCondition params);
    public List<Member> searchMemberByCity(List<String> cities);	//根据城市筛选
}
12.3.2 映射文件
    <select id="searchMemberByCity" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members where member_city =
    </select>

肯定要写where member_city如何如何。但是这里能用“ = ”么,不能。因为你要筛选的城市是一个List<>,里面可能有一个武汉还有一个宜昌,也就是说它是一个集合。

所以这里应该用in,in后面跟一个集合。

如果写成固定的SQL语句的话,应该是这样写的:

    <select id="searchMemberByCity" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members where member_city in ('武汉','宜昌')
    </select>

现在的问题是,我怎么把我的集合cities拿出来,并且构造成“ (‘武汉’,‘宜昌’) ”这样的结构呢?

说白了,也就是在SQL中要遍历集合。

使用标签,其中,collection指定集合的类型(此处是list,实际上这个list是被MyBatis取过别名了,它本身是java.util.List),item表示取的是集合中每个对象的什么属性,separator表示每个元素之间用什么符号分隔开(此处要用逗号)。

此时,它会拼成这样的一个结构: 武汉,宜昌

但是武汉前面要加一个前括号,宜昌后面要加一个后括号。

所以,还要加一个open(表示以什么开头),一个close(表示以什么结尾)。

循环多次,每循环一次,取到一个item。

    <select id="searchMemberByCity" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members where member_city in
        <foreach collection="list" item="cityName" separator="," open="(" close=")">
            #{cityName}
        </foreach>
    </select>

这就表示,对集合循环取出其中每个元素的cityName,之间用逗号隔开,并且在开头补上“(”,在结尾加上“)”。

12.3.3 测试
    @Test
    public void testSearchMemberByCity() {
        List<String> cities = new ArrayList<String>();
        cities.add("厦门");
        cities.add("宜昌");
        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMemberByCity(cities);
        for (Member m:members){
            System.out.println(m);
        }
    }

运行结果:

Member(memberId=2, memberNick=小强, memberGender=男, memberAge=22, memberCity=厦门)
Member(memberId=4, memberNick=翠花, memberGender=女, memberAge=22, memberCity=宜昌)
Member(memberId=5, memberNick=小花, memberGender=女, memberAge=21, memberCity=宜昌)

Process finished with exit code 0

可以看到,只查询出了厦门和宜昌的。

十三、模糊查询

根据用户昵称查询用户信息——模糊查询。例如查询“花“,会查询到小花、翠花、花仙子等。

13.1 DAO接口中定义方法

    //根据昵称查询用户信息——模糊查询
    public List<Member> searchMemberByNick(String keyWord);

13.2 映射文件

    <select id="searchMemberByNick" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where member_nick like '%花%'	//等会儿要想办法把花改为我们传入的keyWord
    </select>

SQL知识:

模糊查询用like。

‘%’ 表示任意字符(任意一个或任意多个都行);

‘_’ 表示任意一个字符。

13.3 测试

    @Test
    public void testSearchMemberByNick(){
        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);
        List<Member> members = memberDAO.searchMemberByNick("花");
        for (Member m:members){
            System.out.println(m);
        }
    }

由于我是要查询昵称中包含那个字的,所以前后都要加%。然后像我们之前取属性值那样,将#{keyWord}写在两个%%之间。

    <select id="searchMemberByNick" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where member_nick like '%#{keyWord}%'	//等会儿要想办法把花改为我们传入的keyWord
    </select>

这样会报错,不可以这样取:

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='keyWord', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0).
### The error may exist in mappers/MemberMapper.xml

我们换成${keyWord}试一下:

    <select id="searchMemberByNick" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where member_nick like '%${keyWord}%'
    </select>
org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'keyWord' in 'class java.lang.String'
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'keyWord' in 'class java.lang.String'

也会报错,说对于keyWord这个属性,没有get方法,也就是无法获取到keyWord这个属性。

现在我们对DAO中的方法进行一些调整:

    //根据昵称查询用户信息——模糊查询
    public List<Member> searchMemberByNick(HashMap params);

对测试类进行修改:

    @Test
    public void testSearchMemberByNick(){
        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);

        HashMap<String,Object> params = new HashMap<String, Object>();
        params.put("keyWord","花");
        List<Member> members = memberDAO.searchMemberByNick(params);
        for (Member m:members){
            System.out.println(m);
        }
    }

13.4 #{}和${}的区别

${key} 表示获取参数,先获取参数的值拼接到SQL语句中,再编译执行SQL语句,可能引起SQL注入问题。

[先取到 k e y 参 数 的 具 体 的 值 , 然 后 和 {key}参数的具体的值,然后和%%拼接,把'% key{key}%'放到SQL语句里面执行]

#{key}表示获取参数,先完成SQL语句的编译(预编译),预编译之后再将获取的参数设置到SQL语句中,可以避免SQL注入问题。

[预编译是什么:预编译可以理解为就是JDBC里面的占位符,它先告诉你这里有一个参数,先把位置占下来,去编译,编译完成后再对每个位置赋值。这样可以避免SQL注入的问题。]

为什么模糊查询这里不能用#{}:因为’%#{keyWord}%‘这个整体是一个参数,而不是其中的#{keyWord}是一个参数。所以你用’%#{keyWord}%'去编译的时候,%#{}%编译就会出错。占位符不可能是放到引号的里面的。

而你如果直接写#{keyWord},是可以作为一个占位符来编译通过的,但是你就不能完成模糊查询了。

大概就是,不需要用拼接,就用#{},类似一个占位符去完成SQL就可以了,能避免SQL注入。需要用到拼接,就用${}去做拼接处理。

为什么在使用${}写SQL的情况下,DAO方法不使用HashMap params而是直接传一个String keyWord作为参数时,执行测试类会报错,说无法获取到keyWord属性(没有get方法)?(见12.4.3)

    <select id="searchMemberByNick" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where member_nick like '%${keyWord}%'
    </select>
MemberDAO.java
//根据昵称查询用户信息——模糊查询
    public List<Member> searchMemberByNick(String keyWord);

测试类
    @Test
    public void testSearchMemberByNick(){
        MemberDAO memberDAO = MyBatisUtil.getMapper(MemberDAO.class);

        List<Member> members = memberDAO.searchMemberByNick("花");
        for (Member m:members){
            System.out.println(m);
        }
    }

会报错说没有keyWord的get方法。

因为,如果你用${}去取值,它并不是直接取参数值,而是会把你传递的参数看成一个对象,从这个对象中获取这个属性。而我现在传的只是一个字符串。

此时你要去声明一个parameterType,为java.lang.String。如果你不声明,它会把你传递的参数当成一个对象,从对象里面取属性。所以,如果你只是传一个字符串,你要直接声明是字符串。

如果你本身就是一个字符串了,你直接声明我的类型就是字符串。

    <select id="searchMemberByNick" parameterType="java.lang.String" resultMap="memberMap">
        select member_id,member_nick,member_gender,member_age,member_city
        from members
        where member_nick like '%${keyWord}%'
    </select>

而且在DAO的方法中要对参数声明@Param

    //根据昵称查询用户信息——模糊查询
    public List<Member> searchMemberByNick(@Param("keyWord") String keyWord);

非String的对象参数可以不用声明@Param。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋秋秋叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值