Mybatis
课程大纲
1、 回顾JDBC
a) 编写JDBC代码
b) JDBC代码的存在的问题
2、 Mybatis的概述
3、 快速入门
4、 完整的CRUD
5、 动态代理实现DAO
6、 全局配置文件mybatis-config.xml详解
7、 Mapper.xml映射文件详解
8、 动态SQL
9、 缓存问题
10、 高级查询
教学笔记
SSH = Struts + spring + hibernate
SSM= springMVC + spring + mybatis
1、 回顾JDBC代码
1.1、 创建新数据库,导入测试数据
1.2、 创建新项目,并且添加父工程
添加父工工程,由父工程统一管理依赖及版本
父工程:
1.3、 引入数据库依赖和Junit
1.4、 代码回顾:
/*
* 回顾JDBC代码
*/
publicclass JdbcTest {
@Test
publicvoid testJDBC(){
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
// 准备连接参数
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/mybatis_test";
String user = "root";
// 加载驱动
Class.forName(driver);
// 建立连接
connection =DriverManager.getConnection(url, user, password);
// 编写SQL
String sql = "SELECT * FROM tb_user WHERE id =?";
// 编译SQL,形成statement
statement = connection.prepareStatement(sql);
// 设置SQL的参数,因为ID是Long类型数据,所以我们用setLong方法
// 通过参数的索引位置进行设置,索引位置是从1开始
statement.setLong(1,1L);
// 执行查询,获取结果集
resultSet = statement.executeQuery();
// 解析数据
while(resultSet.next()){
System.out.println("id:" + resultSet.getLong("id"));
System.out.println("username:" + resultSet.getString("user_name"));
System.out.println("password:" + resultSet.getString("password"));
System.out.println("age:" + resultSet.getInt("age"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {// 关闭资源
if( resultSet != null){
resultSet.close();
}
} catch(SQLException e) {
e.printStackTrace();
}
try {
if( statement != null){
statement.close();
}
} catch(SQLException e) {
e.printStackTrace();
}
try {
if( connection != null){
connection.close();
}
} catch(SQLException e) {
e.printStackTrace();
}
}
}
}
1.5、 问题分析
JDBC操作数据库的流程:
A:准备数据库连接参数
B:连接数据库
C:编译SQL
D:设置SQL参数
E:执行SQL获取结果
F:遍历结果集,封装数据到JavaBean
G:关闭资源
步骤中出现的问题:
1、准备连接参数
问题:将参数硬编码到了程序中,不方便维护和开发
解决:将参数写到外部配置文件,然后通过读取配置文件来获取连接参数
2、连接数据库
问题:频繁连接
解决:连接池
3、编译Sql语句获取statement
问题:Sql语句硬编码到了程序中,业务变更,Sql优化,都需要重新编译
解决:把Sql语句也放置到外部配置文件,程序加载配置文件,获取Sql
4、设置Sql参数
问题:以前手动写Sql,手动设置参数,现在动态获取Sql,必须动态设置参数
1)需要自动判断参数类型
2)需要自动判断参数的位置
解决:目前没有好的解决方案,待定。。
5、执行statement,获取结果集
6、解析结果集,封装数据
问题:遍历解析结果集非常麻烦
1)需要自己判断结果字段类型
2)需要自己明确结果的字段名称
3)需要自己把结果封装到JavaBean
4)如果有多行,需要逐行解析,封装为List
解决:
利用反射实现自动数据封装。但是代码复杂
7、释放资源
问题:频繁关闭连接
解决:连接池
上面的这些问题,我们自己实现起来比较繁琐,现有的持久层框架中,Hibernate也无法解决所有问题,而且Hibernate的学习成本比较高,在面对复杂查询时,查询效率略低。
我们需要新的技术来解决这些问题:Mybatis
2、 Mybatis概述
2.1、 简介:
2.2、 官网
官网:http://www.mybatis.org/mybatis-3/
中文官方文档:http://www.mybatis.org/mybatis-3/zh/index.html
2.3、 特点
Mybatis:
1) 支持自定义SQL、存储过程、及高级映射
2) 实现自动对SQL的参数设置
3) 实现自动对结果集进行解析和封装
4) 通过XML或者注解进行配置和映射
5) 实现Java对象与数据库表的映射转换
可以发现,MyBatis是对JDBC进行了简单的封装,帮助用户进行SQL参数的自动化映射,以及结果集与Java对象的映射。与Hibernate相比,更加配置简单、灵活、执行效率高。但是正因为此,所以没有实现完全自动化,需要手写SQL,这是优点也是缺点。
因此,对性能要求较高的电商类项目,一般会使用MyBatis,而对与业务逻辑复杂,对执行效率要求不高的传统行业,一般会使用Hibernate
2.4、 架构
MyBatis架构总结:
1、 MyBatis有两类配置文件:
a) mybatis-condig.xml,是MyBatis的全局配置文件,包含全局配置信息,如数据库连接参数、插件等。整个框架中只需要一个即可。
b) xxxMapper.xml,是映射文件,里面配置要执行的SQL语句,每个SQL对应一个Statement,可以有多个Mapper.xml文件
2、 首先会通过SqlSessionFactoryBuilder来加载配置文件,生成一个SqlSessionFactory
a) 会加载mybatis-config.xml和mapper.xml
b) 加载mapper.xml的时候,顺便会对Sql进行编译,形成statement
3、 通过SqlSessionFactory建立连接,获取SqlSession对象
4、 MyBatis获取要执行的statement,进行自动参数设置
5、 SqlSession底层会通过Executor(执行器)来执行编译好的Statement,获取结果
6、 SQL的输入参数类型:
a) POJO,普通Java对象
b) HashMap,其实是POJO的Map形式, 键值对就是对象字段名和值z``
c) 各种基本数据类型
7、 查询结果的输出形式
a) POJO,普通Java对象
b) HashMap,其实是POJO的Map形式, 键值对就是对象字段名和值
c) 各种基本数据类型
3、 快速入门
3.1、 导入依赖
添加Log4j的配置文件
3.2、 编写全局配置文件
JDBC配置
3.3、 编写Mapper.xml映射文件
3.4、 编写测试代码
publicstaticvoid main(String[] args) throws Exception {
// 指定全局配置文件的路径
String resource = "mybatis-config.xml";
// 获取输入流,关联全局配置文件
InputStream inputStream =Resources.getResourceAsStream(resource);
// 构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession
SqlSession session = sqlSessionFactory.openSession();
// 执行statement,要指定两个参数:
// 1、通过Mapper.xml中的 namespace + statement的ID来确定要执行哪个statement
// 2、SQL中需要接收的实际参数
User user = session.selectOne("userMapper.queryById", 1L);
System.out.println(user);
}
3.5、 测试结果
我们发现我们写的SQL语句中,#{}部分被替换为了占位符:?
2017-03-1516:10:58,703 [main] [org.apache.ibatis.logging.LogFactory]-[DEBUG] Logginginitialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2017-03-1516:10:58,716 [main][org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG] PooledDataSourceforcefully closed/removed all connections.
2017-03-1516:10:58,716 [main][org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG] PooledDataSourceforcefully closed/removed all connections.
2017-03-1516:10:58,716 [main] [org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG]PooledDataSource forcefully closed/removed all connections.
2017-03-1516:10:58,716 [main][org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG] PooledDataSourceforcefully closed/removed all connections.
2017-03-1516:10:58,781 [main][org.apache.ibatis.transaction.jdbc.JdbcTransaction]-[DEBUG] Opening JDBCConnection
2017-03-1516:10:59,001 [main][org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG] Createdconnection 312072263.
2017-03-1516:10:59,002 [main] [org.apache.ibatis.transaction.jdbc.JdbcTransaction]-[DEBUG]Setting autocommit to false on JDBC Connection[com.mysql.jdbc.JDBC4Connection@1299d847]
2017-03-1516:10:59,004 [main] [userMapper.queryUserById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
2017-03-1516:10:59,047 [main] [userMapper.queryUserById]-[DEBUG] ==> Parameters: 1(Long)
2017-03-1516:10:59,079 [main] [userMapper.queryUserById]-[DEBUG] <== Total: 1
User [id=1,userName=null, password=123456, name=张三, age=30, sex=1, birthday=Wed Aug 08 00:00:00CST 1984, created=Fri Sep 19 16:56:04 CST 2014, updated=Sun Sep 21 11:24:59 CST2014]
3.6、 入门程序执行流程图
流程总结:
1、 编写配置文件(全局配置文件mybatis-config.xml和所有的mapper.xml映射文件)
a) 简单来说:就是准备JDBC连接参数以及要用到的Sql语句
2、 加载配置,创建SqlSessionFactory
a) 这里获取连接参数,获取Sql,对Sql进行预编译,形成statement
3、 通过SqlSessionFactory创建SqlSession
a) 这里就是调用了连接参数,连接数据库,形成会话
4、 通过SqlSession执行statement,实现CRUD
a) 给前面编译好的statement设置Sql参数,然后执行
5、 通过SqlSession提交事务
6、 通过SqlSession关闭会话
3.7、 userName为Null问题
mybatis把查询的结果集封装为Java对象时,默认情况下,是把数据库的列名作为Java对象的属性名,因此如果这两者不一致,就会导致对象属性无法填入数据!
解决方案:
方案1:对查询的结果列使用别名
结果:
思考:这种方式虽然可以,但是很麻烦,不方便后期的开发维护!
更好的解决方案后面会讲。
4、 完整的CRUD
4.1、 定义UserDAO接口
/**
* 用户增删改查接口
* @author虎哥
*/
publicinterface UserDAO {
/**
* 根据ID查询用户
* @param id
* @return
*/
User queryUserById(Long id);
/**
* 查询全部用户
* @return
*/
List<User> queryAll();
/**
* 添加用户
* @param user
*/
void insertUser(User user);
/**
* 修改用户
* @param user
*/
void updateUser(User user);
/**
* 根据ID删除用户
* @param id
*/
void deleteUserById(Long id);
}
4.2、 编写SQL到mapper.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">
<!-- 这个文件,用来定义要用到的SQL信息
namespace:名称空间,是每个Mapper.xml文件的唯一标示,这个可以自定义,但是不能重复 -->
<mapper namespace="userMapper">
<!-- 这里定义一条SQL语句,其实就是一个statement
select:代表是一条查询的SQL,我们还可以定义:update\insert\delete
id: 这个statement的唯一标示,自定义,但是不能重复
parameterType:SQL参数类型,这里要写类的全名
resultType:返回结果类型,这里写类的全名
-->
<select id="queryUserById" parameterType="java.lang.Long" resultType="cn.itcast.mybatis.pojo.User">
<!-- 这里定义真正的SQL语句,#{} 代表占位符,当SQL被编译时,会变成?,然后接收真正的参数 -->
SELECT *,user_name AS userName FROMtb_user WHERE id = #{id}
</select>
<!-- 查询全部用户:
resultType:返回的结果虽然是一个List,这里依然写List中的元素类型,Mybatis会自动判断返回值个数并且做封装
-->
<select id="queryAll" resultType="cn.itcast.mybatis.pojo.User">
SELECT *,user_name AS userName FROMtb_user
</select>
<!-- 添加用户信息: -->
<insert id="insertUser" parameterType="cn.itcast.mybatis.pojo.User">
<!-- 注意,这里用#{字段名},mybatis会自动根据字段名,去User参数中找字段值,设置到Sql中 -->
INSERT INTO tb_user (
id,
user_name,
password,
name,
age,
sex,
birthday,
created,
updated
)
VALUES
(
NULL,
#{userName},
#{password},
#{name},
#{age},
#{sex},
#{birthday},
NOW(),
NOW()
);
</insert>
<!-- 修改用户 -->
<update id="updateUser" parameterType="cn.itcast.mybatis.pojo.User">
UPDATE tb_user
SET
user_name = #{userName},
password = #{password},
name = #{name},
age = #{age},
sex = #{sex},
birthday = #{birthday},
updated = NOW()
WHERE
id = #{id};
</update>
<!-- 删除用户 -->
<delete id="deleteUserById" parameterType="java.lang.Long">
DELETE FROM tb_user WHERE id = #{id}
</delete>
</mapper>
4.3、 编写DAO实现类
/**
* UserDAO的实现类
*
* @author虎哥
*/
publicclass UserDAOImpl implements UserDAO {
private SqlSession sqlSession;
// 通过构造函数接收SqlSession
public UserDAOImpl(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public User queryUserById(Long id) {
returnsqlSession.selectOne("userMapper.queryUserById", id);
}
@Override
public List<User> queryAll() {
returnsqlSession.selectList("userMapper.queryAll");
}
@Override
publicvoid insertUser(User user) {
// 插入数据
sqlSession.insert("userMapper.insertUser", user);
}
@Override
publicvoid updateUser(User user) {
sqlSession.update("userMapper.updateUser", user);
}
@Override
publicvoid deleteUserById(Long id) {
sqlSession.delete("userMapper.deleteUserById", id);
}
}
4.4、 使用工具生成测试类
使用工具生成测试类:
4.5、 编写测试代码
publicclass UserDAOTest {
private UserDAO userDAO;
@Before
publicvoid setUp() throws Exception {
// 设置资源路径
String resource = "mybatis-config.xml";
// 获取输入流,关联配置文件
InputStream inputStream =Resources.getResourceAsStream(resource);
// 读取配置,构建session工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取session,同时打开事务的自动提交,每一次操作都是个独立事务
SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 传入SqlSession对象,构建DAO对象
this.userDAO = new UserDAOImpl(sqlSession);
}
@Test
publicvoid testQueryUserById() {
// 测试根据id查询
User user = userDAO.queryUserById(1L);
System.out.println(user);
}
@Test
publicvoid testQueryAll() {
// 测试查询全部
List<User> list = userDAO.queryAll();
for (User user : list) {
System.out.println(user);
}
}
@Test
publicvoid testInsertUser() {
// 测试添加用户
User user = new User();
user.setAge(16);
user.setBirthday(new Date());
user.setName("杨颖");
user.setPassword("123456");
user.setSex(2);// 2代表女性
user.setUserName("xiaoyingying");
// 插入用户
userDAO.insertUser(user);
}
@Test
publicvoid testUpdateUser() {
// 测试修改用户
// 先查询一个用户
User user = userDAO.queryUserById(1L);
user.setAge(100);
// 修改用户
userDAO.updateUser(user);
}
@Test
publicvoid testDeleteUserById() {
// 测试删除
userDAO.deleteUserById(13L);
}
}
5、 动态代理实现DAO
5.1、 CRUD代码分析
我们上面的 DAO实现类中,增删改查的代码比较简单,而且代码的结构类似,有很多的共同点。
5.2、 思路分析
思考一下:
1)是否可以通过动态代理,代理UserDAO接口,然后动态生成内部的增删改查代码呢?
应该是可以的,因为代码有很多相似之处,都是调用SqlSession的增删改查方法,然后调用 对应的 statement,传递对应的Sql参数。
模式虽然类似,但是我们有新的问题:
2)想要让Mybatis帮我们动态生成DAO代码,有以下问题需要解决?
A:如何确定要调用SqlSession的哪个方法?
每一个DAO中的方法,最终一定对应Mapper.xml中的一个statement,而每个statement的标签名,就代表了要进行的操作:select\insert\update\delete等。
所以要确定应该调用SqlSession的哪个方法,就要先确定DAO的方法对应mapper.xml中的哪个statement
B:如何确定要调用哪个statement?
要确定statement,就必须通过两个参数:mapper.xml文件的namespace+ 这个statement的ID
而我们知道,这两个值都是用户自定义的,可以是任意值。如果是这样,我们是无法确定的!
但是,注意观察的同学可能发现了,我们刚才定义namespace和ID并不是任意的:
namespace恰好就叫UserMapper,与UserDAO接口相关
statement的id恰好与每一个方法的名称一致。
其实呢,在Mybatis中,也是采用类似的约定来做的,mybatis对于Mapper.xml文件的定义有以下约定:
l 约定namespace必须与DAO接口的全名称一致
l 约定statement的ID必须和接口中的方法名称一致
这样以来,Mybatis的底层,只要拿到我们定义的接口,以及接口的方法名称,必然能确定到对应的statement,从而知道我们应该调用哪个SqlSession方法,知道返回值类型是什么,知道参数类型是什么,从而帮我们动态生成DAO的实现代码!
5.3、 mybatis动态代理实现DAO
5.3.1、 规则
经过上面的分析,我们知道,要想动态代理DAO接口,关键点在于确定DAO每个方法对应的statement,而要想自动确定statement,必须遵守以下约定:
1) 每个DAO接口对应一个Mapper.xml文件
2) mapper.xml的namespace必须是接口的全名称
3) mapper.xml中的每个statement的id必须是接口中对应的方法名
4) statement中定义的resultType必须和接口中对应方法的返回值一致(这一点即便不是动态代理也得遵守)
5) 在mybatis中,一般接口命名规则不是XxxDAO,而是XxxMapper,这一点不是必须的,但是在后面有类似约定会用到,所以我们也规范起来!
如果遵循以上约定进行配置,动态代理就可以实现!
5.3.2、 新建Mapper接口
代码其实月UserDAO一样
/**
* 用户增删改查接口
* @author虎哥
*/
publicinterface UserMapper {
/**
* 根据ID查询用户
* @param id
* @return
*/
User queryUserById(Long id);
/**
* 查询全部用户
* @return
*/
List<User> queryAll();
/**
* 添加用户
* @param user
*/
void insertUser(User user);
/**
* 修改用户
* @param user
*/
void updateUser(User user);
/**
* 根据ID删除用户
* @param id
*/
void deleteUserById(Long id);
}
5.3.3、 修改UserMapper.xml文件
注意两点:
1) namespace改成接口全名称
2) statement的id必须与方法名一致
5.3.4、 编写测试类
其实就是复制UserDAO测试类,改一下名字
5.3.5、 总结:
MyBatis动态代理生成DAO的步骤:
1) 编写数据管理的接口XxxMapper
2) 编写该接口对应的Mapper.xml
a) namespace必须与Mapper接口全名一致
b) statement的id必须和Mapper接口中的对应方法名一致
c) statement的resultType必须和Mapper接口中对应方法返回值一致
3) 通过SqlSession的getMapper(XxxMapper.class)方法来获取动态代理的Mapper实现类对象
6、 mybatis-config.xml全局配置文件详解
注意:这些配置在文件中的顺序非常重要!必须严格按照上图中出现的顺序定义
6.1、 Properties属性
这个属性的作用是引用外部的properties文件:
6.2、 settings设置
6.2.1、 全部设置描述
settings 中的设置是非常关键的,它们会改变MyBatis 的运行时行为。下表描述了设置中各项的意图、默认值等。
设置参数 | 描述 | 有效值 | 默认值 |
cacheEnabled | 该配置影响的所有映射器中配置的缓存的全局开关。 | true | false | true |
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 | true | false | false |
aggressiveLazyLoading | 当启用时,带有延迟加载属性的对象的加载与否完全取决于对任意延迟属性的调用;反之,每种属性将会按需加载。 | true | false | true |
multipleResultSetsEnabled | 是否允许单一语句返回多结果集(需要兼容驱动)。 | true | false | true |
useColumnLabel | 使用列标签代替列名。不同的驱动在这方面会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 | true | false | true |
useGeneratedKeys | 允许 JDBC 支持自动生成主键,需要驱动兼容。如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 | true | false | False |
autoMappingBehavior | 指定 MyBatis 是否以及如何自动映射指定的列到字段或属性。NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。FULL 会自动映射任意复杂的结果集(包括嵌套和其他情况)。 | NONE, PARTIAL, FULL | PARTIAL |
defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
defaultStatementTimeout | 设置超时时间,它决定驱动等待数据库响应的秒数。 | Any positive integer | Not Set (null) |
safeRowBoundsEnabled | 允许在嵌套语句中使用行分界(RowBounds)。 | true | false | False |
mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 | true | false | False |
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 | SESSION | STATEMENT | SESSION |
jdbcTypeForNull | 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER |
lazyLoadTriggerMethods | 指定哪个对象的方法触发一次延迟加载。 | A method name list separated by commas | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定动态 SQL 生成的默认语言。 | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver |
callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意原始类型(int、boolean等)是不能设置成 null 的。 | true | false | false |
logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | Any String | Not set |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | Not set |
proxyFactory | 为 Mybatis 用来创建具有延迟加载能力的对象设置代理工具。 |
前面3个标红的是跟缓存相关的内容,我们留到明天讲解。
6.2.2、 解决数据库列名与Java属性名不一致
mapUnderscoreToCamelCase:驼峰匹配
如果设置为true,就会开启驼峰自动匹配。把数据库中的下划线命名 自动映射到Java的驼峰命名方式。
下划线命名:每个单词用下划线间隔
驼峰命名:单词与单词之间没有间隔,但是首字母要求大写
因此:数据库中的:user_name 对应的驼峰方式是: userName
有了这个,就可以解决我们前面的userName为null的问题,不用再手动写别名了!
去除查询结果的别名
测试:
6.2.3、 问题
到这里为止,解决数据库列名与JavaBean属性名不一致,有两种方案,但是有各自的问题:
1、 查询结果使用别名。比较麻烦,不方便开发和维护
2、 开启自动驼峰匹配。方便,但是如果不符合驼峰规则,则无法匹配!
第三种解决方案,后面讲解。
6.3、 typeAliases别名
typeAliases类型别名是为 Java 类型命名的一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余
原来的方式:
所有的类型,都必须写全名称,非常的麻烦!
我们可以通过typeAliases来给这些类起别名(简称),这样就可以简化配置文件的书写:
6.3.1、 方式1:单独定义
6.3.2、 方式2:扫描包
6.3.3、 MyBatis中已经定义好的别名
因此,我们使用这些类型,也可以直接写别名
注意:Mybatis中的别名是不区分大小写的。但是一般建议大家严格按照类的简称来写。
6.4、 typeHandler类型处理器(了解)
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。而MyBatis中已经定义了很多的类型处理器,来解决Java类型与数据库类型的转换问题,所以,一般我们是不需要自定义的。
一些默认的类型处理器:
6.5、 plugins插件(了解)
MyBatis中的插件,其实类似于拦截器的效果,可以实现在MyBatis的整个运行流程中的某些指定位置进行拦截:
Executor:对执行器进行拦截,上图括号内是可以拦截的方法
ParameterHandler:参数处理时进行拦截
ResultSetHandler:处理结果集,封装Java对象时进行拦截
StatementHandler:编译statement时进行拦截
6.6、 Environment环境(了解)
注意:实际场景中,我们经常会把数据环境交给Spring去管理,因此这个配置我们了解即可!
6.7、 Mappers映射器
通过这个设置,来把Mapper.xml文件引入到 MyBatis运行环境中!
6.7.1、 方式1:使用项目资源路径
问题:如果有多个Mapper,需要一个个加载!
6.7.2、 方式2:使用URL地址(几乎不用)
问题1:如果有多个Mapper,需要一个个加载!
问题2:URL地址很麻烦,并且环境改变地址也得变
6.7.3、 方式3:使用接口的全路径名称
运行后报错:
这种方式要求:Mapper文件必须放在接口所在包下!并且文件名与接口名保持一致!
复制后重新测试:
问题1:如果有多个Mapper,需要一个个加载!
问题2:Mapper.xml文件与Java文件在一起,不好。
6.7.4、 方式4:配置扫描包
要求:Mapper文件必须和接口Java文件在一起,并且名称必须与接口名一致!
优点:不用一个一个配置
问题:Mapper.xml文件与Java文件在一起,不好。
7、 Mapper.xml映射文件详解
7.1、 概述
MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。因为这个配置文件,几乎省略掉了90%的JDBC代码。这是我们学习的重点!
我们重点学习以下部分:
CRUD操作:insert\update\delete\select
sql片段:sql
结果集映射:resultMap
cache和cache-ref缓存部分,留在后面统一讲解
7.2、 CRUD操作
7.2.1、 select
进行查询的statement
常用的几个属性:
select元素:代表查询,类似的还有update、insert、delete
id:这个statement的唯一标示
parameterType:输入参数类型,可选参数,mybatis可以自动推断数据类型
resultType:查询结果类型,如果结果是集合,请写集合内元素类型!
resultMap:结果集映射,这个和resultType 只能存在1个,应对复杂的结果集。后面详细讲!
7.2.2、 insert、update和delete
insert、update和delete的语法基本类似:
比较常用的几个属性:
id:这个statement的唯一标示
parameterType:输入参数类型,可选参数,mybatis可以自动推断数据类型
7.2.3、 insert语句实现ID回填
insert语句的特殊参数:
useGenerateKeys:自增主键的回显功能,默认是false
默认没有配置主键回显功能的时候:
插入数据时,没有填写ID,然后插入结束查看ID属性
结果:
开启主键回填功能:
再次运行结果:
7.2.4、 #{}的用法
我们发现,在Mapper.xml映射文件中,经常使用#{属性名} 来作为SQL语句的占位符,来映射Sql需要的实际参数
也就是说:#{}就是一个预编译的占位符作用
7.2.4.1、 如果接口中方法只有一个参数
只有一个参数时,入参可以是以下情况:
1)Java的基本类型、基本类型包装类、String类型等
这种情况下,#{}中写什么都无所谓,因为MyBatis会把实际参数直接传入这个位置,不管参数名称和类型
2)Java引用类型,类似User这样的对象
这种情况下,MyBatis会把User对象中的属性名和属性值以键值对形式保存到一个类似Map的结构中。
因此,当我们用#{属性名} 来取值时,其实就相当于根据键查找值。可以直接获取到User的属性值!
3)HashMap类型(很少用)
如果参数是一个HashMap,这样就非常方便了,我们可以直接用 #{键的名称} 来获取到对应的值。这种方式与上面的方式2类似,因此很少使用
结论:如果是一个参数,
简单数据类型:#{}不管写什么都可以获取值
对象类型:#{}可以根据参数对象中的属性名获取属性值
Map类型:#{}可以根据Map的键来取到对应的值
7.2.4.2、 如果接口中方法有多个参数
尝试运行:
原因:
当有多个参数的情况下,MyBatis就会尝试把这多个参数放入一个Map中,这样才能方便我们通过#{}来取出这些值。
那么问题来了:MyBatis封装这些参数为Map时,键应该是什么??
有同学说:为什么直接用参数的名称做键呢?
大家一起思考一下,MyBatis底层肯定是用反射来进行的这些操作,那么反射可以获取到方法,但是是否能获取的方法的参数名称呢?
答案是不行,方法的参数名称是形参,是可变的。反射类Method中,并没有提供获取方法名称的功能。所以MyBatis并不知道我们传递的参数名称,只知道这些参数的值!
再次回到上面的问题,只有值,那么MyBatis封装Map的时候,应该以什么做键呢?
答案是这样的:默认情况下MyBatis会设置两种键:
A:以从0开始的递增数字作为键,第一个参数是0,第2个参数就是1,以此类推
B:以”param” + i作为键,i是从1递增的数字,第一个参数键就是param1,第2个就是param2,以此类推
所以我们可以通过#{0} 或 #{param1} 来取到第一个参数,以此类推
但是,有同学会觉得这样很不方便。不如直接用参数名直观,所以MyBatis还支持另一种方式:就是Param注解
我们可以在定义接口中方法的时候,直接使用@Param(“参数名”)的方式指定该参数的名称
MyBatis底层可以通过反射获取到方法参数的注解,从而得知你设定的名称。然后在封装参数Map的时候,以这个名字为键,我们就可以用这个名字来取值了!
注意:此时就不能通过#{0} 这样的方式取到参数值了,Param注解会替代参数索引方式!
结论:如果有多个参数,请使用@Param注解来指定参数名称,代码可读性好!
7.2.5、 ${}的用法
7.2.5.1、 ${}获取参数的方式
${}底层其实使用的是OGNL表达式。在处理参数时,与#{}有很大差别。
1)当只有一个参数时:
MyBatis底层会把这个参数 以键为’value’的形式存入Map中,因此取值的时候,必须${value},或者通过@Param注解
2)当有多个参数时:
此时必须以${param1}、${param2}方式或者@Param注解方式来获取参数
最后的结论:无论是1个参数,还是多个参数,都使用@Param注解,然后通过注解的中指定的名称取值
7.2.5.2、 ${}获取参数的问题
注意事项:
1、${}在取值时,并不会进行预编译,而是直接拼接SQL语句:
这样无法防止SQL注入问题
2、${}取值的时候,需要自己判断参数数据类型,如果是字符串,还得自己加引号:”${}”
因此,一般在SQL语句中,如果要获取参数,一般我们都会使用#{},而不是${}
7.2.6、 面试题:#和$的区别与联系
区别:
1) #是占位符,会对SQL进行预编译,相当于?; $是做SQL拼接,有SQL注入的隐患
2) #不需要关注数据类型,Mybatis自动实现类型转换。 $必须自己判断数据类型
联系:
两者都支持通过@Param注解指定参数名称,来获取参数值。推荐这种方式!
一般,如果是做参数传递,都会使用#{}
如果不是做预编译,而是必须拼接SQL,会使用${}
7.3、 ResultMap结果集映射
7.3.1、 概述
ResultMap用来定义SQL查询的结果与Java对象的映射关系。非常重要!
7.3.2、 解决字段名与列名不一致问题方案3
示例:
我们关闭驼峰映射:
再次查询:
依然OK
这就是解决列名与字段名不一致问题的解决方案3,如果名称不符合驼峰规则,就必须使用这种方案。
7.3.3、 autoMapping自动映射
细心的同学可能发现,我们刚才的配置中,只配置了id和userName字段,其它字段没有配置,映射也没有问题,为什么呢?
这是因为,在resultMap中,有一个属性叫做:autoMapping,如果值为true,并且列名称和字段名一致,是可以完成自动映射的。
默认情况下,这个autoMapping的值就是为true的。
那么,如果名称不一致,但是符合驼峰规则,能否自动映射呢?
也是可以的,但是要开启驼峰匹配:
因此,一般我们配置resultMap,只需要把ID配置出来就OK了。其它字段可以自动映射!
7.4、 SQL片段
我们经常会把SQL中比较通用的部分,提前出来,变成一个SQL片段,然后在各个SQL中都可以调用,简化书写:
例如,查询语句中,一般不会使用:Select * ,而是把列名一一列出,但是表的列名往往比较多,这时就可以提取出来:
在其他SQL中使用这个片段:
8、 动态SQL
我们接下来使用动态SQL实现以下功能:
需求1:查询所有男性用户,如果输入了姓名,则按照姓名模糊查找;如果没有输入则不管姓名
需求2:查询所有用户,传递参数orderType,如果值为0,按照年龄升序排序,如果为1则按照年龄降序排序,否则按照ID排序
需求3:查询所有用户,如果有姓名不为空,则按照姓名模糊查找;如果年龄也不为空,则还要满足年龄条件。
需求4:修改用户信息,如果某字段为null,则不修改这个字段
需求5:根据多个ID查询用户
8.1、 if
需求1:查询所有男性用户,如果输入了姓名,则按照姓名模糊查找;如果没有输入则不管姓名
l 添加接口
l 添加statement
注意:
在写SQL时,我们使用了”%${name}%”来拼接SQL,这样在传参数时,可以只写姓名
这里也可以写#{name}来进行预编译,那么传参数时,就必须在参数中写上”%李%”了
l 编写测试
如果name不为null:
结果:
如果name为null:
结果,查找了全部男性:
8.2、 choose,when,otherwise
动态标签中有if,但是没有else,如果我们有多条件,就需要用choose标签
choose中可以定义多个when和1个otherwise,所有状态中只能有一个成立:
多个when类似与if 和 else if
otherwise类似于最后的else
需求2:查询所有用户,传递参数orderType,如果值为0,按照年龄升序排序,如果为1则按照年龄降序排序,否则按照ID排序
l 定义接口
l 编写statement
l 编写测试类:
这里传1,会按照年龄降序:
8.3、 where
需求3:查询所有用户,如果有姓名不为空,则按照姓名模糊查找;如果年龄也不为空,则还要满足年龄小于指定年龄。
l 编写接口
l 编写statement
l 编写测试
没有出现语法错误,多余的AND被去除了!
8.4、 set
需求4:修改用户信息,如果某字段为null,则不修改这个字段
在修改用户时,有一些不需要修改的字段我们可能不愿意填写,这时,修改时的非空判断就非常有必要
而且在多个不确定有哪些字段或者有没有字段需要修改时,我们就需要set标签
l 接口:
l 编写Statement:
l 编写测试
8.5、 foreach
需求5:根据多个ID查询用户
l 编写接口
l 编写statement
l 编写测试
生成的SQL:
9、 缓存
为数据库的查询进行缓存,是减少数据库压力的主要途径,Mybatis与hibernate类似,也支持缓存。并且也分为一级缓存和二级缓存
一级缓存:session级别缓存,作用于当前会话。
二级缓存:SessionFactory级别缓存,作用于整个SessionFactory,多个会话之间可以共享缓存
9.1、 一级缓存
l 特点:
n mybatis的一级缓存默认就是开启的,并且无法关闭。
n mybatis的一级缓存作用域是当前session,一次openSession()后,如果相同的statement和相同参数,则不进行查询而是从缓存命中并且返回,如果没有命中则查询数据库。
n 任何的增删改操作都会导致缓存被清空
n 缓存会使用 Least RecentlyUsed(LRU,最近最少使用的)算法来收回
l 测试一级缓存:
结果:
两次查询结果,但是只有一条SQL
l 测试增删改缓存清空
结果:两次查询进行了两次SQL
l 测试手动清空缓存
结果:
9.2、 二级缓存
9.2.1、 基本使用
l 特点:
u 二级缓存需要手动开启,开启的方式是在Mapper.xml中添加标签:<cache/>
u 二级缓存的作用域是整个SessionFactory,如果namespace、statement和SQL参数一致,则缓存命中
l 开启二级缓存:
l 测试:
报错:
原因:开启二级缓存后,查询的对象需要实现序列化接口!
重新测试,结果:两次查询,但是只有一条SQL
9.2.2、 二级缓存全局开关
我们昨天讲解的mybatis-config.xml全局配置文件中,有一个配置,是二级缓存的全局开关,如果关闭,所有Mapper的二级缓存都会失效,默认是打开的。
9.2.3、 二级缓存的其它配置
10、 高级查询(表关联查询)
10.1、 案例说明:
有以下表:
用户表、订单表、商品表、订单详情表
分别对应4个实体类:
l 用户类:User
l 订单:Order
l 订单详情:OrderDetail
l 商品:Item
关系图:
MySQL作为一个ORM框架,也支持各种的高级查询,在本例中,我们来演示以下几个需求:
l 一对一查询:一个订单只属于一个用户
需求1:查询订单的同时,查询出订单所属用户
l 一对多查询:一个订单可以有多个订单详情
需求2:查询订单,并且查询出所有订单详情及所属用户
l 多对多查询:订单中可以有多个商品,商品也可以属于多个订单
需求3:查询订单,查询出所属用户,并且查询出订单的详情,及订单详情中的对应的商品信息
10.2、 一对一查询
需求1:根据订单编号查询:查询订单的同时,查询出订单所属用户
分析:这里需要用户表和订单表进行关联查询,查询结果中,除了订单的信息,还有用户的信息,用Order类无法封装所有信息!
思路1:扩展Order类,把用户字段也添加进去。
思路2:在Order类中,定义一个User类型的成员。
思路2更符合面向对象的思想,因此这里我们采用思路2:
l 订单类
l 接口定义:
问题:这里Order类的字段不是基本类型,Mybatis无法完成字段映射!这时,我们就需要使用resultMap结果集,来完成复杂映射:
l Mapper.xml
l 测试类:
l 结果
10.3、 一对多查询
需求2:查询订单,并且查询出所有订单详情及所属用户
l 修改Order类,增加订单详情属性:
l 编写接口
l 编写Mapper.xml
l 编写测试用例
10.4、 多对多查询
需求3:查询订单,查询出所属用户,并且查询出订单的详情,及订单详情中的对应的商品信息
l 编写实体类OrderDetail,添加一个商品的属性
l 编写接口方法
l 编写Mapper.xml
l 编写测试类
其实多对多没有什么新东西,就是集合属性与对象属性的嵌套,也就是collection和association的嵌套使用。
10.5、 延迟加载
l 打开延迟加载开关
l 定义接口方法
l 编写Mapper.xml,定义查询和结果集
l 编写测试类
l 测试结果
运行报错:
原因:
延迟加载,是在运行的时候,动态的进行查询,并且给对象添加属性,底层需要依赖Cglib依赖
添加cglib依赖:
<!-- 添加cglib支持 -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
</dependency>
再次运行:
11、 有关xml中的特殊字符:
查询w3cschool的XML文档,发现有两种解决方案:
1)使用特殊符合:
2)使用CDATA