SQL语句拼接
在前面的配置中,无论是基本的增删查改,还是关联嵌套,集合嵌套查询,使用的SQL语句都是很基础的,表关系之间的数据操作。翻回自己大一时写的代码,从一开始的SQL语句直接拼接,如:
String sql = "Update user Set username = " + username + ", gender = " + gender + " Where id = " + id;
这样拼接SQL语句,难免会因为粗心而出现漏逗号等不必要的错误,到后来使用string.Format格式化来拼接SQL语句,语句可读性好了很多:
string.Format("Update FoodItem Set FoodName='{0}',FoodStyle='{1}',Price='{2}' where FoodID='{3}'", FoodName, FoodStyle, Price, FoodId);
传统的JDBC除了拼接语句时有那么一点点麻烦外,在根据不同条件动态配置SQL语句方面,一样不太方便。假如我们想查询一张用户表中,性别为“男”,且姓名为“张三”的用户信息,我们很容易写出这样的查询语句:
SELECT * FROM user WHERE gender = '男' AND username = '张三';
这时如果我们想要查询性别为“男”,且城市字段为“广州”的用户信息,我们就不得不重新写一条查询语句:
SELECT * FROM user WHERE gender = '男' AND city = '广州市';
显然这样的代码量会变得很大,如果你不确定一些查询需求的参数,那么就可能要写很多的查询语句来应对不同的查询。
在配置Mapper(SQL映射配置文件)中有较好的解决办法来减少代码量(例如其映射语句的配置据说最多能减少95%的代码量)。因为MyBatis提供了动态配置SQL语句的一些元素,配置过程中依旧踩了很多坑- -、,但是总算是成功配置并完美运行了!这篇日志总结下自己对if,choose以及foreach三个元素的配置,还有<where>标签,都十分的强大!
if 标签 – 选择查找文本
基本用法
看到“if”元素,大家肯定都猜到它的作用是用来做判断了,把“if”标签放到最前面来讲,因为它是最基本,也是程序员们最熟悉的元素之一。没错,它的作用就是条件判断,选择不同的SQL语句执行,来看各例子:
用到的JavaBean,下面的其他示例都是用这个实体类:
public class User implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private int id;
private String username;
private String password;
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private int age;
public User() {
System.out.println("进入目标类无参数构造方法....");
}
// 构造方法初始化
public User(String username, String password, String gender, String email, String province,
String city, Date birthday) {
super();
System.out.println("进入目标类有参数构造方法....");
this.username = username;
this.password = password;
this.gender = gender;
this.email = email;
this.province = province;
this.city = city;
this.birthday = birthday;
}
// 省略get()和set()方法
}
SQL映射配置文件:
<select id="UserActiveSearch_if" parameterType="User" resultType="User">
SELECT
U.username,
U.gender,
U.email,
U.city
FROM User U WHERE gender = '男'
<if test="username != null">
AND U.username like #{username}
</if>
</select>
上面的SQL语句配置中,传入参数和映射类型都是User实体类,可以看到,我们要查询表中性别为“男”的用户的信息(用户名,性别,电子邮件和城市),且当传入的参数中的username熟悉不为空时,我们把这个username也作为查询条件之一,也就是我们变成查询数据表中,性别为“男”且姓名为传入的“username”的用户信息。如果传入参数User类实例对象中username为null,则不把if标签中的条件作为查询条件,可以写个简单的测试用例看看运行的结果:
@Test
// if表达式
public void TestActiveIf() throws IOException {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
User newUser = new User();
newUser.setUsername("张三");
List<User> resultList = sqlSession.selectList("UserActiveSearch_if", newUser);
for(int i=0; i<resultList.size(); i++) {
User user = resultList.get(i);
result.append("用户名: " + user.getUsername() + "\r\n");
result.append("性别: " + user.getGender() + "\r\n");
result.append("电子邮箱: " + user.getEmail() + "\r\n");
result.append("城市: " + user.getCity() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
测试用例中,我们实例化一个User实例newUser,并给它添加username属性为“张三”,接着实例对象作为参数传入查询。按照SQL映射配置文件中的select语句配置,我们可以查询得到用户“张三”的部分信息,如电子邮箱,城市等,结果也是这样的:
成功查询得到用户“张三”的部分信息。
如上面所说,那如果我们传入的User实例对象中,username为空,那么if标签里的条件便不成立,查询出来的会是什么样的结果,来看看:
@Test
// if表达式
public void TestActiveIf() throws IOException {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
User newUser = new User();
// newUser.setUsername("张三");
List<User> resultList = sqlSession.selectList("UserActiveSearch_if", newUser);
for(int i=0; i<resultList.size(); i++) {
User user = resultList.get(i);
result.append("用户名: " + user.getUsername() + "\r\n");
result.append("性别: " + user.getGender() + "\r\n");
result.append("电子邮箱: " + user.getEmail() + "\r\n");
result.append("城市: " + user.getCity() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
在上面的测试用例中,我们把设置username为“张三”这一句给注释掉,也就是说传入的User实例对象中,所有属性的值都为null,再来看看运行结果:
从查询结果可以看到,由于没有进入if标签中的条件查询,所以整个查询语句帮我们查询出了数据表中所有的gender为“男”的信息。
当然,我们也可以在SQL语句配置一个select标签对中配置多个if条件判断,如果哪一个if条件不满足,则不会把它作为查询条件。
使用<where>标签对
上面我们说道,可以在一个select标签对中,也就是一条select语句中,配置多个if条件判断,例如这样:
<select id="UserActiveSearch_if2" parameterType="User" resultType="User">
SELECT
U.username,
U.gender,
U.email,
U.city
FROM User U WHERE
<if test="city != null">
U.city = #{city}
</if>
<if test="username != null">
AND U.username = #{username}
</if>
</select>
这样的配置,即使username为空导致其中一个if条件不成立,但另一个if条件成立,如传入的city属性为“广州市”,查询语句也可以正确查询出所有city为“广州市”的用户:
这没问题。接下来我们看一个特殊情况,假设我们在这条SQL配置语句查询中,传入一个里面所有属性都是null的User类实例对象,会出现什么样的情况?
结果是程序会出错!为什么,因为如果我们传入的对象里所有属性都为null,意味着select标签中配置的查询语句,所有的if条件都不会被作为查询条件,那么整条SQL语句就变为:
SELECT U.username, U.gender, U.email, U.city FROM User U WHERE;
没错就是这样,这样的SQL语句肯定是查询错误的。还有另一个不易被发现的隐含危险错误是,假如我们第一个if条件“city != null“不成立,但第二个if条件成立,查询同样会出错,为什么?我们来看看如果第一个if不成立但第二个if成立,整条SQL语句会变成怎样:
SELECT U.username, U.gender, U.email, U.city FROM User U WHERE AND U.username = username;
这样的SQL语句,“WHERE AND”连在了一起,一样是错误的,所以会导致查询失败。那怎么办?解决的方法是使用<where>标签对,来包裹住我们需要用到的if条件判断:
<select id="UserActiveSearch_if" parameterType="User" resultType="User">
SELECT
U.username,
U.gender,
U.email,
U.city
FROM User U
<where>
<if test="city != null">
U.city = #{city}
</if>
<if test="username != null">
AND U.username like #{username}
</if>
</where>
</select>
包裹后,只有当<where>标签对中,至少有一个子元素if标签对判断成立,才会向SQL语句中插入“where”子句,并且自动去掉if标签对中开头的“AND”和“OR”(否则拼接出来的SQL语句又是错误的)。来看看使用<where>标签对后的测试结果,首先是传入参数只给了用户名,看<where>是否帮我们忽略掉了子句前面的“AND”:
结果如我们所料,接着到测试传入一个个属性为null的对象,<where>标签对中没有一个if条件判断成立:
由于没有一个if标签成立,“where”没有被拼接到SQL语句中,整条SQL语句变为:
SELECT U.username, U.gender, U.email, U.city FROM User U
也就是帮我们查询出了数据表中所有的使用户数据。
choose标签 – 多选一
上面的<where>,<if>标签对,配置简单,功能强大,可以完成多个条件的查询,只要是满足条件的<if>标签对,都会被拼接到SQL语句中。接下来这个标签对:choose,when,otherwise。作用是在给出的多个when标签条件语句中,选择其中一个符合的条件作为查询条件,拼接到SQL语句中。它和switch语句类似,choose对应switch,when对应case,otherwise则对应default。来看看它的配置方式:
<select id="UserActiveSearch_choose" parameterType="User" resultMap="UserActiveResultMap">
SELECT
U.username as user_name,
U.gender as user_gender,
U.email as user_email,
U.city as user_city
FROM User U
<where>
<choose>
<when test="username != null and username != ''">
username = #{username}
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND id = 6
</otherwise>
</choose>
</where>
</select>
每一个when标签给出一个条件判断,按照这个例子,如果username不为空,则选择它为查询条件,choose标签对里的when标签,在成功匹配了一个条件后,就会退出choose标签。如果两个when标签中的查询条件不符合,则选择otherwise中的条件作为查询条件。可以写个简单的测试用例看看它的执行:
第一个测试中,给出了username,这没问题,choose会选择它作为查询条件拼接到SQL语句中。接下来看第二个,假如我们给出username和email,且这个email属性不属于该username的用户,看看choose标签会怎么帮我们查询:
可以看到,它放弃了第二个条件查询,在选择了第一个when标签条件username后,下面的email条件被放弃了,正如上面所说,choose是在多个条件中选择其中一个执行。
第三个测试是传入一个个属性为空的实例对象,按照配置,两个when标签中的条件都不符合,那么最后选择otherwise标签中的条件执行,即查询id=6的用户信息:
结果也正是这样的。
foreach标签 – 遍历集合
在C#和Java都中接触过foreach循环语句,对比于普通for循环的优点是,普通for循环必须确定循环执行的次数,而foreach循环则不需要,适合于循环次数不确定的循环遍历。在MyBatis中也有foreach标签,它的功能是便利一个集合,假设我们有这样一个例子,需要查询数据表中,id为1,3和5的用户信息,SQL语句可以这么写:
SELECT * FROM User WHERE id in(1,3,5);
或者
SELECT * FROM User WHERE id=1 or id=3 or id=5;
这么做是可以,不过有一点小缺点是,我们把这些id“集合”硬编码进了SQL语句中,假如我们下次想查询另一组id的用户信息,则不得不改动SQL语句,且如果id较多,SQL语句会变得非常的长。解决的办法就是使用foreach标签,把一组id放到集合中,再把集合作为传入参数,用foreach标签来遍历这个集合,把其中的元素一个一个拿出来作为查询条件,即可大大减少代码量。来看看foreach标签对的配置:
<!-- 拼接 WHERE (id=1 or id=3 or id=5) -->
<select id="UserActive_foreach1" resultMap="UserActiveResultMap">
SELECT
U.id as user_id,
U.username as user_name,
U.gender as user_gender,
U.email as user_email,
U.city as user_city
FROM User U
<where>
<foreach collection="list" item="user_id" open="(" separator="or" close=")">
<!-- 每次遍历时拼接上去的字符串 -->
id=#{user_id}
</foreach>
</where>
</select>
这个示例中,我们要用foreach标签配置以下的SQL语句:
SELECT * FROM User WHERE id in(1,3,5);
在foreach标签对中,指定集合的类型collection=“list”,item是指定每次遍历得到的集合项的对象名。接下来三个属性很重要!open指定拼接前缀,这里等于“(”;separator指定两个遍历对象之间的拼接字符串,例如id1和3之间应该拼接一个都好“,”;而close指定结束遍历时要拼接的字符串。最后在标签里指定每次遍历要做的事情,是id=遍历对象,即以此遍历id=1、id=3,id=5。
SQL映射配置文件配置好后,就可以写个简单的测试用例测试下结果了:
@Test
// foreach表达式
public void TestActiveForeach() throws IOException {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
List<Integer> idNum = new ArrayList<Integer>();
for(int j=1; j<6; j+=2) {
idNum.add(j);
}
List<User> resultList = sqlSession.selectList("UserActive_foreach1", idNum);
for(int i=0; i<resultList.size(); i++) {
User user = resultList.get(i);
result.append("用户id: " + user.getId() + "\r\n");
result.append("用户名: " + user.getUsername() + "\r\n");
result.append("性别: " + user.getGender() + "\r\n");
result.append("电子邮箱: " + user.getEmail() + "\r\n");
result.append("城市: " + user.getCity() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
可以看到,我们初始化了一个Integer泛型集合idNum,然后作为参数传入查询中。
测试结果如我们所愿。掌握foreach标签对,关键是配置好open,separator和close三个属性,我们可以用它配置上面的另一条遍历语句:
SELECT * FROM User WHERE id=1 or id=3 or id=5;
<!-- 拼接 WHERE id in (1,3,5) -->
<select id="UserActive_foreach2" resultMap="UserActiveResultMap">
SELECT
U.id as user_id,
U.username as user_name,
U.gender as user_gender,
U.email as user_email,
U.city as user_city
FROM User U
<where>
<foreach collection="list" item="user_id" open="id in (" separator="," close=")">
<!-- 每次遍历时拼接上去的字符串 -->
#{user_id}
</foreach>
</where>
</select>
很简单,只需把遍历对象之间的拼接字符串改为“or”即可。
完整代码已上传GitHub:
https://github.com/justinzengtm/SSM-Framework