一、代码实例
数据库
CREATE TABLE `tbl_dept` ( `id` int(11) NOT NULL AUTO_INCREMENT, `dept_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of tbl_dept -- ---------------------------- INSERT INTO `tbl_dept` VALUES ('1', '研发部'); INSERT INTO `tbl_dept` VALUES ('2', '测试部'); INSERT INTO `tbl_dept` VALUES ('4', '市场部'); INSERT INTO `tbl_dept` VALUES ('5', '运维部'); CREATE TABLE `tbl_employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `last_name` varchar(255) DEFAULT NULL, `gender` char(1) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `d_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_emp_dept` (`d_id`), CONSTRAINT `fk_emp_dept` FOREIGN KEY (`d_id`) REFERENCES `tbl_dept` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of tbl_employee -- ---------------------------- INSERT INTO `tbl_employee` VALUES ('1', 'xiaole', '0', '2236929736@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('3', 'add', '1', null, '2'); INSERT INTO `tbl_employee` VALUES ('4', 'add', '1', null, '1'); INSERT INTO `tbl_employee` VALUES ('5', 'xiaohong', '1', '222@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('6', 'xiaomei', '0', '4444n@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('7', 'xiaohong1', '1', '2232@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('8', 'xiaomei2', '0', '44444n@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('9', 'xiaohong3', '1', '66666@qq.com', '1'); INSERT INTO `tbl_employee` VALUES ('10', 'xiaomei4', '0', '7777@qq.com', '1');
JAVABEAN
Employee
package com.ztloo.bean; import org.apache.ibatis.type.Alias; @Alias(value = "emp") public class Employee { private Integer id; private String lastName; private String email; private String gender; public Employee(Integer id, String lastName, String email, String gender, Department dept) { super(); this.id = id; this.lastName = lastName; this.email = email; this.gender = gender; this.dept = dept; } private Department dept; public Department getDept() { return dept; } public void setDept(Department dept) { this.dept = dept; } public Employee() { super(); // TODO 自动生成的构造函数存根 } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Employee(Integer id, String lastName, String email, String gender) { super(); this.id = id; this.lastName = lastName; this.email = email; this.gender = gender; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override public String toString() { return "Employee [id=" + id + ", lastName=" + lastName + ", email=" + email + ", gender=" + gender + "]"; } }
Department
package com.ztloo.bean; import java.util.List; public class Department { private Integer id; private String departmentName; private List<Employee> emps; public Department(Integer id) { super(); this.id = id; } public Department(Integer id, String departmentName, List<Employee> emps) { super(); this.id = id; this.departmentName = departmentName; this.emps = emps; } public List<Employee> getEmps() { return emps; } public void setEmps(List<Employee> emps) { this.emps = emps; } public Integer getId() { return id; } public Department() { super(); // TODO 自动生成的构造函数存根 } public void setId(Integer id) { this.id = id; } public String getDepartmentName() { return departmentName; } public void setDepartmentName(String departmentName) { this.departmentName = departmentName; } @Override public String toString() { return "Department [id=" + id + ", departmentName=" + departmentName + "]"; } }
EmloyeeMapper
import java.util.List; import org.apache.ibatis.annotations.Param; import com.ztloo.bean.Employee; public interface EmployeeMapper { public List<Employee> getEmpsTestInnerParameter(Employee employee); //携带了哪个字段查询条件就带上这个字段的值 public List<Employee> getEmpsByConditionIf(Employee employee); public List<Employee> getEmpsByConditionTrim(Employee employee); public List<Employee> getEmpsByConditionChoose(Employee employee); public void dynamicUpdateEmp (Employee employee); //查询员工id'在给定集合中的 public List<Employee> getEmpsByConditionForeach(@Param("ids")List<Integer> ids); public void addEmps(@Param("emps")List<Employee> emps); }
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- 自定义的命名空间,只与xml配置文件发生关系,是为了标识 --> <!-- <mapper namespace="com.ztloo.EmployeeMapper"> --> <!-- 1、让配置文件与接口动态绑定,这里相当于配置文件实现了(com.ztloo.dao.EmployeeMapper)此接口 --> <mapper namespace="com.ztloo.dao.EmployeeMapper"> <!-- 批量保存 :两种方式--> <!--public void addEmps(@Param("emps")List<Employee> emps); --> <!--MySQL下批量保存:可以foreach遍历 mysql支持values(),(),()语法--> <!-- <insert id="addEmps"> insert into tbl_employee(last_name,email,gender,d_id) values <foreach collection="emps" item="emp" separator=","> (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id}) </foreach> </insert> --> <!-- 这种方式需要数据库连接属性allowMultiQueries=true; 这种分号分隔多个sql可以用于其他的批量操作(删除,修改) --> <!-- 推荐这种方式:拆分多条insert语句,效率明显会提高 --> <!-- <include refid="insertColumn"></include> 是引入外部的sql片段可以重复利用。 --> <insert id="addEmps"> <foreach collection="emps" item="emp" separator=";"> insert into tbl_employee(<include refid="insertColumn"></include>) values(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.dept.id}) </foreach> </insert> <!-- public void dynamicUpdateEmp(Employee employee); --> <update id="dynamicUpdateEmp"> <!-- Set标签的使用 --> update tbl_employee <set> <if test="lastName!=null"> last_name=#{lastName}, </if> <if test="email!=null"> email=#{email}, </if> <if test="gender!=null"> gender=#{gender} </if> </set> where id=#{id} <!-- Trim:更新拼串 update tbl_employee <trim prefix="set" suffixOverrides=","> <if test="lastName!=null"> last_name=#{lastName}, </if> <if test="email!=null"> email=#{email}, </if> <if test="gender!=null"> gender=#{gender} </if> </trim> where id=#{id} --> </update> <!-- • if:判断 • choose (when, otherwise):分支选择;带了break的swtich-case 如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个 • trim 字符串截取(where(封装查询条件), set(封装修改条件)) • foreach 遍历集合 --> <!-- 查询员工,要求,携带了哪个字段查询条件就带上这个字段的值 --> <!-- public List<Employee> getEmpsByConditionIf(Employee employee); --> <select id="getEmpsByConditionIf" resultType="emp"> select * from tbl_employee <!-- where --> <where> <!-- test:判断表达式(OGNL) OGNL参照PPT或者官方文档。 c:if test 从参数中取值进行判断 遇见特殊符号应该去写转义字符: &&: --> <if test="id!=null"> id=#{id} </if> <if test="lastName!=null && lastName!="""> and last_name like #{lastName} </if> <if test="email!=null and email.trim()!="""> and email=#{email} </if> <!-- ognl会进行字符串与数字的转换判断 "0"==0 --> <if test="gender==0 or gender==1"> and gender=#{gender} </if> </where> </select> <!--public List<Employee> getEmpsByConditionTrim(Employee employee); --> <select id="getEmpsByConditionTrim" resultType="com.ztloo.bean.Employee"> select * from tbl_employee <!-- 后面多出的and或者or where标签不能解决 prefix="":前缀:trim标签体中是整个字符串拼串 后的结果。 prefix给拼串后的整个字符串加一个前缀 prefixOverrides="": 前缀覆盖: 去掉整个字符串前面多余的字符 suffix="":后缀 suffix给拼串后的整个字符串加一个后缀 suffixOverrides="" 后缀覆盖:去掉整个字符串后面多余的字符 --> <!-- 自定义字符串的截取规则 --> <trim prefix="where" suffixOverrides="and"> <if test="id!=null"> id=#{id} and </if> <if test="lastName!=null && lastName!="""> last_name like #{lastName} and </if> <if test="email!=null and email.trim()!="""> email=#{email} and </if> <!-- ognl会进行字符串与数字的转换判断 "0"==0 --> <if test="gender==0 or gender==1"> gender=#{gender} </if> </trim> </select> <!-- public List<Employee> getEmpsByConditionChoose(Employee employee); --> <select id="getEmpsByConditionChoose" resultType="com.ztloo.bean.Employee"> select * from tbl_employee <where> <!-- 如果带了id就用id查,如果带了lastName就用lastName查;只会进入其中一个 --> <choose> <when test="id!=null"> id=#{id} </when> <when test="lastName!=null"> last_name like #{lastName} </when> <when test="email!=null"> email = #{email} </when> <otherwise> gender = 0 </otherwise> </choose> </where> </select> <!--public List<Employee> getEmpsByConditionForeach(List<Integer> ids); --> <select id="getEmpsByConditionForeach" resultType="com.ztloo.bean.Employee"> select * from tbl_employee <!-- collection:指定要遍历的集合: list类型的参数会特殊处理封装在map中,map的key就叫list item:将当前遍历出的元素赋值给指定的变量 separator:每个元素之间的分隔符 open:遍历出所有结果拼接一个开始的字符 close:遍历出所有结果拼接一个结束的字符 index:索引。遍历list的时候是index就是索引,item就是当前值 遍历map的时候index表示的就是map的key,item就是map的值 #{变量名}就能取出变量的值也就是当前遍历出的元素 --> <foreach collection="ids" item="item_id" separator="," open="where id in(" close=")"> #{item_id} </foreach> </select> <!-- 两个内置参数: 不只是方法传递过来的参数可以被用来判断,取值。。。 mybatis默认还有两个内置参数: _parameter:代表整个参数,相当于给employe对象 单个参数:_parameter就是这个参数 多个参数:参数会被封装为一个map;_parameter就是代表这个map _databaseId:如果配置了databaseIdProvider标签。 _databaseId就是代表当前数据库的别名oracle --> <!--public List<Employee> getEmpsTestInnerParameter(Employee employee); --> <select id="getEmpsTestInnerParameter" resultType="com.ztloo.bean.Employee"> <!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 --> <bind name="_lastName" value="'%'+lastName+'%'"/> select * from tbl_employee <if test="_parameter!=null"> where last_name like #{_lastName} </if> </select> <!-- 抽取可重用的sql片段。方便后面引用 1、sql抽取:经常将要查询的列名,或者插入用的列名抽取出来方便引用 2、include来引用已经抽取的sql: 3、include还可以自定义一些property,sql标签内部就能使用自定义的属性 include-property:取值的正确方式${prop}, #{不能使用这种方式} --> <sql id="insertColumn"> last_name,email,gender,d_id </sql> </mapper>
测试类
// @Test // public void testDynamicSql() throws IOException{ // SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); // SqlSession sqlSession = sqlSessionFactory.openSession(); // try{ // EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class); // //select * from tbl_employee where id=? and last_name like ? // //测试if\where // Employee employee = new Employee(1, "%xiao%", null, null); // // List<Employee> emps = mapper.getEmpsByConditionIf(employee ); // for (Employee emp : emps) { // System.out.println(emp); // } //查询的时候如果某些条件没带可能sql拼装会有问题 //1、给where后面加上1=1,以后的条件都and xxx. //2、mybatis使用where标签来将所有的查询条件包括在内。mybatis就会将where标签中拼装的sql,多出来的and或者or去掉 //where只会去掉第一个多出来的and或者or。 //测试Trim // List<Employee> emps2 = mapper.getEmpsByConditionTrim(employee); // for (Employee emp : emps2) { // System.out.println(emp); // } //测试choose // List<Employee> list = mapper.getEmpsByConditionChoose(employee); // for (Employee emp : list) { // System.out.println(emp); // } //测试set标签 // mapper.dynamicUpdateEmp(employee); // sqlSession.commit(); // List<Employee> list = mapper.getEmpsByConditionForeach(Arrays.asList(1,3)); // for (Employee emp : list) { // System.out.println(emp); // } // // }finally{ // sqlSession.close(); // } // } // @Test // public void testBatchSave() throws IOException{ // SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); // SqlSession sqlSession = sqlSessionFactory.openSession(); // try{ // EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class); // List<Employee> emps = new ArrayList<>(); // emps.add(new Employee(null, "xiaohong3", "66666@qq.com", "1",new Department(1))); // emps.add(new Employee(null, "xiaomei4", "7777@qq.com", "0",new Department(1))); // mapper.addEmps(emps); // sqlSession.commit(); // }finally{ // sqlSession.close(); // } // } @Test public void testInnerParam() throws IOException{ SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); try{ EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class); Employee employee2 = new Employee(); employee2.setLastName("e"); List<Employee> list = mapper.getEmpsTestInnerParameter(employee2); for (Employee employee : list) { System.out.println(employee); } }finally{ sqlSession.close(); } }
二、API详解
动态 SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
通常使用动态 SQL 不可能是独立的一部分,MyBatis 当然使用一种强大的动态 SQL 语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中。
动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多的元素需要来了解。MyBatis 3 大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。
-
if
-
choose (when, otherwise)
-
trim (where, set)
-
foreach
if
动态 SQL 通常要做的事情是有条件地包含 where 子句的一部分。比如:
<select id="findActiveBlogWithTitleLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if></select>
这条语句提供了一个可选的文本查找类型的功能。如果没有传入“title”,那么所有处于“ACTIVE”状态的BLOG都会返回;反之若传入了“title”,那么就会把模糊查找“title”内容的BLOG结果返回(就这个例子而言,细心的读者会发现其中的参数值是可以包含一些掩码或通配符的)。
如果想可选地通过“title”和“author”两个条件搜索该怎么办呢?首先,改变语句的名称让它更具实际意义;然后只要加入另一个条件即可。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if></select>
choose, when, otherwise
有些时候,我们不想用到所有的条件语句,而只想从中择其一二。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
还是上面的例子,但是这次变为提供了“title”就按“title”查找,提供了“author”就按“author”查找,若两者都没有提供,就返回所有符合条件的BLOG(实际情况可能是由管理员按一定策略选出BLOG列表,而不是返回大量无意义的随机结果)。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose></select>
trim, where, set
前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在考虑回到“if”示例,这次我们将“ACTIVE = 1”也设置成动态的条件,看看会发生什么。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if></select>
如果这些条件没有一个能匹配上将会怎样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG WHERE
这会导致查询失败。如果仅仅第二个条件匹配又会怎样?这条 SQL 最终会是这样:
SELECT * FROM BLOG WHERE AND title like ‘someTitle’
这个查询也会失败。这个问题不能简单的用条件句式来解决,如果你也曾经被迫这样写过,那么你很可能从此以后都不想再这样去写了。
MyBatis 有一个简单的处理,这在90%的情况下都会有用。而在不能使用的地方,你可以自定义处理方式来令其正常工作。一处简单的修改就能得到想要的效果:
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG <where> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </where></select>
where 元素知道只有在一个以上的if条件有值的情况下才去插入“WHERE”子句。而且,若最后的内容是“AND”或“OR”开头的,where 元素也知道如何将他们去除。
如果 where 元素没有按正常套路出牌,我们还是可以通过自定义 trim 元素来定制我们想要的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它带来的结果就是所有在 prefixOverrides 属性中指定的内容将被移除,并且插入 prefix 属性中指定的内容。
类似的用于动态更新语句的解决方案叫做 set。set 元素可以被用于动态包含需要更新的列,而舍去其他的。比如:
<update id="updateAuthorIfNecessary"> update Author <set> <if test="username != null">username=#{username},</if> <if test="password != null">password=#{password},</if> <if test="email != null">email=#{email},</if> <if test="bio != null">bio=#{bio}</if> </set> where id=#{id} </update>
这里,set 元素会动态前置 SET 关键字,同时也会消除无关的逗号,因为用了条件语句之后很可能就会在生成的赋值语句的后面留下这些逗号。
若你对等价的自定义 trim 元素的样子感兴趣,那这就应该是它的真面目:
<trim prefix="SET" suffixOverrides=","> ... </trim>
注意这里我们忽略的是后缀中的值,而又一次附加了前缀中的值。
foreach
动态 SQL 的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:
<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach></select>
foreach 元素的功能是非常强大的,它允许你指定一个集合,声明可以用在元素体内的集合项和索引变量。它也允许你指定开闭匹配的字符串以及在迭代中间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。
注意 你可以将任何可迭代对象(如列表、集合等)和任何的字典或者数组对象传递给foreach作为集合参数。当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者Map.Entry对象的集合)时,index是键,item是值。
到此我们已经完成了涉及 XML 配置文件和 XML 映射文件的讨论。下一部分将详细探讨 Java API,这样才能从已创建的映射中获取最大利益。
bind
bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。比如:
<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern} </select>
Multi-db vendor support
一个配置了“_databaseId”变量的 databaseIdProvider 对于动态代码来说是可用的,这样就可以根据不同的数据库厂商构建特定的语句。比如下面的例子:
<insert id="insert"> <selectKey keyProperty="id" resultType="int" order="BEFORE"> <if test="_databaseId == 'oracle'"> select seq_users.nextval from dual </if> <if test="_databaseId == 'db2'"> select nextval for seq_users from sysibm.sysdummy1" </if> </selectKey> insert into users values (#{id}, #{name}) </insert>
动态 SQL 中可插拔的脚本语言
MyBatis 从 3.2 开始支持可插拔的脚本语言,因此你可以在插入一种语言的驱动(language driver)之后来写基于这种语言的动态 SQL 查询。
可以通过实现下面接口的方式来插入一种语言:
public interface LanguageDriver { ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);}
一旦有了自定义的语言驱动,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:
<typeAliases> <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/></typeAliases><settings> <setting name="defaultScriptingLanguage" value="myLanguage"/></settings>
除了设置默认语言,你也可以针对特殊的语句指定特定语言,这可以通过如下的 lang 属性来完成:
<select id="selectBlog" lang="myLanguage"> SELECT * FROM BLOG </select>
或者在你正在使用的映射中加上注解 @Lang 来完成:
public interface Mapper { @Lang(MyLanguageDriver.class) @Select("SELECT * FROM BLOG") List<Blog> selectBlog();}
注意 可以将 Apache Velocity 作为动态语言来使用,更多细节请参考 MyBatis-Velocity 项目。
你前面看到的所有 xml 标签都是默认 MyBatis 语言提供的,它是由别名为 xml 语言驱动器 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver 驱动的。