Mybatis
概述
- Mybatis内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动,创建连接,创建Statement等繁琐的过程
- Mybatis通过XML或注解的方式将要执行各种statement配置起来,并通过java对象和statement中的sql的动态参数进行映射,生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回
- 采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc进行了封装,屏蔽了 jdbc api 底层访问细节,使我 们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作
- 为了我们能够更好掌握框架运行的内部过程,并且有更好的体验,下面我们将从自定义 Mybatis 框架开始来 学习框架。此时我们将会体验框架从无到有的过程体验,也能够很好的综合前面阶段所学的基础
ORM概念
- Object Relational Mapping 对象映射关系
- 就是把数据库表和实体类及实体类的属性对应起来
- 让我们可以操作实体类就实现操作数据库表
小结
- mybatis是一个持久层框架,用java编写
- 它封装了jdbc操作的很多细节,使开发者只需要关注sql本身,无需关注注册驱动,创建连接等繁琐细节
- 她使用了ORM思想实现了结果集的封装
- 用的是dom4j解析xml技术
三层架构
表现层
- 是用于展示数据的
业务层
- 处理业务需求
持久层
- 是和数据库交互的
持久层技术解决方案
JDBC技术
- Connection
- PreparedSatement
- ResultSet
Spring中的JdbcTemplate
- spring对jdbc的简单封装
Apache的DBUtils
- 也是对jdbc的简单封装
小结
- 以上这些都不是框架
- 仅仅是jdbc的规范,都只是工具类
Mybatis入门
主配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
环境
- 连接数据库的信息,有了他们就能创建Connection
<!--mybatis的主配置文件-->
<configuration>
<!--配置环境-->
<environments default="mysql">
<!--配置mysql-->
<environment id="mysql">
<!--配置事务类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源(连接池)-->
<dataSource type="POOLED">
<!--配置连接数据库的四个信息-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis"/>
<property name="username" value="root"/>
<property name="password" value="19970930"/>
</dataSource>
</environment>
</environments>
</configuration>
绑定映射文件位置
- 有了它就有了映射配置信息
<!--指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件-->
<mappers>
<mapper resource="com/okt/dao/UserDao.xml"></mapper>
</mappers>
映射配置文件
<?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语句,就可以获取PrepareStatment
- 配置中的返回类型为封装的实体类全体限定类名
<mapper namespace="com.okt.dao.UserDao">
<!--配置查询所有,id是唯一的,但在不同的配置文件中可以用相同id,因为他们都有唯一的namespace-->
<select id="findAll" resultType=“com.okt.domain.User”>
select * from user
</select>
</mapper>
环境搭建的注意事项
- 在创建映射文件时命名有两种方式,一种为UserDao,一种为UserMapper
- 在Idea中创建目录要一级一级创建,不能像包一样创建
- mybatis的映射配置文件必须和dao接口包结构一致
- 映射配置文件的mapper标签的namespace属性的取值必须是dao接口的全限定类名,包+类
- 映射配置文件的操作配置,id属性的取值必须是dao接口的方法名
- 总结:当我们遵从了第三,四,五点之后,我们在开发中就无须再写dao的实现类。
快速入门
public static void main(String[] args) throws IOException {
//1.读取配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();
SqlSessionFactory factory=builder.build(inputStream);
//3.使用工厂模式生产SqlSession对象
SqlSession sqlSession = factory.openSession();
//4.使用SqlSession创建Dao的代理对象
UserDao userDao = sqlSession.getMapper(UserDao.class);
//5.使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
//6.释放资源
inputStream.close();
}
- 使用类加载器或者ServletContext对象获取真实路径
- Mybatis创建工厂时使用了构造者模式SqlSessionFactoryBuilder为构造者,把对象的创建细节隐藏,使使用者直接调用方法即可拿到对象。
- 生产SqlSession使用了工厂模式,其优势为解耦,降低了类之间的依赖关系
- 创建Dao接口实现类使用了代理模式,不修改源码的基础上对已有方法增强
注意事项
- 不要忘记在映射配置中告知mybatis要封装到哪个实体类
- 配置方式:指定实体类的全限定类名
mybatis基于注解入门案例
- 把映射文件删除,在接口方法上使用对应操作的注解,并且指定sql语句
- 同时在主配置文件中的mapper标签使用class属性指定接口的全限定类名
- 我们在实际开发中,越简便越好,所以都是采用不写实现类的方式。不管是使用XML还是注解配置,mybatis支持写实现类的方式
自定义Mybatis
执行查询分析
-
根据配置文件的信息创建Connection对象
- 注册驱动,获取连接
-
获取预处理对象PrepareddStatement
- 此时需要SQL语句
- conn.prepareStatement(sql),语句从XML中解析出来
-
执行查询
- ResultSet resulSet=prepareStatement.executeQuery()
-
遍历结果用于封装
-
List<E> list =new ArrayList(); while(resultSet.next()){ E element=(E)Class.forName("配置的全限定类名").newInstance; element.setXXX(rs.getXXX); list.add(element) }
-
实体类属性和表中列名是一致的,于是我们就可以把表的列名看成实体类的属性名称。利用反射的方式来根据名称获取每个属性,并把值赋进去
-
-
返回list
- return list
执行原理
- 第一个连接信息
- 第二个映射信息
- 第一:执行的SQL语句
- 第二:封装结果的实体类全限定名
- 把这两个信息组合起来定义成一个对象,Mapper,以map形式存储,key—value
- key:Dao接口和执行id
- value是Mapper对象中。
创建代理对象分析
Prox.newProxyInstance(类加载器
,代理对象要实现的接口字节码数组
,如何代理
);
- 类加载器:它使用和被代理对象是相同的加载器
- 代理对象要实现的接口:和被代理对象实现相同的接口一样,即实现传入过来的Dao接口,要实现它。
- 如何代理:增强代理,我们需要自己来提供。
- 此处是一个InvocationHandler的接口,我们需要写一个该接口的实现类,在实现类中调用selectList方法
Mybatis的CRUD操作
插入操作
- Dao接口
/***
* 保存用户
*/
void saveUser(User user);
- 映射配置文件
<!--保存用户-->
<insert id="saveUser" parameterType="com.okt.domain.User">
insert into user(username,sex,birthday,address) values (#{username},#{sex},#{birthday},#{address})
</insert>
- 要点
- parameterType="com.okt.domain.User"为参数类型,类型除了java自带的,自己的要全限定类名
- #{}表示参数形式,里面的名字要与实体类名一一对应
删除操作
- Dao接口
/***
* 根据id删除
* @param id
*/
void deleteUser(int id);
- 映射配置文件
<!--删除用户-->
<delete id="deleteUser" parameterType="Integer">
delete from user where id=#{uid}
</delete>
- 要点
- 在此处的参数为int类型所以在映射配置文件中的parameterType既可以为Integer也可以为int
- SQL语句中参数#{}中的名字没有要求
更新操作
- Dao接口
/***
* 更新用户
*/
void updateUser(User user);
- 映射配置文件
<!--更新用户-->
<update id="updateUser" parameterType="com.okt.domain.User">
update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id = #{id}
</update>
- 要点
- 与添加用户操作相似,参数类型使用全限定类名
查询操作
- dao接口
/**
* 查询所有操作
*/
List<User> findAll();
/***
* 根据姓名模糊查询
* @param id
* @return
*/
User findById(int id);
- 映射配置文件
<!--配置查询所有-->
<select id="findAll" resultType="com.okt.domain.User">
select * from user
</select>
<!--根据名称模糊查询-->
<select id="findByName" parameterType="String" resultType="com.okt.domain.User">
select * from user where username like #{username}
/*select * from user where username like '%#{username}%'*/
</select>
- 要点
- 普通查询跟更新操作和插入操作相似
- 模糊查询中我们要在给予的参数中为其添加%%,而不是在映射配置文件中的sql语句中添加,若要添加则再参数位加单引号但不推荐,因为不是预处理模式所以容易产生SQL注入
SelectKey
- 获取插入后的自增id并封装在user中。
<!--配置插入以后的id-->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
- 在插入语句的映射配置中配置
- keyProperty:对应实体类属性
- keyColumn:对应数据库属性
- resultType:返回值类型
- order:执行时机
使用实体类封装对象
- 映射配置文件
<select id="findByUserByVo" parameterType="com.okt.domain.QueryVo" resultType="com.okt.domain.User">
select * from user where username like #{user.username}
<!--select * from user where username like '%#{username}%'-->
</select>
- 封装对象的实体类
public class QueryVo {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
-
要点
- 在参数为非原本我们要获取的值的实体类时,我们要通过
对象.属性名来获取
实体类属性名重要性
- 实体类属性名在mybatis的映射配置中需要参数为该实体类的时候,其参数名必须与实体类属性名相同,否则将无法进行对应反射
- 但是因为在Windows系统中的mysql是不区分大小写所以可以有大小写不同的情况
- 在linux系统中的mysql是严格区分大小写的
属性名不相同解决方法
-
起别名
- select username as userName …… from user
-
配置列名和实体类属性名的对应关系
-
<resultMap id="userMap"type="com.okt.domain.User"> <!--主键字段对应--> <id property="userId" column="id"> <!--非主键字段对应--> <result property="userName" column="username"> </resultMap>
-
<select id="findAll" resultMap="userMap">
-
查询映射的返回类型要改成resultMap中的id
-
Mybatis连接池
- mybatis连接池有三种配置方式
- 配置位置
- 主配置文件SqlMapConfig.xml文件中的dataSource标签,type属性则表示采用哪种连接池方式
- type的取值
- POOLED:传统的datasource连接池,mybatis中有针对它的实现
- UNPOOLED:采用传统的获取连接方式,虽然也实现了datasource接口,但是没有使用池的思想,即每次用都连接一次
- JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所拿到的思想,如果不是web或者maven的war工程,是不能使用的,我们实际开发中使用的是Tomcat服务器,采用的连接池就是dbcp
- type的取值
- 主配置文件SqlMapConfig.xml文件中的dataSource标签,type属性则表示采用哪种连接池方式
- 配置位置
unpooled配置
<dataSource type="UNPOOLED">
- UNPOOLED:采用传统的获取连接方式,虽然也实现了datasource接口,但是没有使用池的思想,即每次用都连接一次
pooled配置
<dataSource type="UNPOOLED">
- POOLED:POOLED的思想是用户需要连接数据库的时候首先会检测连接池是否有足够的空间连接,若有则分配,若无则会查看活动池是否有足够空间开辟一个新的连接,若有则开辟并分配给用户,若无则去活动池寻找最老的一个或者说是最连接池最早活动的连接并将其变成一个全新的连接分配给用户
Mybatis的事务
面试问题
- 什么是事务
- 事务的四大特性ACID
- 不考虑隔离性会产生的三个问题
- 解决办法:四种隔离级别
如何提交事务
- 它是通过sqlsession对象的commit方法和rollback实现事务的提交和回滚
如何自动提交事务
-
在获取SqlSession对象时将openSession方法中添加参数true即可
-
SqlSession sqlSession = factory.openSession(true);
-
最好一个操作手动事务,不然多个连接都自动提交事务会控制不住。
JNDI扩展知识
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d50cIjmd-1579623120610)(D:\BaiduNetdiskDownload\31.会员版(2.0)]-就业课(2.0)-Mybatis\mybatis\mybatis_day03\资料\JNDI\JNDI.png)
Mybatis缓存
延迟加载和立即加载的概念
延迟加载
-
在真正使用数据时才发起查询,不用的时候不查询,按需加载(懒加载)
-
延迟加载是不会像我们立即加载那样一股脑的直接给你查询出来,当你需要什么的时候他就查询什么例如我们只需要查询account表的时候
-
Test类
@org.junit.Test
public void testSelect() throws IOException {
//5.使用代理对象执行方法
List<Account> accounts = dao.findAll();
// for (Account account : accounts) {
// System.out.println("<----id的信息----->");
// System.out.println(account);
// System.out.println(account.getUser());
// }
}
- 主配置文件
<settings>
<!--开启mybatis全局延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
- 映射配置文件
<association property="user" column="uid" javaType="user" select="com.okt.dao.UserDao.findById">
column:指resultmap中从表以主表的哪个为主
- 控制台输出
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbasTpn6-1579623120613)(C:\Users\okt\Desktop\java学习(markDown)]\img\延迟加载.png)
可以看出当我们仅仅对account类有关联的操作时,mybatis只会进行对account表的查询而并不会查询user表。
立即加载
- 不管用不用,只要一调用方法,马上发起查询
四种表关系的选择
- 一对多,多对多:通常情况下我们采用延迟加载
- 多对一,一对一:通常情况下我们采用立即加载
缓存概念
-
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
-
什么是缓存?
- 存在于内存的临时数据
-
为什么使用缓存
- 减少和数据库的交互次数,提高执行效率
-
什么样的数据能使用缓存,什么样的数据不能使用?
- 适用于缓存:
- 经常查询并且不经常改变。
- 数据的正确与否对最终结果影响不大
- 不适用缓存:
- 经常改变的数据
- 数据的正确与否对最终结果影响很大
- 例如:商品库存,银行汇率
- 适用于缓存:
Mybatis中的一级缓存
-
它指的是Mybatis中SqlSession对象的缓存
-
当我们执行查询之后,查询的结果会同时存入SqlSession为我们提供的一个区域
-
该区域的结构是一个Map。当我们再次查询同样的数据,mybatis会先去sqlsession中查询是否有,有的话直接拿出来用。
-
当SqlSession对象消失时,mybatis的缓存也就消失了
-
在mybatis中一级缓存是自带的,如果要清除缓存需要使用此方法
//清空缓存
sqlSession.clearCache();
- 切记当调用sqlsession的增删改查操作和comit和close,就会清空一级缓存
Mybatis中的一级缓存
-
它指的是Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其缓存
-
二级缓存的使用步骤
- 让mybatis框架支持二级缓存,在SqlMapConfig.xml中配置
<settings> <!--全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。默认true--> <setting name="cacheEnabled" value="true"/> </settings>
- 让当前的映射文件支持二级缓存,在映射配置文件中配置
<!--开启User支持二级缓存--> <cache/>
- 让当前的操作支持二级缓存
<!--根据id查找一个用户--> <select id="findById" parameterType="Integer" resultType="user" useCache="true"> select * from user where id = #{id} </select>
-
要点
- 二级缓存存放的是数据而不是对象例如
- {“id”:1,“username”:“老王”,“address”:“北京”}
- 在我们进行第二次查询时,它不是把对象拿给我们,而不是重新创建一个新的对象然后把这些数据在赋值给这个对象。所以两个对象是不同的。
mybatis注解开发
主配置文件要点
- 在主配置文件中如果我们在绑定映射位置时使用了package的标签,那么mybatis会自动识别有注解的就用注解,有xml文件就用xml文件,当两者都存在时会报错。
- 要么使用注解,要么使用xml,两者不能位于同一位置
注解开发解决属性名不对应
- 使用注解@Results,并在其value中添加@Result进行匹配,可以添加标识符id来为其他需要用到该实体类的操作,无需重复配对。
/***
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "id"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
})
- 调用此配对
@ResultMap(value = {"userMap"})
一对一的情况
- 在一对一和一对多的配置中,首先我们要在从表中添加主表的对象属性
- 然后在接口配置中进行如下配置
/***
* 查询所有账户,并且获取所属账户的用户信息
*/
@Select("select * from account")
@Results(id="accountMap",value = {
@Result(id = true,column = "id",property = "id"),
@Result(column = "uid",property = "uid"),
@Result(column = "money",property = "money"),
@Result(column = "uid",property = "user",one=@One(select = "com.okt.dao.UserDao.findOne",fetchType = FetchType.EAGER))
})
- 与解决不同属性名的方法不同是这里的@Result多了个one属性,这是代表我们一对一的一个注解,其select属性为我们主表的操作接口方法,fetchType为设置延迟加载,column为主表根据从表的哪个列的值来查询
一对多情况
- 一对多相对于一对一,在注解上我们是使用了叫many的属性和对应注解
- 在
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "id"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
@Result(property = "accounts" ,column="id",many = @Many(select = "com.okt.dao.AccountDao.findAccountByUid",fetchType = FetchType.LAZY))
})
- 一对多的情况下我们会在一的实体类中添加一个多的那方的一个集合属性
注解开发二级缓存
属性为我们主表的操作接口方法,fetchType为设置延迟加载,column为主表根据从表的哪个列的值来查询
一对多情况
- 一对多相对于一对一,在注解上我们是使用了叫many的属性和对应注解
- 在
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "id"),
@Result(column = "username",property = "userName"),
@Result(column = "address",property = "userAddress"),
@Result(column = "sex",property = "userSex"),
@Result(column = "birthday",property = "userBirthday"),
@Result(property = "accounts" ,column="id",many = @Many(select = "com.okt.dao.AccountDao.findAccountByUid",fetchType = FetchType.LAZY))
})
- 一对多的情况下我们会在一的实体类中添加一个多的那方的一个集合属性
注解开发二级缓存
- 在对应的dao接口上添加@CacheNamespace(blocking = true)即可,默认是关闭