一、前言
本篇内容以理解MyBatis的基本用法和快速在项目中实践为目的,遵循Make it work,better and excellent原则。 技术栈为MyBatis3.2.7+log4j1.2.17+sqlite3+jdk1.7。
二、示例
示例代码功能:
学校人员管理系统,对象分别为学生、教师和班级,学生与班级为多对一关系,班级与教师为一对多关系。示例代码主要为操作学生对象。
1. 表结构
学生表(TStudentML)
字段 | 数据类型 | 备注 |
ID | Integer | 主键 |
Name | Text | 姓名 |
ClassID | Integer | 班级ID |
班级表(TClassML)
字段 | 数据类型 | 备注 |
ID | Integer | 主键 |
Name | Text | 姓名 |
教师表(TTeacherML)
字段 | 数据类型 | 备注 |
ID | Integer | 主键 |
Name | Text | 姓名 |
ClassID | Integer | 班级ID |
2. 项目目录结构
src |-- com | |-- entity | | |-- ETeacher | | |-- EStudent | | |-- EClass | |-- dao | |-- studentMapper.xml | |-- StudentMapper.java | |-- DBase.java | |-- DStudent.java |-- mybatis-config.xml |-- db.db |-- log4j.properties |-- config.properties
3. 数据库配置文件(config.properties)
driver=org.sqlite.JDBC
url=jdbc:sqlite:src/db.db
4. log4j.properties
log4j.rootLogger=TRACE, studout log4j.appender.studout=org.apache.log4j.ConsoleAppender log4j.appender.studout.layout=org.apache.log4j.PatternLayout log4j.appender.studout.layout.ConversionPattern=[%5p]%t -%m%n
5. 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配置文件,后面内容则通过${属性名}来引用属性值
在使用实例化SqlSessionFactory时,还可以通过new SqlSessionFactoryBuilder.build(config.xml的InputStream实例, Properties实例)来设置属性值。
优先级从高到低是:
1. 通过build方法入参设置
2. 通过resource引入的属性
3. 通过property标签设置的属性
--> <properties resource="config.properties"></properties>
<!--强制指定MyBatis使用log4j作为日志日志框架,若不指定那么当部署到如Tomcat等应用容器时,会被容器设置为使用common-logging来记录日志--> <settings> <setting name="logImpl" value="LOG4J"/> </settings>
<!--设置自定义JAVA类型的别名,否则在映射文件中的resultType、parameterType等特性值就需要填写全限定类名才有效--> <typeAliases>
<!--
这时对于包下的类,在映射文件中的resultType、parameterType等特性值,我们只需写类名或首字母小写的类名
当自定义JAVA类配合@Aliase("别名")使用时,只需写别名即可,且不区分大小写
MyBatis对JAVA原生类型定义了内置别名:
`int`,`long`等就是`_int`,`_long`
`Integer`,`String`就是`int`,`string`
--> <package name="com.entity"/> </typeAliases> <environments default="dev">
<!--运行环境配置--> <environment id="dev">
<!--
type属性用于指定事务管理器类型
JDBC:使用JDBC的提交和回滚设置,依赖从数据源获取的连接来管理事务范围。
MANAGED:让容器(如Spring)来管理事务的生命周期。默认情况会关闭连接,
若不想关闭连接则需要如下配置:
<transactionManager type="MANAGED">
<property name="closeConnection" value="false"/>
</transactionManager>
--> <transactionManager type="JDBC"></transactionManager>
<!--
type属性用于指定连接池类型
UNPOOLED:连接用完就关闭,不放到连接池
POOLED:连接用完则放在连接池
--> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <!--虽然sqlite不用填写username和password,但这两个节点必须保留,否则将报错--> <property name="username" value=""/> <property name="password" value=""/> </dataSource> </environment> </environments>
<!--向MyBatis注册映射信息--> <mappers> <mapper resource="com/dao/studentMapper.xml"></mapper> </mappers> </configuration>
6. 实体类
// 教师实体类 public class ETeacher{ private int id; private String name; // 省略各种setter和getter....... } // 班级实体类 public class EClass{ private int id; private String name; // 省略各种setter和getter....... } // 学生实体类 public class EStudent{ private int id; private String name; private EClass myClass; private List<ETeacher> teachers; // 省略各种setter和getter....... }
7. 映射接口
public interface StudentMapper{ /** * 通过姓名获取学生信息(不含教师、班级信息) * @param name 学生姓名 * @return */ EStudent getJustStudentInfo(String name); /** * 通过姓名模糊查询学生信息(不含教师、班级信息) * @param name 学生姓名 * @return */ List<EStudent> getJustStudentInfos(String name); /** * 通过姓名和班级名称获取学生信息(含教师、班级信息) * @param studentName 学生姓名 * @param className 班级名称 * @return */ EStudent getStudentInfo(String studentName, String className); /** * 新增学生信息 * @param student * @return */ void addStudent(EStudent student); /** * 删除学生信息 * @param id * @return */ void delStudent(int id); }
8. 映射文件
<?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="main.dao.StudentMapper"> <select id="getJustStudentInfo" resultType="EStudent"> select ID,Name from TStudentML where Name = #{0} </select> <select id="getJustStudentInfos" resultType="EStudent"> select * from TStudentML <if test="#{0} != null"> where name like '%'+#{0}+'%' </if> </select> <select id="getStudentInfo" resultMap="getStudentInfoResultMap"> select a.ID a_id, a.Name a_name, b.ID b_id, b.Name b_name, c.ID c_id, c.Name c_name, c.ClassID c_classId from
TStudentML a left outer join TClassML b on(a.ClassID=b.ID) left outer join TTeacherML c on(b.ID=c.ClassID) <trim prefix="where" prefixOverrides="and"> <if test="#{0} != null"> a.Name like '%'+#{0}+'%' </if> <if test="#{1} != null"> and b.Name like '%'+#{1}+'%' /if> </trim> </select> <resultMap id="getStudentInfoResultMap" type="EStudent"> <id property="id" column="a_id"/> <result property="name" column="a_name"/>
<!-- 一对一关系 --> <association property="myClass" javaType="EClass"> <id property="id" column="b_id"/> <result property="name" column="b_name"/> </association>
<!-- 一对多关系 --> <collection property="teachers" ofType="ETeacher"> <id property="id" column="c_id"/> <result property="name" column="c_name"/> <result property="classId" column="c_classId"/> </collection> </resultMap> <insert id="addStudent" parameterType="EStudent"> insert into TStudentML(Name, ClassID) values(#{name}, #{myClass.id}) </insert> <delete id="delStudent"> delete from TStudentML where ID = #{0} </delete> </mapper>
9. 数据库操作基类
public class DBase{ private static SqlSessionFactory factory = null; public DBase(){ if (factory != null) return; InputStream is = this.getClass().getClassLoader().getResourceAsStream("mybatis-config.xml"); factory = new SqlSessionFactoryBuilder().build(is); } /** * 获取SqlSession实例,使用后需调用实例的close()函数释放资源 * @return */ protected SqlSession openSession(){ return factory.openSession(); } }
10. 数据库操作类
public class DStudent extends DBase{ /** * 通过姓名获取学生信息(不含教师、班级信息) * @param name 学生姓名 * @return */ public EStudent getJustStudentInfo(String name){ EStudent ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getJustStudentInfo(name); } finally{ session.close(); } return ret; } /** * 通过姓名模糊查询学生信息(不含教师、班级信息) * @param name 学生姓名 * @return */ public List<EStudent> getJustStudentInfos(String name){ List<EStudent> ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getJustStudentInfos(name); } finally{ session.close(); } return ret; } /** * 通过姓名和班级名称获取学生信息(含教师、班级信息) * @param name 学生姓名 * @param className 班级名称 * @return */ public EStudent getStudentInfo(String name, String className){ EStudent ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getStudentInfo(name, className); } finally{ session.close(); } return ret; } /** * 新增学生信息 * @param student */ public void addStudent(EStudent student){ SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); sm.addStudent(student); session.commit(); } finally{ session.close(); } } /** * 删除学生信息 * @param id */ public void delStudent(int id){ SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); sm.delStudent(id); session.commit(); } finally{ session.close(); } } }
三、基础知识
作为一个ORM框架,可以知道其至少由对象模型转换为关系模型、关系模型转换为对象模型和缓存管理这三个模块组成。
MyBatis在对象模型转换为关系模型模块的实现方式是对象模型实例属性+自定义SQL语句,好处是对SQL语句的可操作性高,同时简化SQL入参的处理;坏处是对于简单的单表操作,依旧要写SQL语句,无法由对象模型自动生成SQL,最明显的问题是在开发初期数据表结构不稳定,一旦表结构改了,代码上不仅要改对象模型还要改SQL语句(不过MyBatis也考虑到这点,通过<sql>标签实现SQL语句复用,缓解这样问题)。
关系模型转换为对象模型则采用关系模型结果集字段映射到对象模型实体字段的方式处理。
缓存模块则分为SQL语句缓存和查询数据缓存两种,由于MyBatis需要开发者自定义SQL语句,因此SQL语句缓存不用考虑;而查询数据缓存则被分为一级和二级缓存,一级缓存以事务为作用域,二级缓存以同一个映射集为作用域,而且二级缓存采用直写的方式处理缓存数据被修改和删除的情况。
(本人不才,曾开发轻量级ORM框架LessSQL.Net,由于设计为SQL语句必须由对象模块实例映射生成,而关系模型数据集合无法自动填充任意的对象模型实体中,无法支撑复杂的查询语句,而缓存方面仅实现了SQL语句缓存性能优化有限,因此框架仅适用于小型工具软件。因为踩过这些坑,所以对ORM框架有一点浅薄的认识和看法)
言归正转,我们一起了解MyBatis的基础知识吧。
1. MyBatis框架配置文件
实际上就是MyBatis会话工厂的配置文件,用于配置如缓存、日志框架、数据库链接信息、两种模型间转换的处理器和注册映射集等。通过上文大家应该知道如何Make it work了。而Make it better也是从这里出发。
2. 映射集
映射集是由多个“标识”——“SQL语句”组成,映射记录上还有如入参类型、返回类型等信息,为对象关系模型转换引擎提供元数据。
设置映射集的方式有两种,一种是通过接口,一种通过xml文档。但上文示例采用两者相结合的方式,综合两者优点。
[a]. 映射接口方式
public interface StudentMapper{ @Select("select * from TStudentML where Name=#{0}") EStudent getJustStudent(String name); @Insert("insert into TStudentML(Name, ClassID) values(#{name}, #{myClass.id})") void addStudent(EStudent student); @Delete("delete from TStudentML where ID=#{0}") void delStudent(int id); @Update("update TStudentML set Name=#{name},ClassID=#{myClass.id} where ID=#{id}") void updateStudent(EStudent student); }
很明显通过接口方式定义映射集是无法实现上文中复杂的查询操作,而好处就是代码量锐减,且由于使用了接口,所以IDE的智能提示和编译时语法、依赖关系检查会降低编码错误的风险。
使用接口方式需要将mybatis-config.xml中的mapper改为如下内容
<mappers> <mapper class="com.dao.StudentMapper"></mapper> </mappers>
<!--
或注册某包下的所有映射接口
<mappers>
<package name="com.dao"/>
</mappers>
-->
[b]. 映射XML文件
上文示例已经展示了映射XML文件的用法,下面我们逐个细节理解。
1. select、update、delete和insert标签用于填写对应动作的SQL语句。
2. parameterType属性就是入参类型具体法则如下(parameterMap已被deprecated,所以不理也罢):
a. parameterType为POJO类型时,可通过 #{属性名} 填入属性值(该属性值经过防SQL注入处理),也可通过 ${name} 填入属性raw值(未经过防SQL注入处理的属性值),更爽的是 #{} 支持短路径操作如上文中的 #{myClass.id} 。
b. parameterType为int、long等值类型时,当仅有一个入参时,可以使用 #{任意字符} 填入属性值,但无法通过 ${任意字符串} 填入属性raw值(报找不到改实例属性),还可以通过 #{0} 和 #{param0} 来填入属性值;而入参为多个时,则只能使用 #{0}到#{n} 和 #{param0}到#{paramn} 来填入属性值了;但由于动态SQL下的标签仅识别 #{0} 等格式的占位符,因此建议通过使用 #{0} 格式的占位符,保持代码一致性。
3. resultType属性就是返回值类型。
4. sql标签则用于重用SQL片段,示例如下:
<sql id="studentCols"> Name, ClassID </sql> <select id="qryStudent" resultType="EStudent"> select <include id="studentCols"/> from TStudentML </select>
5. 模糊查询
网上有很多做法,但试过不是效果不好,就是报错,误打误撞发现最原始的做法 '%'+#{0}+'%'就OK了!
6. 一对一关系
一对一关系MyBatis为我们提供 嵌套结果、嵌套查询 两种查询方式。由于嵌套查询需要向数据库执行两次查询操作,因此推荐使用嵌套结果方式。
嵌套结果示例:
<!-- resultMap属性值为配置返回值映射信息的resultMap标签的id值 -->
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class c inner join teacher t on(c.tid = t.id) where t.id = #{0} </select>
<!-- type属性值为返回值的JAVA数据类型 --> <resultMap id="ClassResultMap" type="Cls">
<!--
id标签表示对象属性对应的是表主键
result标签表示对象属性对应的是普通表字段
注意:必须用id或result标出需要返回的字段/属性映射,否则在查询多条记录时,仅会返回最后一条记录
--> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
一对一关系采用association标签配置关联信息
javaType为JAVA数据类型
--> <association property="teacher" javaType="Teacher"> <id property="id" column="t_id"/> <result property="name" column="t_name"/> </association> </resultMap>
嵌套查询示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class where tid = #{id} </select> <select id="getTeacher" parameterType="int" resultType="Teacher"> select t_id id, t_name name from teacher where t_id = #{0} </select> <resultMap id="ClassResultMap" type="Cls"> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
select属性值为第二执行SQL语句id
而column属性值为传递给第二执行SQL语句的入参,而且入参为第一次SQL语句的查询结果集字段值
注意:若嵌套查询的条件不只一个,那么就需要将column属性设置为column="{prop1: fie;d1, prop2: field2}",然后嵌套查询的SQL中通过#{prop1},#{prop2}获取查询条件值
--> <association prorperty="teacher" column="tid" select="getTeacher"> </association> </resultMap>
7. 一对多关系
一对多关系同样分为 嵌套结果 和嵌套查询两种,由于嵌套查询会由于N+1次查询导致性能下降,一般推荐使用嵌套结果的做法,但有些查询操作必须使用嵌套查询才能完成。
嵌套结果示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class c inner join student s on(c.id = s.cid) where c.id = #{id} </select> <resultMap id="ClassResultMap" type="Cls">
<!--
注意:必须用id或result标出需要返回的字段/属性映射,否则在查询多条记录时,仅会返回最后一条记录
--> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
一对多关系采用collection标签来配置映射信息
ofType属性值为返回值的JAVA数据类型
--> <collection prorperty="students" ofType="Student"> <id property="id" column="s_id"/> <result property="name" column="s_name"/> </collection> </resultMap>
嵌套查询示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class where tid = #{id} </select> <select id="getStudents" parameterType="int" resultType="Student"> select s_id id, s_name name from student where cid = #{id} </select> <resultMap id="ClassResultMap" type="Cls">
<!-- 注意:若不写这句,那么c_id字段将作为嵌套查询的条件,而不会赋值到id属性了 --> <id property="id" column="c_id"/> <result property="name" column="c_name"/> <collection prorperty="students" column="c_id" select="getStudents"> </collection> </resultMap>
8. 动态SQL
MyBatis的动态SQL与数据库中通过exec、sp_executesql()等执行的动态SQL目的是一致的,只是操作形式的不同而已。MyBatis的动态SQL定义方式上与文本模板定义无异,定义后均为经过类似于模板引擎的模块进行解析得到最终的数据。下面我们来了解具体的标签吧。
[a]. <if test="逻辑条件判断"></if>
如果test内返回true,则标签体的内容将被添加到最终结果中。示例:
<if test="name != null and job != null"> and Name like '%'+#{name}+'%' and Description like '%'+#{job}+'%' </if>
注意:test语句中的逻辑条件判断必须使用入参的属性名或键名,而不能使用#{0}或#{1}等形式的入参,否则条件判断一律视为true。
[b]. <choose></choose>
相当于Java的switch语句。示例:
<choose> <when test="#{title} !=null"> and Title = #{title} </when> <when test="#{name} !=null"> and Name = #{name} </when> <otherwise> and Age > 10 </otherwise> </choose>
[c]. <where></where>
用于处理动态条件时,where留存与否的尴尬。具体就是
select * from tbl where <if test="#{name}!=null"> Name = #{name} </if> <if test="#{title}!=null"> and Title = #{title} </if>
当两个条件都不符合时,sql语句就变成 select * from tbl where ,报错是必然的。而 where标签 会根据其标签体是否有值来决定是否插入where关键字,并会自动去除无用的 or 和 and 关键字。示例:
<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>
[d]. <set></set>
用于在update语句中,动态设置更新的列。示例:
<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>
[e].<trim></trim>
当标签体有内容时则为内容添加前缀、后缀,而且可以除去内容前后部分内容。与 where标签 功能相同的示例:
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG
<!--
prefix属性值为添加到内容的前缀信息
prefixOverrides属性值为除去内容前的内容,当需要除去多个内容时,使用管道符|分割,注意:空格也将被除去
--> <trim prefix="where" prefixOverrides="AND |OR "> <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> </trim> </select>
与 set标签 功能相同的示例:
<update id="updateAuthorIfNecessary"> update Author <trim prefix="set" suffixOverrides=","> <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> </trim> where id=#{id} </update>
[f]. <foreach></foreach>
主要用于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>
入参为List或Array类型时,MyBatis会自动将其添加到内部Map对象中,并且List类型对象的键名为list,Array类型对象的键名为array,并通过 foreach标签 的collection属性指定入参的键名。而item属性用于指定 foreach标签 内集合元素的占位符名称,index属性则指定 foreach标签 内当前元素索引的占位符名称,而open、close和separator属性则分别指定动态SQL的开头、结束文本和集合元素项间的分隔符。
3. 操作数据
如上文示例那样,通过 SqlSessionFactoryBuilder实例 生成 SqlSessionFactory实例 ,然后在生成操作数据库的 SqlSession实例 。
需要注意的是:
a. 每次使用完 SqlSession实例 必须调用其 close() 方法释放链接;
b. 由于mybatis-config.xml中 <transactionManager type="JDBC"></transactionManager> 而且通过 SqlSessionFactory实例.openSession() 获取链接对象,因此链接对象默认时不会自动提交增、删和改操作的,因此需要调用 commit() 方法手动提交操作。
4. 生命周期
[a]. SqlSessionFactoryBuilder
由于`SqlSessionFactoryBuilder`实例用于生成`SqlSessionFactory`实例而已,因此并没有必要以应用程序全局作为作用域,并且无必要多线程共享。因此作为函数的局 部变量使用即可。
[b]. SqlSessionFactory
作为数据库连接池和连接池管理器使用,为达到数据库连接复用的效果,`SqlSessionFactory`实例应当以程序全局作为作用域,并且多线程共享。采用单例或静态单例模式较好
[c]. SqlSession
由于`SqlSession`实例非线程安全,因此作为函数的局部变量使用。而且由于数据库连接为共享资源,因此必须遵循晚调用早释放原则, 确保调用`close()`函数释放连接。
四、总结