简介
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
- MyBatis官方文档:https://mybatis.org/mybatis-3/zh/index.html
- GitHub源代码库:http://github.com/mybatis/mybatis-3
简单程序样例
- 创建 mybatis 数据库,及 user 表
CREATE DATABASE `mybatis`;USE `mybatis`;DROPTABLE IF EXISTS `user`;CREATETABLE `user` ( `id` int(11)NOTNULL, `name` varchar(30) DEFAULT 'NULL', `password` varchar(30) DEFAULT 'NULL', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;insertinto `user`(`id`,`name`,`password`)values(1,'admin','admin'),(2,'test','test');
- IDEA 创建一个 Maven 项目
- pom.xml 导入相关依赖
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.6</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency>
- 创建实体类
importlombok.AllArgsConstructor; importlombok.Data; publicclassUser { privateintid; privateStringname; privateStringpassword; }
- 编写 Mapper 接口类
importpojo.User; importjava.util.List; importjava.util.Map; publicinterfaceUserMapper { intaddUser(Useruser); intdeleteUserById(intid); intupdateUserById(Useruser); List<User>selectUser(); UserselectUserByNameAndPassword(Map<String, Object>map); }
- 编写 Mapper.xml 配置文件
<mappernamespace="mapper.UserMapper"><insertid="addUser"parameterType="pojo.User"> insert into user(id, name, password) values (#{id}, #{name}, #{password}) </insert><deleteid="deleteUserById"parameterType="int"> delete from user where id = #{id} </delete><updateid="updateUserById"parameterType="pojo.User"> update user set name=#{name}, password = #{password} where id = #{id} </update><selectid="selectUser"resultType="pojo.User"> select * from user </select><selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user where name = #{name} and password = #{password} </select></mapper>
- 编写 jdbc.properties 文件
url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8 driver=com.mysql.jdbc.Driver username=root password=123456
- 编写MyBatis核心配置文件
<configuration><propertiesresource="jdbc.properties"/><!--环境配置--><environmentsdefault="development"><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${driver}"/><propertyname="url"value="${url}"/><propertyname="username"value="${username}"/><propertyname="password"value="${password}"/></dataSource></environment></environments><!--注册 Mapper--><mappers><mapperclass="mapper.UserMapper"/></mappers></configuration>
- 编写 MybatisUtils 工具类
importorg.apache.ibatis.io.Resources; importorg.apache.ibatis.session.SqlSession; importorg.apache.ibatis.session.SqlSessionFactory; importorg.apache.ibatis.session.SqlSessionFactoryBuilder; importjava.io.IOException; importjava.io.InputStream; publicclassMybatisUtils { /*** 通过输入流从 XML 文件中构建 SqlSessionFactory 的实例*/privatestaticSqlSessionFactorysqlSessionFactory; static { try { InputStreaminputStream=Resources.getResourceAsStream("mybatis-config.xml"); sqlSessionFactory=newSqlSessionFactoryBuilder().build(inputStream); } catch (IOExceptione) { e.printStackTrace(); } } /*** 获取 SqlSession 连接,并设置自动提交事务** @return SqlSession连接*/publicstaticSqlSessiongetSession() { returnsqlSessionFactory.openSession(true); }
- 编写测试类
importlombok.Cleanup; importorg.apache.ibatis.session.SqlSession; importorg.junit.Test; importjava.util.HashMap; importjava.util.List; importjava.util.Map; publicclassUserMapperTest { publicvoidaddUser() { SqlSessionsqlSession=MybatisUtils.getSession(); UserMapperuserMapper=sqlSession.getMapper(UserMapper.class); intnum=userMapper.addUser(newUser(3, "tes", "123123")); System.out.println(num); } publicvoiddeleteUserById() { SqlSessionsqlSession=MybatisUtils.getSession(); UserMapperuserMapper=sqlSession.getMapper(UserMapper.class); intnum=userMapper.deleteUserById(3); System.out.println(num); } publicvoidupdateUserById() { SqlSessionsqlSession=MybatisUtils.getSession(); UserMapperuserMapper=sqlSession.getMapper(UserMapper.class); intnum=userMapper.updateUserById(newUser(1, "admin", "123465")); System.out.println(num); } publicvoidselectUser() { SqlSessionsqlSession=MybatisUtils.getSession(); UserMapperuserMapper=sqlSession.getMapper(UserMapper.class); List<User>userList=userMapper.selectUser(); for (Useruser : userList) { System.out.println(user); } } publicvoidselectUserByNameAndPassword() { SqlSessionsqlSession=MybatisUtils.getSession(); UserMapperuserMapper=sqlSession.getMapper(UserMapper.class); Map<String, Object>map=newHashMap<>(); map.put("name", "test"); map.put("password", "test"); Useruser=userMapper.selectUserByNameAndPassword(map); System.out.println(user); } }
- 运行测试,成功的查询出来的我们的数据,ok!
XML 配置
<!--配置--><configuration><!--属性--><properties><!--设置--><settings><!--类型别名--><typeAliases><!--类型处理器--><typeHandlers><!--对象工厂--><objectFactory><!--插件--><plugins><!--环境配置--><environments><!--数据库厂商标识--><databaseIdProvider><!--映射器--><mappers></configuration><!-- 注意元素节点的顺序!顺序不对会报错 -->
Properties
属性可以在外部文件进行配置,通过 <properties resource="jdbc.properties"/>
引入外部文件
url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8 driver=com.mysql.jdbc.Driver username=root password=123456
<propertiesresource="jdbc.properties"/><!--环境配置--><environmentsdefault="development"><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${driver}"/><propertyname="url"value="${url}"/><propertyname="username"value="${username}"/><propertyname="password"value="${password}"/></dataSource></environment></environments
也可以在 properties 元素的子元素中设置,并可以进行动态替换
<!--环境配置--><environmentsdefault="development"><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="com.mysql.jdbc.Driver"/><propertyname="url"value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8"/><propertyname="username"value="root"/><propertyname="password"value="123456"/></dataSource></environment></environments>
Settings
常用的设置
设置名 | 描述 | 有效值 | 默认值 |
cacheEnabled | 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 | true | false | true |
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 | true | false | false |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | 未设置 |
一个配置完整的 settings 元素的示例如下:
<settings><settingname="cacheEnabled"value="true"/><settingname="lazyLoadingEnabled"value="true"/><settingname="multipleResultSetsEnabled"value="true"/><settingname="useColumnLabel"value="true"/><settingname="useGeneratedKeys"value="false"/><settingname="autoMappingBehavior"value="PARTIAL"/><settingname="autoMappingUnknownColumnBehavior"value="WARNING"/><settingname="defaultExecutorType"value="SIMPLE"/><settingname="defaultStatementTimeout"value="25"/><settingname="defaultFetchSize"value="100"/><settingname="safeRowBoundsEnabled"value="false"/><settingname="mapUnderscoreToCamelCase"value="false"/><settingname="localCacheScope"value="SESSION"/><settingname="jdbcTypeForNull"value="OTHER"/><settingname="lazyLoadTriggerMethods"value="equals,clone,hashCode,toString"/></settings>
TypeAliases
类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
- 扫描类使用别名
<typeAliases><typeAliastype="pojo.User"alias="User"/></typeAliases>
- 扫描包
<typeAliases><packagename="pojo"/></typeAliases>
- 每一个在包
pojo
中的 JavaBean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。若有注解,则别名为其注解值。
- 在类上使用注解
importorg.apache.ibatis.type.Alias; "user") (publicclassUser { privateintid; privateStringname; privateStringpassword; }
下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。
别名 | 映射的类型 |
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
Environments
配置 MyBatis 的多套运行环境,将 SQL 映射到多个不同的数据库上,必须指定其中一个为默认运行环境(通过 default 指定),通过 id 进行区别
<!--环境配置--><environmentsdefault="development"><!--development环境--><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${driver}"/><propertyname="url"value="${url}"/><propertyname="username"value="${username}"/><propertyname="password"value="${password}"/></dataSource></environment><!--test--><environmentid="test"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${driver}"/><propertyname="url"value="${url}"/><propertyname="username"value="${username}"/><propertyname="password"value="${password}"/></dataSource></environment></environments>
子元素节点:environment
- transactionManager 事务管理器
<!--语法--><transactionManagertype="[ JDBC | MANAGED ]"/>
- dataSource 数据源
- 使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。数据源是必须配置的。有三种内建的数据源类型:
- unpooled:这个数据源的实现只是每次被请求时打开和关闭连接。
- pooled:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来 , 这是一种使得并发 Web 应用快速响应请求的流行处理方式。
- jndi:这个数据源的实现是为了能在如 Spring 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。
Mapper
Mapper 映射器告诉 MyBatis 到哪里去找映射文件
- 使用相对于类路径的资源引用
<mappers><mapperresource="mapper/UserMapper.xml"/></mappers>
- 类名
<!--需要配置文件名称和接口名称一致,并且位于同一目录下--><mappers><mapperclass="mapper.UserMapper"/></mappers>
- 包名
<!--需要配置文件名称和接口名称一致,并且位于同一目录下--><mappers><packagename="mapper"/></mappers>
Mapper 文件
<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="mapper.UserMapper"><insertid="addUser"parameterType="pojo.User">insertintouser(id, name, password) values (#{id}, #{name}, #{password}) </insert></mapper>
- namespace 的命名必须跟某个接口同名
- 接口中的方法与映射文件中sql语句id应该一一对应
- namespace命名规则 : 包名+类名
XML 映射
ParameterType
- 一个基本类型的参数,可以通过
#{参数名}
获取
<mappernamespace="mapper.UserMapper"><deleteid="deleteUserById"parameterType="int"> delete from user where id = #{id} </delete></mapper>
- 多个参数时采用Map,通过
#{Key}
获取
Map<String,Object> map = new HashMap<>(); map.put("name", "test"); map.put("password", "test"); User user = userMapper.selectUserByNameAndPassword(map);
<mappernamespace="mapper.UserMapper"><selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user where name = #{name} and password = #{password} </select></mapper>
- 对象传递参数,通过
#{属性名}
获取
<mappernamespace="mapper.UserMapper"><insertid="addUser"parameterType="pojo.User"> insert into user(id, name, password) values (#{id}, #{name}, #{password}) </insert></mapper>
- 使用注解,在接口方法的参数前加 @Param() 属性,直接取 @Param() 中设置的值即可,不需要单独设置参数类型
import org.apache.ibatis.annotations.Param; public interface UserMapper { int deleteUserById(@Param("id") int id); }
<mappernamespace="mapper.UserMapper"><deleteid="deleteUserById"> delete from user where id = #{id} </delete></mapper>
关于@Param
使用 @Param 注解用于给方法参数起一个名字。使用原则:
- 在方法只接受一个参数的情况下,可以不使用 @Param。
- 在方法接受多个参数的情况下,建议一定要使用 @Param 注解给参数命名。
- 如果参数是 JavaBean , 则不能使用@Param。
- 不使用 @Param 注解时,参数只能有一个,并且是Javabean。
#与$的区别
- #{} 的作用主要是替换预编译语句(PrepareStatement)中的占位符? 【推荐使用】
INSERT INTO user (name) VALUES (#{name}); INSERT INTO user (name) VALUES (?);
- ${} 的作用是直接进行字符串替换
INSERT INTO user (name) VALUES ('${name}'); INSERT INTO user (name) VALUES ('zhangsan');
ResultType
自动映射
resultMap
元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBCResultSets
数据提取代码中解放出来。- 实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份
resultMap
能够代替实现同等功能的长达数千行的代码。 - ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了
简单映射语句示例,不需要显指定 resultMap
。比如:
<mappernamespace="mapper.UserMapper"><selectid="selectUserById"resultType="map"> select id , name , pwd from user where id = #{id} </select></mapper>
上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 不是一个很好的模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为模型。
手动映射
返回值类型为 resultMap,编写 resultMap,实现手动映射!
<mappernamespace="mapper.UserMapper"><!-- 编写 resultMap --><resultMapid="UserMaps"type="pojo.User"><!-- id为主键 --><idcolumn="id"property="id"/><!-- column是数据库表的列名 , property是对应实体类的属性名 --><resultcolumn="name"property="name"/><resultcolumn="password"property="password"/></resultMap><!-- 返回值类型为 resultMap ,值为编写的 resultMap 的 id --><selectid="selectUser"resultMap="UserMaps"> select * from user </select></mapper>
注解开发
适用于简单的 SQL 语句,利用注解开发就不需要 mapper.xml 映射文件
- 在接口中添加注解
packagemapper; importorg.apache.ibatis.annotations.*; importpojo.User; importjava.util.List; publicinterfaceUserMapper { "insert into user(id, name, password) values (#{id}, #{name}, #{password})") (intaddUser(Useruser); "delete from user where id=#{id}") (intdeleteUserById( ("id") intid); "update user set name=#{name},password=#{password} where id = #{id}") (intupdateUserById(Useruser); "select id,name,password from user") (List<User>selectUser(); }
- 在mybatis的核心配置文件中注入
<!--注册Mapper--><mappers><mapperclass="mapper.UserMapper"/></mappers>
- 测试 OK 即可
一对多
多个学生对应一个老师
- 创建学生实体类
publicclassStudent { privateintid; privateStringname; //多个学生可以是同一个老师,即多对一privateTeacherteacher; }
- 编写 StudentMapper 接口
publicinterfaceStudentMapper { //获取所有学生及对应老师的信息publicList<Student>getStudents(); }
- 编写 Mapper.xml 文件
<mappernamespace="mapper.StudentMapper"><!--需求:获取所有学生及对应老师的信息--><!--思路1:嵌套查询association → 一个复杂类型的关联;使用它来处理关联查询--><selectid="getStudents"resultMap="StudentTeacher"> select * from student </select><resultMapid="StudentTeacher"type="Student"><!--association关联属性 property→属性名 javaType→属性类型 column→关联列名 select→子查询--><associationproperty="teacher"column="tid"javaType="Teacher"select="getTeacher"/></resultMap><!-- 传递一个参数的时候,下面可以写任何值 --><selectid="getTeacher"resultType="teacher"> select * from teacher where id = #{id} </select><!----------------------------------------------------------------------------------------------------><!--思路2:结果集的映射--><selectid="getStudents2"resultMap="StudentTeacher2"> select s.id sid, s.name sname , t.name tname from student s,teacher t where s.tid = t.id </select><resultMapid="StudentTeacher2"type="Student"><idproperty="id"column="sid"/><resultproperty="name"column="sname"/><!--association关联属性 property→属性名 javaType→属性类型--><associationproperty="teacher"javaType="Teacher"><!-- column是数据库表的列名 , property是对应实体类的属性名 --><resultproperty="name"column="tname"/></association></resultMap></mapper>
注:当子查询需要传多个参数时
<mappernamespace="mapper.StudentMapper"><selectid="getStudents"resultMap="StudentTeacher"> select * from student </select><!-- 传递多个参数时association 中 column 多参数配置:column="{key=value,key=value}"其实就是键值对的形式,key 是传给下个 sql 的取值名称,value 是片段一中 sql 查询的字段名--><resultMapid="StudentTeacher"type="Student"><!--association关联属性 property→属性名 javaType→属性类型 column→关联列名 select→子查询--><associationproperty="teacher"column="{id=tid,name=tname}"javaType="Teacher"select="getTeacher"/></resultMap><selectid="getTeacher"resultType="teacher"> select * from teacher where id = #{id} and name = #{name} </select></mapper>
多对一
一个老师对应多个学生
- 创建教师实体类
publicclassTeacher { privateintid; privateStringname; //一个老师多个学生privateList<Student>students; }
- 编写 TeacherMapper 接口
publicinterfaceTeacherMapper { //获取指定老师,及老师下的所有学生publicTeachergetTeacher(intid); }
- 编写 Mapper.xml 文件
<mappernamespace="mapper.TeacherMapper"><!--需求:获取指定老师,及老师下的所有学生--><!--思路1:嵌套查询--><selectid="getTeacher"resultMap="TeacherStudent2"> select * from teacher where id = #{id} </select><resultMapid="TeacherStudent2"type="Teacher"><!--column是 student 表外键的列名--><collectionproperty="students"javaType="ArrayList"ofType="Student"column="id"select="getStudentByTeacherId"/></resultMap><selectid="getStudentByTeacherId"resultType="Student"> select * from student where tid = #{id} </select><!----------------------------------------------------------------------------------------------------><!--思路2:结果集映射集合映射使用 collection!JavaType 和 ofType都是用来指定对象类型的JavaType是用来指定 pojo 中属性的类型ofType 指定的是映射到 list集合属性中 pojo的类型。--><selectid="getTeacher"resultMap="TeacherStudent"> select s.id sid, s.name sname , t.name tname, t.id tid from student s,teacher t where s.tid = t.id and t.id=#{id} </select><resultMapid="TeacherStudent"type="Teacher"><resultproperty="name"column="tname"/><collectionproperty="students"ofType="Student"><resultproperty="id"column="sid"/><resultproperty="name"column="sname"/><resultproperty="tid"column="tid"/></collection></resultMap></mapper>
动态SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
IF 语句
<!--如果名字为空,那么只根据密码查询,反之,则根据名字来查询select * from blog where name = #{name} and password = #{password}--><selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user where <iftest="name!=null"> name = #{name} </if><iftest="password!=null"> and password = #{password} </if></select>
上述语句中,如果 title 为空那么查询语句为 select * from user where and password = #{password}
,这是错误的 SQL 语句
Where 语句
<!--如果名字为空,那么只根据密码查询,反之,则根据名字来查询select * from blog where name = #{name} and password = #{password}--><selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user <where><iftest="name != null"> name = #{name} </if><iftest="password != null"> and password = #{password} </if></where></select>
“where”标签:如果它包含的标签中有返回值的话,它就会插入一个‘where’。此外,如果标签返回的内容是以 AND 或 OR 开头的,则它会将其剔除掉。
Set 语句
<!--update user set name=#{name},password=#{password} where id=#{id}--><updateid="updateUserById"parameterType="pojo.User"> update user <set><iftest="name!=null"> name=#{name}, </if><iftest="password!=null"> password = #{password} </if></set> where id = #{id} </update>
Choose语句
类似于 Java 的 switch 语句,查询条件有一个满足即可。
<selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user <where><choose><whentest="name != null"> name = #{name} </when><whentest="password != null"> and password = #{password} </when><otherwise> and id=#{id} </otherwise></choose></where></select>
foreach
foreach 元素的功能是非常强大的,它允许你指定一个集合,声明可以用在元素体内的集合项和索引变量。它也允许你指定开闭匹配的字符串以及在迭代中间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。
动态 SQL 的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:
<!--select * from user where id in (1,2,3)--><selectid="selectUser"resultType="pojo.User"> select * from user where id in <!--List 实例将会以 "list" 作为键数组实例的键是 "array"--><foreachcollection="array"item="id"open="("separator=","close=")"> #{id} </foreach></select>
- collection:指定输入对象中的集合属性
- item:每次遍历生成的对象
- open:开始遍历时的拼接字符串
- close:结束时拼接的字符串
- separator:遍历对象之间需要拼接的字符串
注意:可以将一个 List 实例或者数组作为参数对象传给 MyBatis,当你这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以 “list” 作为键,而数组实例的键将是 “array”。
SQL片段
有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。
- 提取重复 SQL 片段:
<sqlid="if-name-password"><iftest="name != null"> name = #{name} </if><iftest="password != null"> and password = #{password} </if></sql>
- 引用SQL片段:
<selectid="selectUserByNameAndPassword"parameterType="map"resultType="pojo.User"> select * from user <where><!-- 引用 sql 片段,如果 refid 指定的不在本文件中,那么需要在前面加上 namespace --><includerefid="if-name-password"></include><!-- 在这里还可以引用其他的 sql 片段 --></where></select>
注意:
- 最好基于单表来定义 sql 片段,提高片段的可重用性
- 在 sql 片段中不要包括 where
日志
内置日志
在 Mybatis 核心配置文件中设置即可
<settings><settingname="logImpl"value="STDOUT_LOGGING"/></settings>
Log4j
Log4j 是 Apache 的一个开源项目,通过使用 Log4j,可以控制日志信息输送的目的地是控制台、文件、GUI组件,我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
- 导入 Log4j 依赖
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency>
- 为了实现只保留7天日志文件,扩展 DailyRollingFileAppender
importjava.io.File; importjava.io.FileFilter; importjava.io.IOException; importjava.io.InterruptedIOException; importjava.text.ParseException; importjava.text.SimpleDateFormat; importjava.util.Arrays; importjava.util.Calendar; importjava.util.Date; importjava.util.GregorianCalendar; importjava.util.Locale; importjava.util.TimeZone; importorg.apache.log4j.FileAppender; importorg.apache.log4j.Layout; importorg.apache.log4j.helpers.LogLog; importorg.apache.log4j.spi.LoggingEvent; /*** Log4j的扩展-保留最近指定个数的日志文件**/publicclassMyDailyRollingFileAppenderextendsFileAppender{ staticfinalintTOP_OF_TROUBLE=-1; staticfinalintTOP_OF_MINUTE=0; staticfinalintTOP_OF_HOUR=1; staticfinalintHALF_DAY=2; staticfinalintTOP_OF_DAY=3; staticfinalintTOP_OF_WEEK=4; staticfinalintTOP_OF_MONTH=5; /*** 默认设置:"'.'yyyy-MM-dd"=* 设置说明:按天循环打印日志*/privateStringdatePattern="'.'yyyy-MM-dd"; privateintmaxBackupIndex=1; privateStringscheduledFilename; /**下一次Rolling日志文件的时间*/privatelongnextCheck=System.currentTimeMillis () -1; Datenow=newDate(); SimpleDateFormatsdf; RollingCalendarrc=newRollingCalendar(); intcheckPeriod=TOP_OF_TROUBLE; /*** 获取当前环境所处的时区* 仅供computeCheckPeriod方法使用*/staticfinalTimeZonegmtTimeZone=TimeZone.getTimeZone("GMT"); publicMyDailyRollingFileAppender() {} publicMyDailyRollingFileAppender (Layoutlayout, Stringfilename, StringdatePattern) throwsIOException { super(layout, filename, true); this.datePattern=datePattern; activateOptions(); } publicStringgetDatePattern() { returndatePattern; } publicvoidsetDatePattern(Stringpattern) { this.datePattern=pattern; } publicintgetMaxBackupIndex() { returnmaxBackupIndex; } publicvoidsetMaxBackupIndex(intmaxBackupIndex) { this.maxBackupIndex=maxBackupIndex; } /*** activateOptions译名为激活操作* 意思是按照配置的参数进行初始化* scheduledFilename为log的最后一次修改时间*/publicvoidactivateOptions() { super.activateOptions(); if(datePattern!=null&&fileName!=null) { now.setTime(System.currentTimeMillis()); sdf=newSimpleDateFormat(datePattern); inttype=computeCheckPeriod(); printPeriodicity(type); rc.setType(type); Filefile=newFile(fileName); scheduledFilename=fileName+sdf.format(newDate(file.lastModified())); } else { LogLog.error("Either File or DatePattern options are not set for appender ["+name+"]."); } } intcomputeCheckPeriod() { RollingCalendarrollingCalendar=newRollingCalendar(gmtTimeZone, Locale.getDefault()); //设置初始时间为格林尼治时间:1970-01-01 00:00:00 GMTDateepoch=newDate(0); if(datePattern!=null) { for(inti=TOP_OF_MINUTE; i<=TOP_OF_MONTH; i++) { //将所示的时间格式化为当前时区SimpleDateFormatsimpleDateFormat=newSimpleDateFormat(datePattern); simpleDateFormat.setTimeZone(gmtTimeZone); Stringr0=simpleDateFormat.format(epoch); rollingCalendar.setType(i); Datenext=newDate(rollingCalendar.getNextCheckMillis(epoch)); Stringr1=simpleDateFormat.format(next); //System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);if(r0!=null&&r1!=null&&!r0.equals(r1)) { returni; } } } returnTOP_OF_TROUBLE; // Deliberately head for trouble... } /*** 按照周期将当前日志文件转存为日期文件** @throws IOException*/voidrollOver() throwsIOException { if (datePattern==null) { errorHandler.error("Missing DatePattern option in rollOver()."); return; } StringdatedFilename=fileName+sdf.format(now); //如果最后一次的修改时间为当前时间 ,则不做任何任何操作if (scheduledFilename.equals(datedFilename)) { return; } // 关闭当前文件,重命名为日期文件this.closeFile(); Filetarget=newFile(scheduledFilename); if (target.exists()) { target.delete(); } Filefile=newFile(fileName); booleanresult=file.renameTo(target); if(result) { LogLog.debug(fileName+" -> "+scheduledFilename); } else { LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"]."); } //获取日志文件列表,控制数量,实现清理策略if (file.getParentFile().exists()){ File[] files=file.getParentFile().listFiles(newLogFileFilter(file.getName())); Long[] dateArray=newLong[files.length]; for (inti=0; i<files.length; i++) { FilefileItem=files[i]; StringfileDateStr=fileItem.getName().replace(file.getName(), ""); Datefiledate=null; try { filedate=sdf.parse(fileDateStr); longfileDateLong=filedate.getTime(); dateArray[i] =fileDateLong; } catch (ParseExceptione) { LogLog.error("Parse File Date Throw Exception : "+e.getMessage()); } } Arrays.sort(dateArray); if (dateArray.length>maxBackupIndex) {//需要清理多余日志文件了for (inti=0; i<dateArray.length-maxBackupIndex; i++) { StringdateFileName=file.getPath() +sdf.format(dateArray[i]); FiledateFile=newFile(dateFileName); if (dateFile.exists()) { dateFile.delete(); } } } } try { this.setFile(fileName, true, this.bufferedIO, this.bufferSize); } catch(IOExceptione) { errorHandler.error("setFile("+fileName+", true) call failed."); } scheduledFilename=datedFilename; } /*** 写入日志之前判断是否需要新起一个日志来记录*/protectedvoidsubAppend(LoggingEventevent) { longn=System.currentTimeMillis(); if (n>=nextCheck) { now.setTime(n); nextCheck=rc.getNextCheckMillis(now); try { rollOver(); } catch(IOExceptionioe) { if (ioeinstanceofInterruptedIOException) { Thread.currentThread().interrupt(); } LogLog.error("rollOver() failed.", ioe); } } super.subAppend(event); } /*** 文件过滤器*/classLogFileFilterimplementsFileFilter { privateStringlogName; publicLogFileFilter(StringlogName) { this.logName=logName; } publicbooleanaccept(Filefile) { if (logName==null||file.isDirectory()) { returnfalse; } else { LogLog.debug(file.getName()); returnfile.getName().startsWith(logName); } } } /*** 根据type打印做出日志打印* @param type*/voidprintPeriodicity(inttype) { switch(type) { caseTOP_OF_MINUTE: LogLog.debug("Appender ["+name+"] to be rolled every minute."); break; caseTOP_OF_HOUR: LogLog.debug("Appender ["+name+"] to be rolled on top of every hour."); break; caseHALF_DAY: LogLog.debug("Appender ["+name+"] to be rolled at midday and midnight."); break; caseTOP_OF_DAY: LogLog.debug("Appender ["+name+"] to be rolled at midnight."); break; caseTOP_OF_WEEK: LogLog.debug("Appender ["+name+"] to be rolled at start of week."); break; caseTOP_OF_MONTH: LogLog.debug("Appender ["+name+"] to be rolled at start of every month."); break; default: LogLog.warn("Unknown periodicity for appender ["+name+"]."); } } /*** 内部类* @author 日志Rolling日历,获取下一次日志Rolling时间**/classRollingCalendarextendsGregorianCalendar { privatestaticfinallongserialVersionUID=-3560331770601814177L; inttype=MyDailyRollingFileAppender.TOP_OF_TROUBLE; /*** RollingCalendar默认构造器*/RollingCalendar() { super(); } /*** RollingCalendar构造器* 根据地点时区 ,获取对应的日历Calendar* @param tz* @param locale*/RollingCalendar(TimeZonetz, Localelocale) { super(tz, locale); } voidsetType(inttype) { this.type=type; } publiclonggetNextCheckMillis(Datenow) { returngetNextCheckDate(now).getTime(); } /*** 根据所传入的时间以及时间类型获取下一个时间* @param now* @return*/publicDategetNextCheckDate(Datenow) { this.setTime(now); switch(type) { caseMyDailyRollingFileAppender.TOP_OF_MINUTE: this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MINUTE, 1); break; caseMyDailyRollingFileAppender.TOP_OF_HOUR: this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.HOUR_OF_DAY, 1); break; caseMyDailyRollingFileAppender.HALF_DAY: this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); inthour=get(Calendar.HOUR_OF_DAY); if(hour<12) { this.set(Calendar.HOUR_OF_DAY, 12); } else { this.set(Calendar.HOUR_OF_DAY, 0); this.add(Calendar.DAY_OF_MONTH, 1); } break; caseMyDailyRollingFileAppender.TOP_OF_DAY: this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.DATE, 1); break; caseMyDailyRollingFileAppender.TOP_OF_WEEK: this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek()); this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.WEEK_OF_YEAR, 1); break; caseMyDailyRollingFileAppender.TOP_OF_MONTH: this.set(Calendar.DATE, 1); this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MONTH, 1); break; default: thrownewIllegalStateException("Unknown periodicity type."); } returngetTime(); } } }
- 编写 log4j.properties
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码 log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file=utils.MyDailyRollingFileAppender #日志文件路径 log4j.appender.file.File=./logs/log.log log4j.appender.file.Encoding=utf-8 log4j.appender.file.Append=true log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout #设置日志文件名为日期并按天生成日志文件 log4j.appender.file.datePattern = '.'yyyy-MM-dd #保留7天的日志文件 log4j.appender.file.maxBackupIndex=7 log4j.appender.file.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss,SSS}][%c]%m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
- 运行测试,查看生成的日志文件
缓存
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
- 为了提高扩展性,MyBatis定义了缓存接口Cache。可以通过实现Cache接口来自定义二级缓存
一级缓存
一级缓存也叫本地缓存:
- 与数据库同一次会话期间查询到的数据会放在本地缓存中。
- 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;
二级缓存
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
- 基于namespace级别的缓存,一个名称空间,对应一个二级缓存
- 工作机制
- 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
- 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
- 新的会话查询信息,就可以从二级缓存中获取内容;
- 不同的 mapper 查出的数据会放在自己对应的缓存(map)中;
使用步骤:
- 在 核心配置文件中开启全局缓存
<settingname="cacheEnabled"value="true"/>
- 每个 mapper.xml 中配置使用二级缓存
<cache/>
高级配置
<cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
提示: 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
问题
1、org.apache.ibatis.binding.BindingException: Type interface xxxxxxxxx is not known to the MapperRegistry.
- 原因:Mapper.xml 未注册
- 解决方法:在 Mybatis 核心配置文件中添加
<!--每一个 Mapper.xml 都需要再 Mybatis 核心配置文件中注册--><mappers><!--使用相对于类路径的资源引用--><mapperresource="mapper/UserMapper.xml"/><!--使用映射器接口实现类的完全限定类名--><mapperclass="mapper.UserMapper"/><!--将包内的映射器接口实现全部注册为映射器 --><packagename="mapper"/></mappers>
2、 Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.io.IOException: Could not find resource xxxxxxxx
- 原因:Maven 静态资源过滤问题
- 解决方法:在 Maven pom.xml 配置文件中添加过滤
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>true</filtering></resource><resource><directory>src/main/resources</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>true</filtering></resource></resources></build>
3、org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: pojo.User
- 原因:使用 开启二级缓存之后,readOnly 属性默认为 false,可读写的缓存会(通过序列化)返回缓存对象的拷贝,
- 解决方法:实体类 实现 可序列化
publicclassUserimplementsSerializable { privateintid; privateStringname; privateStringpassword; }