什么是MyBatis?
- MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
- MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
- MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
- MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code,并且改名为MyBatis 。
- 附MyBatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html
MyBatis入门程序
-
MyBatis相关Jar包:(使用Maven构建)`
<groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency>`
-
MySQL数据库驱动包:`
<groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>`
-
Junit单元测试包:`
<groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>`
-
XML核心配置文件(mybatis-config.xml)构建SqlSessionFactory:
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybaits?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
环境搭建:
public class User {
private int id;
private String last_name;
private String email;
public User() {
}
public User(int id, String last_name, String email) {
this.id = id;
this.last_name = last_name;
this.email = email;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getLast_name() {
return last_name;
}
public void setLast_name(String last_name) {
this.last_name = last_name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", last_name='" + last_name + '\'' +
", email='" + email + '\'' +
'}';
}
}
Tips:
平时为了避免不必要的麻烦,可以在设置实体类属性的时候,尽量保证其跟数据库字段名一致,当数据库字段名有下划线时候,实体类属性名可以采用驼峰命名规则。但是具体还是看要求,约定大于配置!!!后面也会有相关属性名跟字段名不一致的解决方案。
还有就是,最好自动写上get,set方法以及构造器和tostring方法。
public interface UserMapper {
//查询所有用户
List<User> getUserList();
}
<?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">
<!--namespace==绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.xxx.dao.UserMapper">
<!--select查询所有用户语句-->
<select id="getUserList" resultType="com.xxx.pojo.User">
/*执行sql*/
select * from user
</select>
</mapper>
-
单元测试:
- 每个基于MyBatis 的应用都是以一个 SqlSessionFactory的实例为核心的,SqlSessionFactory 的实例可以通过SqlSessionFactoryBuilder 获得。
- SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的Configuration 实例来构建出SqlSessionFactory 实例。
编写一个MyBatis工具类获取sqlSession对象
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
private static InputStream inputStream;
static {
try {
//1.使用MyBatis获取SqlSessionFactory 实例
String resource = "mybatis-config.xml";
inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
@Test
public void getUserList() {
//1:获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//方式1:getMapper,执行sql
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserList();
for (User user : userList) {
System.out.println(user);
}
//3:关闭sqlSession
sqlSession.close();
}
以上是使用xml文件从SqlSessionFactory 中获取 SqlSession,使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。
还有一种方式是:可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:(不推荐使用)
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}
注意点和遇到的问题
<!--每一个mapper.xml都需要在Mybatis核心配置文件中配置-->
<mappers>
<mapper resource="com/xxx/dao/UserMapper.xml"/>
</mappers>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<argLine>
-Dfile.encoding=UTF-8
</argLine>
</configuration>
</plugin>
</plugins>
-
添加后还是有如下问题:
这个问题我上网找了很多资料,都是说这是编码为UTF-8形式即可,但是还是一直报错,最后我自己捣鼓试了一下将UTF-8修改为UTF8,居然可以了,真是夺笋啊!!!!而且,是要将项目设计的两个UserMapper,xml和mybatis-config.xml都修改才行,如下所示:
<?xml version="1.0" encoding="UTF8" ?>
相信很多人都出现过如下情况:
org.apache.ibatis.binding.BindingException: Type interface com.xxx.dao.UserMapper is not known to the MapperRegistry.
表示未绑定注册mapper,有可能是idea无法扫描到所需的xml资源,解决方法如下:在maven的pom文件中的标签内加上:
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
即可扫描到src下的java和resources下的所有properties和xml资源。
MyBatis常用XML配置
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- mappers(映射器)
注意:
configuration" 里的标签顺序如下:(否则报错如下信息)
"(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?
objectWrapperFactory?,reflectorFactory?,plugins?,environments?,
databaseIdProvider?,mappers?)"
具体还是建议查看文档吧,这里比较齐全:
MyBatis的CRUD操作示例
public interface UserMapper {
//查询所有用户
List<User> getUserList();
//根据id查询用户
User getUserById(int id);
//添加一个用户
int addUser(User user);
//修改用户
int updateUser(User user);
//删除一个用户
int deleteUser(int id);
}
<?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">
<!--namespace==绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.xxx.dao.UserMapper">
<!--select查询所有用户语句-->
<select id="getUserList" resultType="com.xxx.pojo.User">
/*执行sql*/
select * from user
</select>
<!--根据id查询用户-->
<select id="getUserById" resultType="com.xxx.pojo.User" parameterType="_int">
select * from mybaits.user where id =#{id}
</select>
<!--添加一个用户-->
<insert id="addUser" parameterType="com.xxx.pojo.User" >
insert into mybaits.user(id,last_name,email) values (#{id},#{last_name},#{email})
</insert>
<!--修改用户-->
<update id="updateUser" parameterType="com.xxx.pojo.User">
update mybaits.user
set last_name =#{last_name},email=#{email}
where id =#{id};
</update>
<!--删除用户-->
<delete id="deleteUser" parameterType="com.xxx.pojo.User">/*这里写int也可以*/
delete from mybaits.user where id =#{id}
</delete>
</mapper>
以上第二个select这个语句名为:getUserById,接受一个 int(或 Integer)类型的参数,并返回一个User类型的对象,其中的键是列名,值便是结果行中的对应值。其中,参数形式为:#{}
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器
resultType 期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
resultMap 对外部 resultMap 的命名引用。结果映射是
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
resultOrdered 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。
resultSets 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。
Update,Insert,Delete常用属性跟Select差不多,只是个别不同,用到时候再记录。
public class UserMapperTest {
@Test
public void getUserList() {
//1:获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//方式1:getMapper,执行sql
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserList();
for (User user : userList) {
System.out.println(user);
}
//3:关闭sqlSession
sqlSession.close();
}
@Test
public void getUserById() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User userById = mapper.getUserById(2);
System.out.println(userById);
sqlSession.close();
}
@Test
public void addUser() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int addUser = mapper.addUser(new User(4, "赵丽颖", "com@zhao"));
if(addUser>0){
System.out.println("插入成功!");
}
//一定要提交事务,否则及时输出插入成功,数据库也不会有数据
sqlSession.commit();
sqlSession.close();
}
@Test
public void updateUser() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int updateUser = mapper.updateUser(new User(3, "张艺兴", "com@zhang"));
if(updateUser>0){
System.out.println("修改成功!");
}
//一定要提交事务,否则及时输出插入成功,数据库也不会有数据
sqlSession.commit();
sqlSession.close();
}
@Test
public void deleteUser() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int deleteUser = mapper.deleteUser(2);
if(deleteUser>0){
System.out.println("删除成功!");
}else {
System.out.println("删除失败!");
}
//一定要提交事务,否则即使输出插入成功,数据库也不会有数据
sqlSession.commit();
sqlSession.close();
}
}
解决数据库表字段名跟属性名不一致问题:(例如:字段名为last_name,属性名为lastName,若使用resultType,查询出来lastName为NULL)
- lastName为NULL的原因为:MyBatis的自动映射机制会根据这些查询的列名(会将列名转化为小写,数据库不区分大小写) ,
去对应的实体类中查找相应列名的set方法设值 , 由于找不到setlastName() , 所以lastName返回null. - 解决方法:
方法一:为列名指定别名 , 别名和java实体类的属性名一致 。
<select id="getUserList" resultType="com.xxx.pojo.User">
select id , last_name as lastName , eamil from user where id = #{id}
</select>
方法二:使用结果集映射->ResultMap 【推荐】
<select id="getUserList" resultType="com.xxx.pojo.User" resultMap="userMap">
/*执行sql*/
select * from user
</select>
<resultMap id="getUserList" type="com.xxx.pojo.User">
<result column="last_name" property="name"></result>
</resultMap>
(注意:除了select操作,其他三种都要提交事务,否则不会报错,但是会执行失败!)
作用域(Scope)和生命周期
理解我们目前已经讨论过的不同作用域和生命周期类是至关重要的,因为错误的使用会导致非常严重的并发问题。
我们可以先画一个流程图,分析一下Mybatis的执行过程!
-
作用域理解
- SqlSessionFactoryBuilder 的作用在于创建 SqlSessionFactory,创建成功后,SqlSessionFactoryBuilder 就失去了作用,所以它只能存在于创建 SqlSessionFactory 的方法中,而不要让其长期存在。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)
- SqlSessionFactory 可以被认为是一个数据库连接池,它的作用是创建 SqlSession 接口对象。因为 MyBatis 的本质就是 Java 对数据库的操作,所以 SqlSessionFactory 的生命周期存在于整个 MyBatis 的应用之中,所以一旦创建了 SqlSessionFactory,就要长期保存它,直至不再使用 MyBatis 应用,所以可以认为 SqlSessionFactory 的生命周期就等同于 MyBatis 的应用周期
- 由于 SqlSessionFactory 是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多个 SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据库资源的控制,也会导致数据库连接资源被消耗光,出现系统宕机等情况,所以尽量避免发生这样的情况
- 因此在一般的应用中我们往往希望 SqlSessionFactory 作为一个单例,让它在应用中被共享。所以说 SqlSessionFactory 的最佳作用域是应用作用域
- 如果说 SqlSessionFactory 相当于数据库连接池,那么 SqlSession 就相当于一个数据库连接(Connection 对象),你可以在一个事务里面执行多条 SQL,然后通过它的 commit、rollback 等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后,应该关闭这条连接,让它归还给 SqlSessionFactory,否则数据库资源就很快被耗费精光,系统就会瘫痪,所以用 try…catch…finally… 语句来保证其正确关闭
- 所以 SqlSession 的最佳的作用域是请求或方法作用域
日志工厂和分页
Mybatis内置的日志工厂提供日志功能,具体的日志实现有以下几种工具:
- Log4j 2
- Log4j
- Apache Commons Logging
- SLF4J
- JDK logging
部署日志可以在MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择其它日志实现。
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>
可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了 org.apache.ibatis.logging.Log 接口,且构造方法以字符串为参数的类完全限定名。
日志配置步骤:(LOG4J为例子)
- 使用maven导入log4j的包:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 配置文件编写(log4j.properties)放到resources下
#将等级为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 = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/xxx.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%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核心配置文件的configuration标签下设置日志实现:
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="logImpl" value="LOG4J"/>
</settings>
- 在程序中使用log4j进行输出:
//注意导包:org.apache.log4j.Logger
static Logger logger = Logger.getLogger(MyTest.class);
@Test
public void testLog4j(){
logger.info("info:进入selectUser方法");
logger.debug("debug:进入selectUser方法");
logger.error("error: 进入selectUser方法");
SqlSession session = MybatisUtils.getSqlSession();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.getUserList();
for (User user: users){
System.out.println(user);
}
session.close();
}
- 分页的接口: (起始位置 = (当前页面 - 1 ) * 页面大小)
public interface UserMapper {
//分页
List<User> getLimit(Map<String,Integer> map);
}
- 接口的关系映射配置:
<!--分页-->
<select id="getLimit" parameterType="map" resultType="com.xxx.pojo.User" resultMap="userMap">/*因为之前设置过resultMap,此处只需要设置与其id值同即可*/
select * from user limit #{startIndex},#{pageSize}
</select>
- 测试:
@Test
public void getLimit(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
HashMap<String, Integer> map = new HashMap<String,Integer>();
map.put("startIndex",1);
map.put("pageSize",2);
List<User> userList = mapper.getLimit(map);
for (User user : userList) {
System.out.println(user);
}
}
- MyBatis也有一个分页插件:PageHelper,感兴趣的可以去了解一下。
Mybatis也可以使用注解进行简化开发,但是对于一些较复杂的关系,注解无法很好的完成,加上后面学习Spring后,Spring可以很好的整合MyBatis,所以MyBatis还是建议使用XML配置文件形式来开发。对注解感兴趣的同学可以自行去看。
MyBatis 数据库关系处理
数据库设计:
- 创建一个student表:
- 创建一个teacher表:
创建Student和Teacher实体类并提供get,set方法和构造器以及tostring方法:
Student实体类:
public class Student {
private int id;
private String name;
//多个学生需要关联一个老师
private Teacher teacher;
public Student() {
}
public Student(int id, String name, Teacher teacher) {
this.id = id;
this.name = name;
this.teacher = teacher;
}
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 Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", teacher=" + teacher +
'}';
}
}
Teacher实体类:
public class Teacher {
private int id;
private String name;
public Teacher() {
}
public Teacher(int id, String name) {
this.id = id;
this.name = name;
}
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;
}
@Override
public String toString() {
return "Teacher{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
注意:在Student类中,要表示多个学生关联一个老师,则应该创建一个Teacher类属性 teacher.
编写StudentMapper接口查询所有学生信息及其老师信息
public interface StudentMapper {
//查询所有学生信息以及其老师信息
public List<Student> getStudents();
}
在resources目录下编写StudentMapper.xml配置文件
注意:这里有个巨大的坑!
一般来说,我们编写的mapper接口和mapper.xml文件最好是同名而且是同一个包下的.但是mapper.xml文件不建议跟mapper接口一起放在java下的同一个包,如果包多了的话会显得缭乱,所以,都是将mapper.xml文件放在resources目录下的,我们可以创建一个相同的包名就可以了。
但是,创建相同包名的时候不要:com.xxx.mapper这种形式,这个只在java下有用,在resources下应该为:com/xxx/mapper 这样才行,否则会报mapper注册失败,即找不到资源文件的错误。
这是血的教训,我找了很久才解决,切记切记!!!!!
StudentMapper.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">
<!--namespace==绑定一个对应的Dao/Mapper接口,以后Mapper.xml文件都放在resourse下,
但是要建立一个跟Mapper接口相对应得包
注意!!这里有一个坑,当在resources下建立包时候,不要写为:com.xxx.dao
应该为:com/xxx/dao
-->
<mapper namespace="com.xxx.dao.StudentMapper">
<!--
=============================嵌套查询的方式一-===================================
使用如下数据库的嵌套查询可以输出结果,但是teacher的name为空,则下列方法不能查出
name为空,则应该用resultMap来
<select id="getStudents" resultType="com.xxx.pojo.Student">
select s.id,s.name,t.name from student s,teacher t where s.tid=t.id
</select>
-->
<!--
<select id="getStudents" resultMap="studentMap">
select * from student
</select>
<resultMap id="studentMap" type="com.xxx.pojo.Student">
<!–resultMap里面不需要对同名的,以及常规类型的属性进行处理
只需处理不同名的,以及其他属性,如这里的Teacher对象属性
对象使用association,集合使用collection,其标签里面的属性javaType是对象的类型(全类名)
select标签写要关联的子查询id–>
<association column="tid" property="teacher" javaType="com.xxx.pojo.Teacher" select="getTeacher"></association>
</resultMap>
<select id="getTeacher" resultType="com.xxx.pojo.Teacher">
select * from teacher where id=#{tid}
</select>
-->
<!--===================================嵌套查询的方式二(按照结果嵌套查询(推荐))=====================================
第二种是直接使用sql嵌套查询语句,但是一定要给要查询的字段名起一个别名
接着使用resultMap映射,在里面将常规结果使用result/id完成不同名的映射
使用association或者collection分别对对象或者集合进行映射,这时候里面不需要column属性
只要property实体类属性名和javaType的对象的全类名即可
再在里面将该嵌套的字段不同名的属性进行映射
-->
<select id="getStudents" resultMap="studentMap">
select s.id as sid,s.name as sname,t.name as tname from student s,teacher t where s.tid=t.id
</select>
<resultMap id="studentMap" type="com.xxx.pojo.Student">
<result column="sid" property="id"></result>
<result column="sname" property="name"></result>
<association property="teacher" javaType="com.xxx.pojo.Teacher">
<result column="tname" property="name"></result>
</association>
</resultMap>
</mapper>
以上有两种方法实现:一种是按查询嵌套处理,另一种是按结果嵌套处理。按照查询进行嵌套处理就像SQL中的子查询,按照结果进行嵌套处理就像SQL中的联表查询。
** 关联(association)**元素处理“有一个”类型的关系。 比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
-
嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
-
嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集
首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 select 和 resultMap 属性上有所不同。
property 映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。 无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
javaType 一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
关联的嵌套 Select 查询
column 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
select 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。 具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
关联的嵌套结果映射
resultMap 结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 有部分数据是重复的。 为了将结果集正确地映射到嵌套的对象树中, MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。
在MyBatis核心配置文件中绑定mapper
<!--每一个mapper.xml都需要在Mybatis核心配置文件中配置-->
<mappers>
<mapper resource="com/xxx/dao/StudentMapper.xml"/>
<mapper resource="com/xxx/dao/TeacherMapper.xml"/>
</mappers>
测试:
public class StudentMapperTest {
static Logger logger = Logger.getLogger(StudentMapperTest.class);
@Test
public void getStudents(){
long start = System.currentTimeMillis();
//1:获取SqlSession对象
SqlSession sqlSession = MybatisUtils.getSqlSession();
//方式1:getMapper,执行sql
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.getStudents();
for (Student student : students) {
System.out.println(student);
}
long end = System.currentTimeMillis();
System.out.println(end-start);
//3:关闭sqlSession
sqlSession.close();
}
}
数据库还是使用同一个
对应实体类
Student类:
public class Student {
private int id;
private String name;
//一个学生对应一个老师,则不用对象类型,直接int tid
private int tid;
public Student() {
}
public Student(int id, String name, int tid) {
this.id = id;
this.name = name;
this.tid = 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;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", tid=" + tid +
'}';
}
}
Teacher类:
public class Teacher {
private int id;
private String name;
//一个老师有多个学生,应该是有list集合
private List<Student> students;
public Teacher() {
}
public Teacher(int id, String name, List<Student> students) {
this.id = id;
this.name = name;
this.students = students;
}
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 List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
@Override
public String toString() {
return "Teacher{" +
"id=" + id +
", name='" + name + '\'' +
", students=" + students +
'}';
}
}
注意:在Teacher类中,要表示一个老师有多个学生,则应该创建一个带泛型的List集合属性students
编写TeacherMapper接口
public interface TeacherMapper {
List<Teacher> getTeacher(int id);
}
编写TeacherMapper.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">
<!--namespace==绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.xxx.dao.TeacherMapper">
<select id="getTeacher" resultMap="teacherMap" parameterType="_int">
select s.id as sid,s.name as sname,t.name as tname,t.id as tid
from student s,teacher t
where s.tid=t.id
and t.id=#{tid}
</select>
<resultMap id="teacherMap" type="com.xxx.pojo.Teacher">
<result column="tid" property="id"></result>
<result column="tname" property="name"></result>
<collection property="students" ofType="com.xxx.pojo.Student">
<result column="sid" property="id"></result>
<result column="sname" property="name"></result>
<result column="tid" property="tid"></result>
</collection>
</resultMap>
</mapper
一对多其实跟多对一差不多,区别在于:
-
多对一时:学生实体类用将老师对象封装为属性,使用association进行绑定,用的是JavaType,association里面映射老师属性,JavaType是用来指定pojo中属性的类型
-
一对多时:老师实体类将学生集合封装为属性,使用collection进行绑定,用的是ofType,collection里面映射学生属性,ofType指定的是映射到list集合属性中pojo的类型
集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分
在MyBatis核心配置文件中绑定mapper
<!--每一个mapper.xml都需要在Mybatis核心配置文件中配置-->
<mappers>
<mapper resource="com/xxx/dao/StudentMapper.xml"/>
<mapper resource="com/xxx/dao/TeacherMapper.xml"/>
</mappers>
测试:
public class TeacherMapperTest {
@Test
public void getTeacher(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
List<Teacher> teacher = mapper.getTeacher(1);
for (Teacher teacher1 : teacher) {
System.out.println(teacher1);
}
sqlSession.close();
}
}
动态SQL
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
环境搭建
- 提供一个user数据库表,里面有id,author,title,state属性,并且提供get和set,tostring方法和构造器。
- 编写对应的接口类
- 在接口类的.xml文件进行配置:具体如下:
<!--根据条件动态查询if-->
<select id="queryUserIf" resultType="com.xxx.pojo.User" parameterType="map">
select * from user
<where>
/*这里可以使用where标签,或者直接用where 1=1*/
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</where>
</select>
queryUserIf对应接口的一个查询方法,根据作者名字和博客名字来查询博客!如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询。
如果 author 等于 null,那么查询语句为 select * from user where title=#{title},由于使用了“where”标签,它会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。此外,如果标签返回的内容是以AND 或OR 开头的,则它会剔除掉。则,如果title为空,那么查询语句为 select * from user where author=#{author}。
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
<!--choose,相当于swtich,case:break,只要有满足就输出,否则执行otherwise(所以其实这里后面的and可以不要,因为只有一个满足,拼接上就知道了)-->
<select id="queryUserChoose" parameterType="map" resultType="com.xxx.pojo.User">
select * from user
<where>
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
and author = #{author}
</when>
<otherwise>
and state = #{state}
</otherwise>
</choose>
</where>
</select>
以上例子,传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,则返回state值。
<!--Set 会在sql自动添加set,且将set语句后面多余的逗号去除-->
<update id="updateUserSet" parameterType="map">
update user
<set>
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author}
</if>
</set>
where id = #{id};
</update>
set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)比如:需要查询user表中 id 分别为1,2,3的博客信息
<select id="queryUserForeach" parameterType="map" resultType="user">
select * from user
<where>
<!--
collection:指定输入对象中的集合属性
item:每次遍历生成的对象
open:开始遍历时的拼接字符串
close:结束时拼接的字符串
separator:遍历对象之间需要拼接的字符串
select * from user where 1=1 and (id=1 or id=2 or id=3)
-->
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id=#{id}
</foreach>
</where>
</select>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况 下,该属性的值是不一样的,主要有一下3种情况:
1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list
2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array
3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了
- 上述collection的值为ids,是传入的参数Map的key,对应的mapper测试为:
@Test
public void testQueryUserForeach(){
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
HashMap map = new HashMap();
List<Integer> ids = new ArrayList<Integer>();
ids.add(1);
ids.add(2);
ids.add(3);
map.put("ids",ids);
List<User> users = mapper.queryUserForeach(map);
System.out.println(users);
session.close();
}
- 单参数array数组的类型:
1 <select id="queryUserForeach" parameterType="java.util.ArrayList" resultType="User">
2 select * from user where id in
3 <foreach collection="array" index="index" item="item" open="(" separator="," close=")">
4 #{item}
5 </foreach>
6 </select>
- 上述collection为array,对应的Mapper测试为:
@Test
public void queryUserForeachTest() {
SqlSession session = MybatisUtils.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
int[] ids = new int[] {1,3,6,9};
List users = userMapper.queryUserForeach(ids);
for (User user : users)
System.out.println(user);
session.close();
}
至此,我们已经完成了与 XML 配置及映射文件相关的讨论。其实动态 sql 语句的编写往往就是一个拼接的问题,为了保证拼接准确,我们最好首先要写原生的 sql 语句出来,然后在通过 mybatis 动态sql 对照着改,防止出错。多在实践中使用才是熟练掌握它的技巧。
MyBatis缓存
-
什么是缓存(Cache)?
缓存就是用来提高查询访问速度,就是将每次的访问记录缓存在一地方,在下次进行查询时首先访问的不是数据库,而是缓存,如果在缓存中查询到了该次查询条件的结果集就直接返回结果,不再访问数据库。 -
为什么使用缓存?
减少和数据库的交互次数,减少系统开销,提高系统效率。 -
缓存的使用场景?
经常查询并且不经常改变的数据
MyBatis缓存
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
-
默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
-
二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
-
为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
-
一级缓存
-
同一个 SqlSession 对象, 在参数和 SQL 完全一样的情况先, 只执行一次 SQL 语句(如果缓存没有过期)
-
与数据库同一次会话期间查询到的数据会放在本地缓存中。
-
以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;
-
一级缓存失效的四种情况:
1、sqlSession不同(每个sqlSession中的缓存相互独立)
2、sqlSession相同,查询条件不同
3、sqlSession相同,两次查询之间执行了增删改操作!(因为增删改操作可能会对当前数据产生影响)
4、sqlSession相同,手动清除一级缓存(如在配置文件中配置:flushCache=“true”,则会刷新缓存) -
二级缓存(要手动开启)
具体步骤如下:
- 开启全局缓存 【mybatis-config.xml】
<setting name="cacheEnabled" value="true"/>
- 去每个mapper.xml中配置使用二级缓存
<cache/>
官方示例=====>查看官方文档
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
只要开启了二级缓存,我们在同一个Mapper中的查询,可以在二级缓存中拿到数据
查出的数据都会被默认先放在一级缓存中
只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中