一. 什么是Mybatis
- Mybatis是一款优秀的持久层框架,它支持定制SQL、存储过程以及高级映射。
- Mybatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
- Mybatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJO映射成数据库中的记录。
二. 回顾JDBC连接数据库操作
package com.example.zhang;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import com.example.zhang.pojo.User;
public class JdbcTest {
public static void main(String[] args) {
PreparedStatement preparedStatement;
ResultSet resultSet;
try {
// 1. 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "root");
// 3. 定义sql
String sql = "select * from user where id = ?";
// 4. 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 5. 设置参数(序号从1开始)
preparedStatement.setInt(1, 2);
// 6. 执行sql
resultSet = preparedStatement.executeQuery();
// 7. 遍历结果集
while (resultSet.next()) {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setPwd(resultSet.getString("pwd"));
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过JDBC操作数据库的代码,可以看出有一些不好的地方:
- 在创建connection时,存在硬编码,也即是直接将连接信息写死,不方便后期维护
- sql语句存在硬编码问题,不利于维护,一旦SQL语句修改,需要对java重新编译
- 每进行一次数据库连接后都会关闭数据库连接,频繁开启/关闭数据库连接影响性能
- resultSet遍历结果集数据时,存在硬编码,将获取表的字段进行硬编码,不利于系统维护
而Mybatis就是来解决掉这些问题的。
Mybatis是将数据库连接信息写在配置文件中,不存在硬编码问题;
mybatis执行的sql语句也是通过配置文件进行配置,不需要写在Java代码中;
mybatis的连接池管理、缓存管理等让数据库和查询数据效率更高
三. 第一个MyBatis程序
-
创建测试表
CREATE TABLE `user` ( id INT(10) NOT NULL, NAME VARCHAR(20) DEFAULT NULL, pwd VARCHAR(20) DEFAULT NULL, PRIMARY KEY(`id`) )ENGINE=INNODB DEFAULT CHARSET=utf8; INSERT INTO USER (id, NAME, pwd) VALUES (1, '张三', '123456'), (2, '李四', 'abcd'), (3, '王五', 'abc123');
-
导包
org.mybatis mybatis 3.5.6 mysql mysql-connector-java 5.1.5 -
创建对应的javaBean
package com.example.zhang.pojo; public class User { private int id; private String name; private String pwd; public int getId() { return this.id; } public void setId(int id) { this.id = id; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getPwd() { return this.pwd; } public void setPwd(String pwd) { this.pwd = pwd; } @Override public String toString() { return "{" + " id='" + getId() + "'" + ", name='" + getName() + "'" + ", pwd='" + getPwd() + "'" + "}"; } }
-
创建mybatis核心配置文件
-
编写Mybatis工具类
package com.example.zhang.utils; import java.io.IOException; import java.io.InputStream; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; // MyBatis工具类 public class MyBatisUtils { private static SqlSessionFactory sqlSessionFactory; static { /* 根据全局配置文件,利用SqlSessionFactoryBuilder创建SqlSessionFactory */ try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } /* 使用SqlSessionFactory获取SqlSession对象。 一个SqlSession对象代表和数据库的一次会话。 */ public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }
-
编写DAO接口类(命名为Mapper)
package com.example.zhang.dao; import java.util.List; import com.example.zhang.pojo.User; public interface UserMapper { List
selectUser(); } -
编写Mapper的xml配置文件(以前是写DAO接口实现类,现在不需要,只写对应的xml配置文件)
-
编写测试类
package com.example.zhang; import java.util.List; import com.example.zhang.dao.UserMapper; import com.example.zhang.pojo.User; import com.example.zhang.utils.MyBatisUtils; import org.apache.ibatis.session.SqlSession; import org.junit.jupiter.api.Test; /** * Unit test for simple App. */ public class AppTest { @Test public void selectUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); // 方法一 不推荐 // List
users = session.selectList("com.example.zhang.dao.UserMapper.selectUser"); // 方法二 UserMapper mapper = session.getMapper(UserMapper.class); List users = mapper.selectUser(); for (User user : users) { System.out.println(user); } session.close(); } } -
说明
- SQLSession的实例不是线程安全的,因此是不能被共享的
- SQLSession每次使用完成后需要正确的关闭,这个关闭操作是必须的
- 推荐使用SqlSession获取到DAO接口的代理类,执行代理对象的方法,可以更安全的进行类型检查操作(也即是上面测试类的第二种方式)
四. MyBatis全局核心配置文件
MyBatis的配置文件包含了影响MyBatis行为甚深的设置(settings)和属性(properties)信息。
能配置的内容如下:(元素节点必须按顺序,否则会报错)
- configuration配置
- properties 属性
- settings 设置
- typeAliases 类型命名
- typeHandlers 类型处理器
- objectFactory 对象工厂
- plugins 插件
- environments 环境
- envionment 环境变量
- transactionManager 事务管理器
- dataSource 数据源
- envionment 环境变量
- databaseIdProvider 数据库厂商标识
- mappers 映射器
4.1 properties属性
prop.driver=com.mysql.jdbc.Driver
prop.url=jdbc:mysql:///mybatis
prop.username=root
prop.password=root
数据库连接参数可以在外部进行配置,并可以进行动态替换。
我们既可以在典型的Java属性文件中配置这些属性(就是db.properties)
也可以在外部属性文件的基础上,在properties元素的子元素中设置
最后这些设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值,比如
还可以在SqlSessionFactoryBuilder.build()方法中传入属性值,例如:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);
// ... 或者 ...
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);
如果属性在不只一个地方进行了配置,那么Mybatis将按照下面的顺序来加载:
- 在properties元素体内指定的属性先被读取
- 然后根据properties元素中的resource属性读取类路径下属性文件,或根据url属性指定的路径读取属性文件,并覆盖已读取的同名属性
- 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性
4.2 settings属性
settings属性设置是Mybatis中极为重要的,它们会改变Mybatis的运行时行为
设置名 | 描述 | 有效值 | 默认值 |
---|---|---|---|
cacheEnabled | 该配置影响所有映射器中配置的缓存的全局开关 | true|false | true |
lazyLoadingEnabled | 延迟加载的全局开关,当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 | true|false | false |
mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则映射,即从经典数据库列名A_COLUMN到经典Java属性名aColumn的类似映射 | true|false | false |
logImpl | 指定Mybatis缩影日志的具体实现,未指定时将自动查找 | SLF4J |LOG4J |LOG4J2 |JDK_LOGGING |COMMONS_LOGGING |STDOUT_LOGGING |NO_LOGGING | 未设置 |
4.3 typeAliases别名处理器
-
类型别名是为Java类型设置一个短的名字,可以方便我们引用某个类。
它仅用于XML配置,存在的意义在于降低冗余的全限定类名书写。比如:
当这样配置后,
employee
和department
可以用在任何使用com.example.zhang.Employee
和com.example.zhang.Department
的地方 -
在类很多的情况下,可以批量设置别名,为这个包下的每一个类创建一个默认的别名,默认是简单类名小写,若有注解@Alias,则别名为注解值
@Alias("hello") public class Employee { ... }
若是这样,Employee类的别名就是hello。
-
需要注意的是,Mybatis已经为许多常见的Java类型内建了相应的别名,它们都是大小写不敏感的,我们在起别名时不可占用已有的别名
4.4 typeHandlers 类型处理器(了解即可)
**无论是Mybatis在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成Java类型**。
我们也可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。
4.5 对象工厂(objectFactory)(了解即可)
Mybatis每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。
默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法来实例化。
如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。
4.6 plugins 插件
插件是Mybatis提供的一个非常强大的机制,我们可以通过插件来修改Mybatis的一些核心行为。**插件通过动态代理机制**,可以介入四大对象的任何一个方法的执行。(以后再学)
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
4.7 environments 环境配置
- MyBatis可以配置多种环境,比如开发,测试和生产环境需要有不同的配置
- 每种环境使用一个
environment
标签进行配置并指定唯一标识符 - 可以通过
environments
标签中的default属性指定一个环境的标识符来快速切换环境
4.7.1 environment -指定具体环境
- id:指定当前环境的唯一标识
- transactionManager:事务管理器
- type:JDBC | MANAGED
- JDBC:使用JDBC的提交和回滚设置,依赖于从数据源得到的连接来管理事务范围
- MANAGED:不提交或回滚一个连接,让容器来管理事务的整个生命周期
- type:JDBC | MANAGED
- dataSource:数据库连接源
- type:UNPOOLED | POOLED | JNDI
- UNPOOLED:不使用连接池
- POLLED:使用连接池
- JNDI:在EJB或应用服务器这类容器中查找指定的数据源
- type:UNPOOLED | POOLED | JNDI
实际开发中,我们使用Spring管理数据源,并进行事务控制的配置来覆盖上述配置
4.8 mappers 映射器
在自动查找资源方面,Java并没有提供一个很好的解决方案,所以最好的方法是直接告诉Mybatis去哪里找映射文件,mappers映射器就是用来定义映射SQL语句文件的。
我们可以使用相对于类路径的资源引用,或完全限定资源定位符(包括`file:///`形式的URL),或类名和包名。
使用相对于类路径的资源引用:
使用完全限定资源定位符(URL):
使用映射器接口实现类的完全限定类名(需要配置文件名称和接口名称一致,并且位于同一目录下):
将包内的映射器接口实现全部注册为映射器(需要配置文件名称和接口一致,并且位于统一目录下):
五.Mapper映射文件
映射文件指导着Mybatis如何进行数据库增删改查,有着非常重要的意义。
SQL映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
- cache:命名空间的二级缓存配置
- cache-ref:其他命名空间缓存配置的引用
- resultMap:自定义结果集映射
parameterMap:已废弃!老式风格的参数映射- sql:抽取可重用语句块
- insert:映射插入语句
- update:映射更新语句
- delete:映射删除语句
- select:映射查询语句
namespace:命名空间,必须跟某个接口同名,接口中的方法与映射文件中sql语句id应该一一对应。
5.1 select
select标签是mybatis中最常用的标签之一
-
select语句有很多属性可以详细配置每一条SQL语句
重点关注几个:
- id:唯一标识符,用来引用这条语句,需要和接口的方法名一致
- parameterType:参数类型,可以不传,Mybatis会根据TypeHandler自动推断
- resultType:返回值类型,别名或者全类名,如果返回的是集合,定义集合中元素的类型,不能和resultMap同时使用
需求:根据id查找用户
-
在接口类UserMapper中添加对应的接口方法
public interface UserMapper { List
selectUser(); // 根据ID查找用户 User findUserById(int id); } -
在映射文件UserMapper.xml中添加select语句
-
编写测试代码
@Test public void findUserByIdTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.findUserById(2); System.out.println(user); session.close(); }
5.2 insert,update和delete
数据变更语句insert,update和delete的实现非常接近,和select使用差不多。
需求一:增加一个用户
-
在UserMapper接口中新增对应的方法
public interface UserMapper { List
selectUser(); // 根据ID查找用户 User findUserById(int id); // 新增一个用户 int addUser(User user); } -
在映射文件UserMapper.xml中添加insert语句
insert into user values(#{id}, #{name}, #{pwd}) -
编写测试代码
@Test public void addUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User(5, "老虎", "abcd123"); userMapper.addUser(user); session.commit(); session.close(); }
注意:增,删,改操作需要提交事务。
需求二:修改用户信息
-
在UserMapper接口中新增对应的方法
public interface UserMapper { List
selectUser(); // 根据ID查找用户 User findUserById(int id); // 新增一个用户 int addUser(User user); // 修改用户信息 int updateUser(User user); } -
在映射文件UserMapper.xml中添加update语句
update user set name = #{name}, pwd = #{pwd} where id = #{id} -
编写测试代码
@Test public void updateUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User(5, "大熊猫", "123456"); userMapper.updateUser(user); session.commit(); session.close(); }
注意:增,删,改操作需要提交事务。
需求三:删除指定ID的用户
-
在UserMapper接口中新增对应的方法
public interface UserMapper { List
selectUser(); // 根据ID查找用户 User findUserById(int id); // 新增一个用户 int addUser(User user); // 修改用户信息 int updateUser(User user); // 删除指定用户 int deleteUserById(int id); } -
在映射文件UserMapper.xml中添加delete语句
delete from user where id = #{id} -
编写测试代码
@Test public void deleteUserByIdTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); userMapper.deleteUserById(5); session.commit(); session.close(); }
注意:增,删,改操作需要提交事务。
以上增删改操作都需要提交事务才生效,每次都是要在最后加上一句session.commit()
.
也可以直接在openSession时候默认自动提交事务,修改MyBatisUtils工具类:在openSession传递参数true
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession(true);
}
六. 参数传递
-
单个参数
可以接受基本类型,对象类型,集合类型的值,这种情况Mybatis可以直接使用这个参数,不需要经过任何处理
-
多个参数
任意多个参数,都会被Mybatis重新包装成一个Map传入,Map的key就是param1,param2...,值就是参数的值
-
命名参数
可以为参数使用
@Param
起一个名字,Mybatis就会将这些参数封装金Map中,key就是我们自己指定的名字 -
Map(万能的map)
我们也可以封装多个参数为map,直接传递一个map
七. 结果映射(初级)
-
resultMap
元素是Mybatis中最重要最强大的元素,它可以让你从90%的JDBCResultSets
数据提取代码中解放出来。 - ResultMap的设计事项时,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
- resultMap就是用来解决结果集属性名与数据库列名不一致导致获取不到数据的问题。
下面来看一个例子:
数据库中三个字段属性名分别为:id,name,pwd
-
Java中实体类设计如下:
public class Customer { private int id; private String name; private String password; // 构造函数,get/set ,toString()省略 ... }
-
接口方法
public interface CustomerMapper { Customer selectCustomerById(int id); }
-
mapper映射文件
-
测试结果
查询结果发现password为空,说明出现问题。
分析:
sql语句:
select * from user where id = #{id}
实质上可以看做select id,name,pwd from user where id = #{id}
,mybatis会根据这些查询的列名(列名会转化为小写,数据库不区分大小写)去对应的实体类中查找相应列名的set方法设置值,由于找不到setPwd(),所以password返回null。这种就是数据库列名与java类属性名不一致的表现,有这两种解决方案:
-
使用别名,为列名指定别名,别名和java类属性名一致
-
使用结果集映射ResultMap
-
八. 日志
我们在开发过程中,如果出现了异常需要排错,如果有日志,能帮助我们快速定位问题。
Mybatis通过使用内置的日志工程提供日志功能,内置的日志工程有以下几种实现:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
Mybatis具体选择哪个日志实现工具是由mybatis的内置日志工程确定的,它会使用最先找到的(按以上的顺序查找),如果一个都没找到,就会禁用日志功能
不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging。注意,在这种配置环境下,MyBatis 会把 Commons Logging 作为日志工具。这就意味着在诸如 WebSphere 的环境中,由于提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。这个时候你就会感觉很郁闷:看起来 MyBatis 将你的 Log4J 配置忽略掉了(其实是因为在这种配置环境下,MyBatis 使用了 Commons Logging 作为日志实现)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志实现,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择其它日志实现。
...
...
logImpl可选的值有:
- SLF4J
- LOG4J(掌握)
- LOG4J2
- JDK_LOGGING
- COMMONS_LOGGING
- STDOUT_LOGGING
- NO_LOGGING
在Mybatis中具体使用哪一个日志实现,在设置settings中设置
-
标准日志实现
运行测试程序,终端会输出日志:
-
Log4j
简介:
- Log4j是Apache的一个开源项目
- 通过使用Log4j,我们可以控制日志信息输送的目的地,可以是控制台,文本,GUI组件等
- 我们也可以控制每一条日志的输出格式
- 通过定义每一条日志信息的级别,能够更加细致地控制日志的生成过程。
- 通过配置文件灵活的进行配置,不需要修改应用的代码
使用步骤:
-
导入Log4j的包
log4j log4j 1.2.17 -
编写配置文件
#将等级为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/zhang.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
-
setting设置日志实现为LOG4J
-
测试程序中使用Log4j
public class AppTest { // 参数为当前类 static Logger logger = Logger.getLogger(AppTest.class); @Test public void findUserByIdTest() { logger.info("info: 进入findUserByIdTest方法"); logger.debug("debug: 进入findUserByIdTest方法"); logger.error("error: 进入findUserByIdTest方法"); SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.findUserById(2); System.out.println(user); session.close(); } }
输出结果如下:
不仅终端生成了日志信息,还生成了一个日志文件,在指定路径里./log/zhang.log
九. Mybatis分页实现
思考:为什么需要分页?
在使用数据库时,会经常对数据库进行增删改查操作,其中使用最多的是查询操作,查询大量数据的时候,我们往往使用分页查询,也就是每次处理小部分数据,这样对数据库压力就在可控范围内。
1. 使用Limit实现分页
语法:
select * from table limit startIndex, pageSize
select * from table limit 5, 10; # 检索记录行 6-15
# 为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为-1
select * from table limit 95, -1; # 检索记录行 96 - 末尾
# 如果只给定一个参数,它表示返回最大的记录行数目
select * from table limit 5; # 检索前5个记录行
# 相当于 limit n 等价于 limit 0, n
操作步骤:
-
添加接口方法,参数为map
public interface UserMapper { // 查找用户 List
selectUser(Map map); } -
修改Mapper文件
-
编写测试方法
推断:起始位置 = (当前页面 -1) 页面大小*
@Test public void testSelectUser() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); int currentPage = 2; // 第几页 int pageSize = 2; // 每页显示几个 Map
map = new HashMap (); map.put("startIndex", (currentPage -1) * pageSize); map.put("pageSize", pageSize); List users = userMapper.selectUser(map); for (User u:users) { System.out.println(u); } session.close(); }
2. RowBounds分页(了解即可)
使用RowBounds在Java代码层面实现分页
-
接口方法
List
getUserByRowBounds(); -
Mapper文件
-
测试方法
@Test public void testUserByRowBounds() { SqlSession session = MyBatisUtils.getSqlSession(); int currentPage = 2; // 第几页 int pageSize = 2; // 每页显示几个 RowBounds rowBounds = new RowBounds((currentPage -1) * pageSize, pageSize); // 通过session.* 方法传递rowBounds, 这种方法已经不推荐使用了 List
users = session.selectList("com.example.zhang.dao.UserMapper.getUserByRowBounds", null, rowBounds); for (User u:users) { System.out.println(u); } session.close(); }
3. PageHelper
这是一款Mybatis的分页插件,官网文档为:https://pagehelper.github.io/
十. 注解开发
Mybatis最初配置信息是基于XML的,映射语句SQL也是定义在XML中,但到了Mybatis 3提供了新的基于注解的配置,但Java注解的表达力和灵活性十分有限,并不能完全用注解来构建。
注解方式主要用于SQL,主要有几个注解:
@Select()
@Update()
@Insert()
@Delete()
注意:使用注解开发就不需要mapper.xml映射文件了
使用方法:
-
在接口方法中添加注解
@Select("select * from user") List
selectUser(); -
mybatis核心配置文件,使用class绑定接口(不需要mapper.xml文件了)
-
测试代码,与之前一样
@Test public void selectUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper mapper = session.getMapper(UserMapper.class); List
users = mapper.selectUser(); for (User user : users) { System.out.println(user); } }
注解方式实现增删改查:
注意增删改需要提交事务
-
编写接口方法
// 根据ID查找用户 @Select("select * from user where id = #{id}") @Results(id = "userMap", value = { @Result(id=true,column = "id",property = "id"), @Result(column = "name",property = "name"), @Result(column = "pwd", property = "password") }) User findUserById(@Param("id")int id); // 新增一个用户 @Insert("insert into user (id,name,pwd) values(#{id},#{name},#{password})") @ResultMap(value = {"userMap"}) int addUser(User user); // 修改用户信息 @Update("update user set name=#{name},pwd=#{password} where id = #{id} ") int updateUser(User user); // 删除指定用户 @Delete("delete from user where id = #{id}") int deleteUserById(int id);
这里特意将数据库列名pwd与实体类属性名password设置为不一致,在XML配置中我们可以使用resultMap结果集映射的方式解决,但在注解方式中,就得使用注解
@Results,@ResultMap,@Result
实现了@Results()注解
:有两个常用的参数,一个是id,一个是value。
id的作用在于唯一标记这个Results注解,在其他接口抽象方法中需要通过@Results注解来解决 属性名与字段名不一致问题时,能被引用。
value的作用在于使用@Result注解建立实体类与数据库表映射关系,可以写多个@Result,如果是主键字段,@Result注解中需要设置id=true,比如上面例子的id是主键,column代表数据库字段名,property代表实体类属性名
@ResultMap()注解:
用于引用@Results注解,value值为数据,是@Results注解的id值
-
测试方法
@Test public void findUserByIdTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.findUserById(2); System.out.println(user); session.close(); } @Test public void addUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User(9, "华南虎", "abcd123"); userMapper.addUser(user); session.close(); } @Test public void updateUserTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User(5, "大熊猫", "123456789"); userMapper.updateUser(user); session.commit(); session.close(); } @Test public void deleteUserByIdTest() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); userMapper.deleteUserById(5); session.commit(); session.close(); }
关于注解@Param
@Param
注解用于给方法参数起一个名字,以下是使用原则总结:
- 在方法只接受一个参数的情况下,可以不适用@Param
- 在方法接受多个参数的情况下,建议一定要用@Param注解给参数命名
- 如果参数是JavaBean,则不能使用@Param
- 不使用@Param注解时,参数只能有一个,并且是JavaBean
# 与 $ 的区别
- #{}的作用主要是替换预编译语句(PrepareStatement)中的占位符? (推荐使用)
- ${}的作用是直接进行字符串替换
总结
**使用注解和配置文件协同开发,才是Mybatis的最佳实践!**
十一. Mybatis执行流程
Mybatis使用Mapper调用接口方法,本质是反射机制实现,底层使用动态代理
Mybatis详细的执行流程:
十二. 结果映射(高级)
1. 多对一处理
多对一的理解:
- 多个学生对应一个老师
- 对于学生这边而言,是关联,多个学生关联一个老师(多对一)
- 对于老师而言,是集合,一个老师有很多学生(一对多)
(1)设计数据库:
# 老师表
CREATE TABLE teacher (
id INT(10) NOT NULL,
NAME VARCHAR(30) DEFAULT NULL,
PRIMARY KEY(id)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO teacher(id,NAME) VALUE(1,'张老师');
# 学生表
CREATE TABLE student (
id INT(10) NOT NULL,
NAME VARCHAR(30) DEFAULT NULL,
tid INT(10) DEFAULT NULL,
PRIMARY KEY(id),
KEY fktid(tid) ,
CONSTRAINT fktid FOREIGN KEY(tid) REFERENCES teacher(id)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO student(id, NAME, tid) VALUES(1, '小明', 1);
INSERT INTO student(id, NAME, tid) VALUES(2, '小红', 1);
INSERT INTO student(id, NAME, tid) VALUES(3, '小米', 1);
INSERT INTO student(id, NAME, tid) VALUES(4, '小李', 1);
INSERT INTO student(id, NAME, tid) VALUES(5, '小王', 1);
两个表的对应关系:
(2)添加实体类:
public class Teacher {
private int id;
private String name;
// 构造器,get/set方法,toString 省略...
}
public class Student {
private int id;
private String name;
// 多个学生可以是同一个老师,多对一
private Teacher teacher;
// 构造器,get/set方法,toString 省略...
}
(3)编写实体类对应的Mapper接口类
无论有没有需求,都应该写上,以备后来之需
public interface TeacherMapper {
}
public interface StudentMapper {
}
(4) 编写对应的Mapper.xml文件
无论有没有需求,都应该写上,以备后来之需
(5) 按查询嵌套处理(子查询)
-
给StudentMapper接口添加方法
public interface StudentMapper { // 获取所有学生及对应老师信息 List
getStudents(); } -
编写对应的Mapper文件(记得要在mybatis-config.xml中注册Mapper)
-
测试
@Test public void test3() { SqlSession session = MyBatisUtils.getSqlSession(); StudentMapper studentMapper = session.getMapper(StudentMapper.class); List
students = studentMapper.getStudents(); for (Student s: students) { System.out.println(s); } session.close(); }
(6)按结果嵌套处理(联表查询)
-
接口方法
List
getStudents1(); -
Mapper文件
(7)小结
按照查询进行嵌套处理就像SQL中的子查询
按照结果进行嵌套处理就像SQL中的联表查询
2. 一对多处理
一对多的理解:
- 一个老师拥有多个学生
- 对于老师这边,就是一个一对多的现象,即从一个老师下面有一群学生(集合)
按查询嵌套处理(子查询)
(1) 实体类编写
public class Teacher {
private int id;
private String name;
private List students;
// 构造器,get/set方法,toString 省略...
}
public class Student {
private int id;
private String name;
private int tid;
// 构造器,get/set方法,toString 省略...
}
(2) TeacherMapper接口编写方法
public interface TeacherMapper {
// 获取指定老师,以及老师下的所有学生
public Teacher getTeacher(int id);
}
(3) 编写接口对应的Mapper文件
(4)测试
@Test
public void test4() {
SqlSession session = MyBatisUtils.getSqlSession();
TeacherMapper teacherMapper = session.getMapper(TeacherMapper.class);
Teacher teacher = teacherMapper.getTeacher(1);
System.out.println(teacher);
session.close();
}
按结果嵌套处理(联表查询)
(1)编写接口对应的Mapper文件
十三. 动态SQL
-
介绍
什么是动态SQL?
动态SQL指的是根据不同的查询条件,生成不同的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 -------------------------------
-
搭建环境
-
创建数据库
CREATE TABLE blog ( id VARCHAR(50) NOT NULL COMMENT '博客id', title VARCHAR(100) NOT NULL COMMENT '博客标题', author VARCHAR(30) NOT NULL COMMENT '博客作者', create_time DATETIME NOT NULL COMMENT '创建时间', views INT(30) NOT NULL COMMENT '浏览量' )ENGINE=INNODB DEFAULT CHARSET=utf8
-
创建IDUtils工具类(用来生产博客id)
import java.util.UUID; public class IDUtils { public static String genId() { return UUID.randomUUID().toString().replaceAll("-", ""); } }
-
编写实体类
public class Blog { private String id; private String title; private String author; private Date createTime; private int views; // 省略get/set toString()犯法 // ... }
注意这里的属性createTime与数据库的列create_time不是一致!后面做处理
-
编写Mapper接口类,添加一个方法,新增博客,用于初始化
public interface BlogMapper { // 新增一个博客 public int addBlog(Blog blog); }
-
编写对应的Mapper.xml
insert into blog(id, title, author, create_time, views) values(#{id}, #{title}, #{author}, #{createTime}, #{views}); -
mybatis-config核心配置文件,增加设置:下划线驼峰自动转换,用来解决上面数据库列名create_time和实体类属性名createTime不一致问题,会自动转换。
注册mapper文件
.... -
因为ID使用了随机的UUID生产,这里先初始化一些数据库数据
@Test public void addBlogTest() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper blogMapper = session.getMapper(BlogMapper.class); Blog blog = new Blog(); blog.setId(IDUtils.genId()); blog.setAuthor("张三"); blog.setTitle("Mybatis如此简单"); blog.setCreateTime(new Date()); blog.setViews(9999); blogMapper.addBlog(blog); blog.setId(IDUtils.genId()); blog.setTitle("Java如此简单"); blogMapper.addBlog(blog); blog.setId(IDUtils.genId()); blog.setTitle("Spring如此简单"); blogMapper.addBlog(blog); blog.setId(IDUtils.genId()); blog.setTitle("Python如此简单"); blogMapper.addBlog(blog); session.close(); }
-
-
if 标签
需求:根据作者名字和博客名字来查询博客,如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询
-
编写接口类
// 使用万能Map作为传参 List
queryBlogIf(Map map); -
编写SQL语句
-
测试代码
@Test public void testQueryBlogIf() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper blogMapper = session.getMapper(BlogMapper.class); HashMap
map = new HashMap<>(); map.put("title", "Mybatis如此简单"); map.put("author", "张三"); List blogs = blogMapper.queryBlogIf(map); System.out.println(blogs); session.close(); }
分析:
上面查询结果是两个条件都提供了,title和author,也能正确查询出结果。
但是只用了
if
语句,再看这个SQL语句,如果author为null,title不为null,那查询语句就会变成:select * from blog where title = #{title}
,刚好可以执行。如果title为null的情况下呢?那查询语句就变成了
select * from blog where and author = #{author}
,这就是错误的sql语句了,显然这样子只依赖if
语句是不行的,需要配合另一个where
语句,下面讲解。 -
-
where 标签
-
修改上面的SQL语句,使用
where
标签where标签会知道如果它包含的标签中有返回值的话,他就插入一个where。如果标签返回的内容是以and或者or开头的,它就会将and或or剔除,保证sql的正确
-
测试
title 为null,author不为null
@Test public void testQueryBlogIf() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper blogMapper = session.getMapper(BlogMapper.class); HashMap
map = new HashMap<>(); //map.put("title", "Mybatis如此简单"); map.put("author", "张三"); List blogs = blogMapper.queryBlogIf(map); System.out.println(blogs); session.close(); } 测试结果:
可以看到确实将where给加上了,sql语句变为:
select * from blog where title = ?
而且结果也是正常的返回。
-
-
set标签
同样的,在select语句中会有where关键字,那如果是更新操作,含有的set关键字,就要使用
set
标签了。-
编写接口方法
int updateBlog(Map map);
-
编写sql配置文件
update blog title = #{title}, author = #{author} 注意:set是用逗号隔开,set元素会动态的在行首插入set关键字,并会删除额外的逗号
-
测试
@Test public void testUpdateBlog() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); HashMap
map = new HashMap (); map.put("id", "2156717bb4894bb5bfc5dc82f6030c70"); map.put("title", "动态SQL"); map.put("author", "李四"); mapper.updateBlog(map); session.close(); }
-
-
choose标签
有时候我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,使用choose标签可以解决此类问题,类似于java的switch语句
-
编写接口
List
queryBlogChoose(Map map); -
编写SQL配置文件
-
测试
@Test public void testQueryBlogChoose() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); HashMap
map = new HashMap (); map.put("title", "Java如此简单"); // map.put("author", "张三"); // map.put("views", 9999); List blogs = mapper.queryBlogChoose(map); System.out.println(blogs); session.close(); }
-
-
foreach标签
foreach元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量,它也允许指定开头和结尾的字符串以及集合项迭代之间的分隔符,这个元素也不会错误的添加多余的分隔符。
你可以将任何可迭代对象(如List,Set等),Map对象或者数组对象作为集合参数传递给foreach,当使用可迭代对象或者数组时,index是当前迭代的序号,item的值是本次迭代获取到的元素,当使用map对象时,index是键,item是值
示例:
先将数据库的前三个数据的id修改为固定的1,2,3
需求:我们需要查询blog表中id分别为1,2,3的博客信息
-
编写接口
List
queryBlogForeach(Map map); -
编写对应mapper文件
foreach一个常见使用场景是对集合进行遍历,尤其是在构建IN条件语句的时候
-
测试
@Test public void testQueryBlogForeach() { SqlSession session = MyBatisUtils.getSqlSession(); BlogMapper blogMapper = session.getMapper(BlogMapper.class); HashMap
map = new HashMap<>(); List ids = new ArrayList<>(); ids.add(1); ids.add(2); ids.add(3); map.put("ids", ids); List blogs = blogMapper.queryBlogForeach(map); System.out.println(blogs); session.close(); }
-
-
SQL片段
有时候可能某个sql语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。
抽取SQL片段:
title = #{title} and author = #{author} 引用SQL片段:
注意:
- 最好基于单表来定义sql片段,提高片段的可重用性
- 在sql片段中不要包括where
十四.缓存
Mybatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
Mybatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存
- 为了提高扩展性,Mybatis定义了缓存接口Cache,我们可以通过实现Cache接口来自定义二级缓存。
一级缓存
一级缓存也叫本地缓存:
- 与数据库同一次会话期间查询到的数据会放在本地缓存中
- 以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库
测试:(加入日志,方便查看结果)
-
编写接口方法
User findUserById(@Param("id")int id);
-
接口对应的mapper文件
-
测试
@Test public void test() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.findUserById(1); System.out.println(user); User user2 = userMapper.findUserById(1); System.out.println(user); System.out.println(user == user2); session.close(); }
结果分析
一级缓存失效的四种情况
一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它。
-
sqlSession不同
@Test public void test5() { SqlSession session = MyBatisUtils.getSqlSession(); SqlSession session1 = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); UserMapper userMapper1 = session1.getMapper(UserMapper.class); User user = userMapper.findUserById(1); System.out.println(user); User user1 = userMapper1.findUserById(1); System.out.println(user1); System.out.println(user == user1); session.close(); session1.close(); }
运行结果:发送了两条SQL语句
结论:每个sqlsession中的缓存相对独立
-
sqlSession相同,查询条件不同
@Test public void test5() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); // UserMapper userMapper1 = session.getMapper(UserMapper.class); User user = userMapper.findUserById(1); System.out.println(user); User user1 = userMapper.findUserById(2); System.out.println(user1); System.out.println(user == user1); session.close(); }
运行结果:发送了两条SQL语句
结论:当前缓存中,不存在这个数据,就没法用缓存数据直接读取
-
sqlSession相同,两次查询之间执行了增删改操作
增加接口方法:
int updateUser(Map map);
编写mapper文件:
update user set name = #{name} where id = #{id} 测试:
@Test public void test6() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.findUserById(1); System.out.println(user); HashMap
map = new HashMap<>(); map.put("id", 2); map.put("name", "哪吒"); userMapper.updateUser(map); User user1 = userMapper.findUserById(1); System.out.println(user1); System.out.println(user == user1); session.close(); } 运行结果:同一个查询在中间执行了增删改的操作后,重新执行了
结论:因为增删改操作可能会对当前数据产生影响,所以不会直接读取缓存的了
-
sqlSession相同,手动清除一级缓存
@Test public void test7() { SqlSession session = MyBatisUtils.getSqlSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.findUserById(1); System.out.println(user); // 手动清除缓存 session.clearCache(); // 再重新查询 User user1 = mapper.findUserById(1); System.out.println(user1); System.out.println(user == user1); session.close(); }
运行结果:
二级缓存
- 二级缓存也叫全局缓存,一级缓存的作用域太低了,所以诞生了二级缓存
- 基于namespace级别的缓存,一个名称空间对应一个二级缓存
- 工作机制
- 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
- 如果当前会话关闭了,这个会话对应的一级缓存就没了,但是我们想要的是,会话关闭了,一级缓存的数据被保存到二级缓存中;
- 新的会话查询信息,就可以从二级缓存中获取内容;
- 不同的mapper查出的数据会放在自己对应的缓存(map)中
使用步骤:
-
开启全局缓存(在核心配置文件中mybatis-config.xml)
-
在每个要使用二级缓存的mapper.xml中配置
也可以自定义参数(官方例子)
-
代码测试
注意:必须将所有实体类实现序列化接口,否则会报错
org.apache.ibatis.cache.CacheException:Error serializing object. Cause: java.io.NotSerializableException: com.example.zhang.pojo.User
User类加上序列化接口:
public class User implements Serializable
测试代码:(查询完关闭一级缓存,再进行一次查询)
@Test public void testCache() { SqlSession session = MyBatisUtils.getSqlSession(); SqlSession session2 = MyBatisUtils.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); UserMapper userMapper2 = session2.getMapper(UserMapper.class); User user = userMapper.findUserById(1); System.out.println(user); session.close(); User user2 = userMapper2.findUserById(1); System.out.println(user2); System.out.println(user == user2); session2.close(); }
测试结果:
-
总结
- 只要开启了二级缓存,我们在同一个Mapper中查询,可以在二级缓存中拿到数据
- 查出的数据都会被默认放在一级缓存中
- 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中
缓存原理
自定义缓存
可以使用第三方缓存实现--EhCache
Ehcache是一种广泛使用的java分布式缓存,用于通用缓存,以后缓存会用Redis。