本篇篇幅较长,请善用 Ctrl + F 搜索功能。
结尾补充了 MyBatis 中 resultMap 的映射级别。
MyBatis学完也有几天了,总结一下学习到的内容,也算是复习了。
使用MyBatis之前,我们要知道,什么是MyBatis?
MyBatis是apache一个开源的,基于Java的持久层框架。
MyBatis的优点有哪些?
学习简单,提供半自动的关系映射,SQL语句与代码分离。
MyBatis的缺点又有哪些?
要会写SQL语句;每个数据库的SQL语句都多少会有偏差,所以不方便更换数据库。
MyBatis适合什么样的项目?
适合性能要求很高,或者需要变化较多的项目。
要使用MyBatis,得有MyBatis的jar包。
在https://github.com/mybatis/mybatis-3/releases下载mybatis-x.x.x.zip,我用的版本是3.3.2。
压缩包里的mybatis-x.x.x.jar就是我们要的jar包了,.pdf的是帮助文档,lib文件夹里的是MyBatis的依赖包,具体作用自行百度,一起丢到项目的lib的文件夹里全部add build一下就行了。
哦对了,jdbc的jar包请自行准备。
只有jar包还不够,我们还需要xml配置文件。
新建一个Source Folder,将配置文件统一放在里面。为了方便识别,MyBatis的配置文件我们可以命名为“mybatis-config.xml”,配置文件的内容大致是这样的,改改就能用:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="mybatis.properties" /><!-- jdbc配置文件 -->
<typeAliases><!-- 类型别名 -->
<typeAlias type="cn.bwm.pojo.User" alias="User"/><!-- 给指定的类起一个别名 -->
<package name="cn.bwm.pojo" /><!-- 给包里的所有类起一个和类名一样的别名 -->
</typeAliases>
<environments default="test"><!-- 配置环境 ,default选择默认配置-->
<environment id="test"><!-- 环境元素 -->
<transactionManager type="JDBC" /> <!-- 配置事务管理器 -->
<dataSource type="POOLED"><!-- 数据源 -->
<property name="driver" value="${driver}" /><!-- jdbc配置文件对应属性 -->
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers><!-- 映射器 -->
<mapper resource="cn/bwm/dao/IUserMapper.xml" /><!-- xml映射文件 -->
</mappers>
</configuration>
每个标签的具体作用请参考官方文档,中文的,http://www.mybatis.org/mybatis-3/zh/getting-started.html。
最后一步也是举足轻重的一步,配置MyBatis的两个dtd文件。
这两个文件,一个叫“mybatis-3-config.dtd”,另一个叫“mybatis-3-mapper.dtd”,我们可以通过解压缩mybatis-x.x.x.jar,在\org\apache\ibatis\builder\xml 这个目录下找到这两个文件。
我用的是MyEclipse10.6,配置的步骤是:
Windows → Preferences ; 选择XML Catalog ,选择User Specified Entries,单击 Add ; 单击 File System 后选择dtd文件, Key填写 -//mybatis.org//DTD Config 3.0//EN (与MyBatis-config.xml文件投中的 -//mybatis.org//DTD Config 3.0//EN 相同)。
两个dtd文件配置步骤一样,配置完成以后,在MyBatis-config.xml文件和xml映射文件中就可以使用 alt + / 自动联想了。
完成以上步骤以后,MyBatis就算是部署到项目中了,至于如何使用MyBatis,我们还需要准备一个数据库。这个数据库至少要有两张结构简单有主外键关系的表,以及少量数据。在项目中创建与数据库的表对应的实体类,在数据访问层创建实体类对应的接口,并声明抽象方法,例如:
/**
* 老师类
* @author Administrator
*
*/
public class Teacher {
private int id;
private String name;
private Student student;
private List<Student> studentList;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
public List<Student> getStudentList() {
return studentList;
}
public void setStudentList(List<Student> studentList) {
this.studentList = studentList;
}
}
/**
* 学生类
* @author Administrator
*
*/
public class Student {
private int id;
private String name;
private int tid;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getTid() {
return tid;
}
public void setTid(int tid) {
this.tid = tid;
}
}
/**
* Teacher实体类的对应接口
* @author Administrator
*
*/
public interface ITeacherMapper {
/**
* 根据id查询老师
* @param id
* @return Teacher对象
*/
public Teacher queryTeacherById(int id);
}
在接口的同目录下创建同名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">
<mapper namespace="cn.bwm.dao.ITeacherMapper">
<select id="queryTeacherById" parameterType="int" resultType="Teacher">
SELECT `Id`,`Name` FROM `teacher` WHERE `Id` = #{id}
</select>
</mapper>
写完这些代码和配置文件,现在,我们再来简单了解一下MyBatis的核心接口和类:
- SqlSessionFactoryBuilder:提供多个build()方法的重载,只负责构建SqlSessionFactory的对象,只需要使用一次。
- SqlSessionFactory:提供OpenSession()方法的重载,用来创建SqlSession对象,需要使用多次。
- SqlSession:用于执行已映射的SQL语句,在未使用close()方法关闭前可多次使用SQL。
深入了解请查看官方文档,中文的, http://www.mybatis.org/mybatis-3/zh/java-api.html 。
根据三个核心接口和类,我们再写一个用来获取SqlSession对象的工具类:
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
static{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
try {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取SqlSession对象
* @return
*/
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
/**
* 关闭SqlSession对象
* @param sqlSession
*/
public static void closeSqlSession(SqlSession sqlSession){
if (sqlSession != null) {
sqlSession.close();
}
}
}
现在,我们可以写测试代码了:
public class Test {
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Teacher teacher = sqlSession.getMapper(ITeacherMapper.class).queryTeacherById(1);
System.out.println("编号:" + teacher.getId() + "\t姓名:" + teacher.getName());
SqlSessionUtil.closeSqlSession(sqlSession);
}
}
相信大家也看出来了,我们使用MyBatis执行了简单的查询操作,并将结果封装成了一个对象,而这其中的关键就在于与接口同名的xml映射文件。
我们来看一下映射文件中用到了哪些元素:
- mapper标签:映射文件的根节点。 namespace属性:用于区分不同的napper,全局唯一,必须与对应的接口同名。
- select标签:映射查询语句。 id属性:命名空间中唯一的标识符,与接口中的方法名对应。 parameterType:表示传入参数的类型的完全限定名或别名。完全限定名就是 java.long.String 这样的完整路径,别名则是在 mybatis-config.xml 中设置的别名,MyBatis已经为部分Java类型提供了别名。可以省略(省略好像也没有什么影响)。 resultType:查询语句返回结果类型的完全限定名或别名。在这里我写的是 Teacher ,MyBatis就会按照查询的结果的列名和类的属性字段进行匹配映射,一样的就调用set方法进行赋值,所以类的属性要尽量和数据库的字段名一样,否则就要在查询的时候给查询结果起别名。如果想返回 int 或 String ,可以使用MyBatis提供的别名。如果方法想返回一个List集合,这里写List集合里元素的类型。
- #:这就告诉 MyBatis 创建一个预处理语句参数,通过 JDBC,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中。实际上,无论MyBatis的入参是哪种参数类型,都会被放在一个Map中,单参数入参就会有这些情况 基本类型:对应接口中方法的参数名作为 key , 变量值为 value。 对象:对象的属性名作为 key,属性值为 value。 List:默认 list 作为 key,该 list 即为 value。 数组:默认 array 作为 key,该数组即为 value。 Map:键值不变。
我们在查询的时候,不可能只有单参数查询,而parameterType只能写一个类型,这个时候,有三种方法:
- 使用对象作为参数。
- 将参数封装成Map集合。
- 使用@Param注解。
先说第一种,使用对象作为参数。这种方法应该比较适合插入操作,我们这里强行使用一波。
先在接口里写上方法:
/**
* 根据编号和姓名查询老师
* @param t Teacher对象
* @return Teacher对象
*/
public Teacher queryTeacherByIdName(Teacher t);
在xml映射文件里写对应的SQL语句,parameterType写的是参数类型 Teacher,#和#都是 Teacher 类的属性,CONCAT()是MySQL的函数,用来拼接字符串:
<select id="queryTeacherByIdName" parameterType="Teacher" resultType="Teacher">
SELECT `Id`,`Name` FROM `teacher` WHERE `Id` = #{id} AND `Name` LIKE CONCAT('%' , #{name} , '%')
</select>
测试代码是这样的:
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Teacher t = new Teacher();
t.setId(1);
t.setName("小");
Teacher teacher = sqlSession.getMapper(ITeacherMapper.class).queryTeacherByIdName(t);
System.out.println("编号:" + teacher.getId() + "\t姓名:" + teacher.getName());
SqlSessionUtil.closeSqlSession(sqlSession);
}
这样便完成了使用对象作为参数来进行查询的操作。
然后是第二种,将参数封装成Map集合,我们修改一下刚才的方法,把参数改成 Map<String , Object>集合:
/**
* 根据编号和姓名查询老师
* @param map Map<String,Object>集合
* @return Teacher对象
*/
public Teacher queryTeacherByIdName(Map<String,Object> map);
xml映射文件里,parameterType改成了 map,是MyBatis提供的别名:
<select id="queryTeacherByIdName" parameterType="map" resultType="Teacher">
SELECT `Id`,`Name` FROM `teacher` WHERE `Id` = #{id} AND `Name` LIKE CONCAT('%' , #{name} , '%')
</select>
测试代码把刚才作为参数的 Teacher 对象改成 Map<String , Object>集合:
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Map<String,Object> map = new HashMap<String,Object>();
map.put("id", 1);
map.put("name", "小");
Teacher teacher = sqlSession.getMapper(ITeacherMapper.class).queryTeacherByIdName(map);
System.out.println("编号:" + teacher.getId() + "\t姓名:" + teacher.getName());
SqlSessionUtil.closeSqlSession(sqlSession);
}
这样就完成了使用 Map 传递多个参数。
最后是第三种,使用@Param注解,还是直接修改刚才的方法,把 Map<String , Object> 换成 @Param 注解的参数:
/**
* 根据编号和姓名查询老师
* @param id 编号
* @param name 姓名
* @return Teacher 对象
*/
public Teacher queryTeacherByIdName(@Param("tid")int id , @Param("tname")String name);
xml文件里,因为@Param("")里写的是 tid 和 tname,所以 #{} 也要写的一样:
<select id="queryTeacherByIdName" parameterType="map" resultType="Teacher">
SELECT `Id`,`Name` FROM `teacher` WHERE `Id` = #{tid} AND `Name` LIKE CONCAT('%' , #{tname} , '%')
</select>
测试代码里,参数也不用放在 Map 集合里了:
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Teacher teacher = sqlSession.getMapper(ITeacherMapper.class).queryTeacherByIdName(1 , "小");
System.out.println("编号:" + teacher.getId() + "\t姓名:" + teacher.getName());
SqlSessionUtil.closeSqlSession(sqlSession);
}
这就是 @Param 的使用方法。
三种方法说完了,但不管参数有多少,我们现在查询的结果都只是一个简单对象,如果结果的类型包含另一个类型,或者包含一个集合,这里我们就要了解一下 <resultMap> 元素及其子元素 <result>、<association> 和 <conllection> 。
我们通过代码来讲解这几个个元素的使用方法,我的 Teacher 类中已经有一个 Student 属性和一个 List<Student> 属性:
/**
* 老师类
* @author Administrator
*
*/
public class Teacher {
private int id;
private String name;
private Student student; //学生对象
private List<Student> studentList; //学生集合
//省略 getset 方法
}
接口里的方法可以不用修改,xml映射文件需要大改一下:
<select id="queryTeacherByIdName" resultMap="query">
select t.`Id` as tid , t.`Name`as tname , s.`Id` as sid , s.`Name` as sname from `teacher` as t
inner join `student` as s on s.`Tid` = t.`Id` where t.`Id` = #{tid}
</select>
<resultMap type="Teacher" id="query">
<id column="tid" property="id"/>
<result column="tname" property="name"/>
<association property="student" javaType="Student">
<id column="sid" property="id"/>
<result column="sname" property="name"/>
<result column="tid" property="tid"/>
</association>
<collection property="studentList" ofType="Student">
<id column="sid" property="id"/>
<result column="sname" property="name"/>
<result column="tid" property="tid"/>
</collection>
</resultMap>
select 标签的 resultType 属性换成了 resultMap属性,值与 resultMap 标签的 id 一样。
- resultMap标签:用来自定义结果映射,通常在实体类的属性名与数据库里表的字段不一致导致无法自动映射时,以及需要映射复杂对象时使用。要注意的是,resultMap 和 resultType 这两个元素只能使用其中一个,不能同时存在。 id属性:resultMap 的唯一标识。 type属性:表示该 resultMap 的映射结果类型。
- id 标签:用来标记主键,可以提高整体性能。 column 属性:对应SQL语句查询结果的字段名。 property 属性:对应要赋值的属性。
- result 标签:标志简单属性。
- association 标签:用来映射 JavaBean 的某个 复杂类型 属性,仅处理一对一的关联关系。 javaType 属性:完整 Java 类名或者别名。
- collection 元素:映射 JavaBean 的某个 复杂类型 的集合属性,用来处理一对多的关联关系。 ofType 属性:完整 Java 的类名或或别名,即集合所包含的类型。
另外,resultMap 结果映射可以复用,我们改一下xml映射文件的代码:
<select id="queryTeacherByIdName" resultMap="query1">
select t.`Id` as tid , t.`Name`as tname , s.`Id` as sid , s.`Name` as sname from `teacher` as t
inner join `student` as s on s.`Tid` = t.`Id` where t.`Id` = #{tid}
</select>
<resultMap type="Student" id="student">
<id column="sid" property="id"/>
<result column="sname" property="name"/>
<result column="tid" property="tid"/>
</resultMap>
<resultMap type="Teacher" id="query1">
<id column="tid" property="id"/>
<result column="tname" property="name"/>
<association resultMap="student" property="student" javaType="Student"/>
</resultMap>
<resultMap type="Teacher" id="query2">
<id column="tid" property="id"/>
<result column="tname" property="name"/>
<collection resultMap="student" property="studentList" ofType="Student"/>
</resultMap>
在代码里,两个 resultMap 里的 association 和 collection 都重用了 query 这个 resultMap ,可以节省不少代码。顺便一提,我在测试时,association 和 collection 无法在一个 resultMap 里重用同一个 resultMap,collection 会失效,最终查询出来的结果,List的元素数量是0,具体原因暂时还没找到。
查询部分算是结束了,接下是比较简单的增删改操作,先在接口里写上对应的方法:
/**
* 增加老师
* @param teacher Teacher对象
* @return
*/
public int addTeacher(Teacher teacher);
/**
* 修改老师
* @param teacher Teacher对象
* @return
*/
public int updateTeacher(Teacher teacher);
/**
* 根据id删除老师
* @param id 老师的编号
* @return
*/
public int deleteTeacher(int id);
xml映射文件里也要使用对应的标签,分别是<insert>、<update>和<delete>:
<insert id="addTeacher" parameterType="Teacher">
INSERT INTO `teacher`(`Name`) VALUES(#{name})
</insert>
<update id="updateTeacher" parameterType="Teacher">
UPDATE `teacher` SET `Name` = #{name} WHERE id = #{id}
</update>
<delete id="deleteTeacher" parameterType="int">
DELETE FROM `teacher` WHERE `Id` = #{id}
</delete>
因为增删改的操作返回的是数据库受影响的行数,所以这个三个标签是没有 resultType 和 resultMap 两个属性的。
测试部分需要增加一点点代码:
/**
* 添加老师
*/
private static void addTeacher() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Teacher teacher = new Teacher();
teacher.setName("小黑白");
int result = sqlSession.getMapper(ITeacherMapper.class).addTeacher(teacher);
if (result > 0) {
sqlSession.commit(); //提交事务
System.out.println("添加成功!");
}else{
System.out.println("添加失败!");
}
SqlSessionUtil.closeSqlSession(sqlSession);
}
/**
* 修改老师
*/
private static void updateTeacher() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
Teacher teacher = new Teacher();
teacher.setId(2);
teacher.setName("小黑");
int result = sqlSession.getMapper(ITeacherMapper.class).updateTeacher(teacher);
if (result > 0) {
sqlSession.commit(); //提交事务
System.out.println("修改成功!");
}else{
System.out.println("修改失败!");
}
SqlSessionUtil.closeSqlSession(sqlSession);
}
/**
* 根据id删除老师
*/
private static void deleteTeacher() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
int result = sqlSession.getMapper(ITeacherMapper.class).deleteTeacher(2);
if (result > 0) {
sqlSession.commit(); //提交事务
System.out.println("删除成功!");
}else{
System.out.println("删除失败!");
}
SqlSessionUtil.closeSqlSession(sqlSession);
}
这三个操作都有一个共同的特点,就是在判断数据库受影响的行数大于0以后,都会调用 SqlSession 的 commit() 方法。
这是因为 MyBatis 在执行增删改操作的时候并没有直接操作数据库,我们可以理解成是在操作一个虚拟的数据库,当我们调用 commit() 方法后,我们所做的操作才会对真正的数据库产生影响。
以上就是使用 MyBatis 对数据库进行增删改查操作的示例,我们不难发现,把 SQL语句写在 xml映射文件里会导致我们无法用代码改变 SQL 语句,显得不够灵活,对此,MyBatis为我们提供了动态SQL。
动态SQL是MyBatis的一个强大的特性,基于 OGNL 的表达式,使我们可以方便的动态改变 SQL 语句。
用于动态SQL的元素有:
- if
- choose(when , otherwise)
- trim(where , set)
- foreach
假设,我们使用 id 和 name 来查询老师,如果 name 没有传入参数,就只用 id 查询老师,这时候就可以使用 if 来进行判断:
<select id="queryTeacher" parameterType="map" resultType="Teacher">
SELECT * FROM `teacher`
WHERE `Id` = #{id}
<if test="name != null and name != ''">
AND `Name` LIKE CONCAT('%' , #{name} , '%')
</if>
</select>
test 属性其他标签也有,用来判断条件。
再假设,id 和 name 只要有其中一个就可以了,按照有条件进行查询,如果没有就查询全部,这时可以使用choose 和 它的子元素 when、otherwise来实现:
<select id="queryTeacher" parameterType="map" resultType="Teacher">
SELECT * FROM `teacher`
WHERE
<choose>
<when test="id != null and id != 0">
`Id` = #{id}
</when>
<when test="name != null and name != ''">
`Name` = #{name}
</when>
<otherwise>
1 = 1
</otherwise>
</choose>
</select>
choose 相当于 Java 中的 switch 语句,从上向下开始判断,只要有 when 的 test 成立,就返回该 when 里的 SQL 语句并跳出 choose,如果所有的 when 都不成立,就返回 otherwise 里的SQL语句。因为是从上向下判断的,所以要注意 SQL 语句的优先顺序。
还有最后的 otherwise 里的 1 = 1,如果没有这个的话,很有可能因为 id 和 name 两个都没有参数而造成 where 后面什么都没有,造成 SQL 语句报错。这时,就需要使用trim元素,它可以灵活的去除多余的关键字:
<select id="queryTeacher" parameterType="map" resultType="Teacher">
SELECT * FROM `theacher`
<trim prefix="WHERE" prefixOverrides="and | or">
<if test="id != null and id != 0">
AND `id` = #{id}
</if>
<if test="name != null and name != ''">
AND `name` = #{name}
</if>
</trim>
</select>
trim 元素会自动识别标签内是否有返回值,有的话就在内容的前面加上 prefix 属性的值 where,并忽略 prefixOverrides 里所包含的内容 and 和 or 。(这部分代码只为了展示 trim 的作用,请忽略代码的意义)
这部分的 trim 的使用等价于 where 标签:
<select id="queryTeacher" parameterType="map" resultType="Teacher">
SELECT * FROM `theacher`
<where>
<if test="id != null and id != 0">
AND `id` = #{id}
</if>
<if test="name != null and name != ''">
AND `name` = #{name}
</if>
</where>
</select>
trim 不仅可以在 SQL 语句前添加内容和忽略内容,也可以在 SQL 语句后追加内容和忽略内容,例如刚才的修改操作:
<update id="updateTeacher" parameterType="Teacher">
UPDATE `teacher` SET `Name` = #{name} WHERE id = #{id}
<trim prefix="Set" suffixOverrides="," suffix="WHERE id = #{id}">
<if test="id != null">
`id` = #{id} ,
</if>
<if test="name != null">
`name` = #{name} ,
</if>
</trim>
</update>
使用 prefix 在 SQL 语句前加上 prefix 的值 Set, 在 SQL 语句的最后加上 suffix 的值 WHERE id = #,忽略 SQL 语句最后的 suffixOverrides 的值 , 。(这段代码只展示了 trim 的作用,请无视不合理的部分)
这部分 trim 的使用等价于 set 标签:
<update id="updateTeacher" parameterType="Teacher">
UPDATE `teacher` SET `Name` = #{name} WHERE id = #{id}
<set>
<if test="id != null">
`id` = #{id} ,
</if>
<if test="name != null">
`name` = #{name} ,
</if>
</set>
WHERE `id` = #{id}
</update>
不过 WHERE 需要自己写。
另外,prefix、perfixOverriders、suffix 和 suffixOverriders四个属性是可以同时使用的,根据时间情况选择。
foreach 元素,用于循环集合,主要用于构建 IN 条件语句的时候使用:
<select id="queryTeacher" resultType="Teacher">
SELECT * FROM `theacher`
WHERE `id` IN
<foreach collection="list" item="id" index="index" open="(" separator="," close=")">
#{id}
</foreach>
</select>
这里我们迭代了 list 集合,collection 属性表示迭代时每个元素的别名;index 表示每次迭代到的位置;open 表示 SQL 语句以什么开始;separator 表示每次迭代直接用什么进行分隔;close 表示 SQL 语句以什么结束;collection 必须指定,对应要遍历的集合。
在遍历 对象 和 数组时,index 表示当前迭代的次数,item 的值是表示当前迭代获取的元素。
遍历 Map 时,index 是键, item 是值。
补充一下resultMap 的映射级别。
我在使用 resultMap 自定义映射结果的时候发现,即使我只在 resultMap 里定义一个 id ,其他的属性也还是会自动映射上去。
这是因为MyBatis 的配置文件中,autoMappingBehavior 的默认级别是 PARTIAL,只会自动映射没有定义嵌套结果集映射的结果集,改成 NONE 就可以取消自动映射,改成FULL的话,会自动映射复杂的结果集,无论是否嵌套。
到这里,关于MyBatis我所学习到的内容就全部结束了。
第一次写博客没有经验,写的很长,也很烂。
感谢耐心看到这里的你,如果对你有所帮助就再好不过了。