目录
一、初识Mybatis
(一)Mybatis的原理
(二)Mybatis的配置
二、基于SqlSession的单表增删改查
(一)无参数的
(二)有参数的
(三)SqlSession总结
三、基于Sql动态代理的单表增删改查
四、MybatisSql语句的动态拼接
五、Automapping自动注入和自定义注入
(一)Automapping自动注入
(二)自定义注入
六、动态代理的多表联合查询
(一)业务装配方式(N+1方式)
(二)ResultMap嵌套查询(N+1方式)
(三)ResultMap嵌套结果
七、Mybatis的注解
八、Mybatis的缓存
(一)一级缓存
(二)二级缓存
(三)清空缓存
1.flush()对缓存的影响
3.close()对缓存的影响
3.commit()对缓存的影响
九、基于ThreadLocal的封装
(一)ThreadLocal
(三)基于ThreadLocal的封装
十、责任链设计模式
在MyBatis开始运行时,先通过Resources加载全局配置文件,然后实例化SqlSessionFactoryBuilder构建器,帮助SqlSessionFactory接口实现类DefaultSqlSessionFactory。SqlSessionFactoryBuilder构建器调用build方法,在build方法中创建XmlConfigBuilder(解析器对象)解析配置文件流(也就是我们传入的InputStream is),并把解析结果存放在Configuration-(
//作用:获取资源配置文件的流对象,便于对资源文件的读取和解析
InputStream is = Resuorces.getResourceAsStream("mybatis.xml");
/**
* 作用:快速创建DefaultSqlSessionFactory对象,用来生产SqlSession对象
* 内部操作:
* XMLConfigBuilder:用来解析XML配置文件的工具类
* Configuration:存储XML解析结果的实体类
* DefaultSqlSessionFactory:用来生产SqlSession对象的工厂类
*/
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
/**
* 作用:生产SqlSession对象,用来完成对数据库的操作
* 内部操作:
* Environment:存储了数据库环境信息(来自Configuration对象)
* TransactionFactory:用来生产事务管理对象(来自Configuration对象)
* executor:执行器,存储了事务管理对象和数据库操作的模式SIMPLE
* DefaultSqlSession:用来操作数据库的对象
*/
SqlSession ss = factory.openSession();
(1)mapper.xml文件的配置
insert into flower values(default,'随便花',199,'bjsxt')
update flower set name='随便花' where id = 6
dalete from flower where id = 6
(2)在java中的应用
public class Teast{
public void main(String[] args) throws IOException{
//1.获取SqlSession对象
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionBuilder().build(is);
SqlSession ss = factory.openSession();
//2.使用SqlSession对象完成数据库操作
//a.查询
List f = ss.selectList("com.bjsxt.mapper.FlowerMapper.selAll");
//b.增加
int i = ss.insert("com.bjsxt.mapper.FlowerMapper.insF");
//c.修改
int i2 = ss.update("com.bjsxt.mapper.FlowerMapper.upF");
//d.删除
int i3 = ss.delete("com.bjsxt.mapper.FlowerMapper.delF");
}
}
(1)mapper.xml文件的配置
insert into flower values(default,#{name},#{price},#{producation})
(2)在java中的应用
public class Test{
public void main(String[] args) throws IOException{
//1.获取SqlSession对象
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession ss = factory.openSession();
//2.使用SqlSession对象完成数据库的操作
//a.一个参数
Flower f = ss.selectOne("com.bjsxt.mapperFlowerMapper.selF",2);
//b.多个参数
Flower f1 = new Flower();
fp.setId(1);
fp.setNmae("别的花");
Flower f2 = ss.selectOne("com.bjsxt.mapper.FlowerMapper.selF2",f1);
//c.
Map map = new HashMap();
map.put("name","彼岸花");
map.put("price","99.99");
map.put("production","彼岸");
int i = ss.insert("com.bjsxt.mapper.FlowerMapper.insF2",map)
}
}
(1)因为SqlSession提供的数据库操作方法声明的实参类型为object,需要通过该属性将数据强制转换为对应的类型。在查询标签上使用parameterType属性声明参数类型。
(2)在查询标签上使用parameterType属性声明参数类型
(3)在SQL语句中使用#{0}(基本类型的数据都可以使用角标来占位)、#{属性名|健名}进行占位
(4)第一个参数为要执行的Sql语句的全限定路径,第二个参数为接收的实参。
(5)因为SqlSession对象提供的数据库操作方法只接收一个实参,所以,如果Sql语句中需要使用多个参数,我们必须将参数封装成一个对象,将该对象传递给方法完成数据库操作
(1)mapper.xml文件的配置
(2)在java中的应用
public interface FlowerMapper{
//注意:形参名和xml中的键没有任何关系!只能用@Param()建立映射联系
//无参数
List selF();
//单参数
Flower selById(int id);
Flower selByIdName(Flower f);
//多参数
Flower selByIdName2(int id,String name);
Flower selByIdName3(int id,Flower f);
//注解方式
Flower selByIdName4(@Param("id")int id,@Param("flower")Flower f);
}
(1)
(2)
(3)
(4)
update flower
id=#{param3},
name=#{param1},
production=#{param2}
where id=#{param3}
(5)
update flower
id=#{param3},
name=#{param1},
production=#{param2},
where id=#{param3}
(6)
(7)
(8)
price, production
Mybatis默认按照字段名和属性名一致的规则将查询的数据注入到实体类的对象中,这种方式叫Automapping自动注入。如果实体类的属性名和字段名不一致,那么就需要我们使用自定义注入。
自定义注入:提前声明查询结果和实体类之间的注入规则,就是告诉Mybatis将哪个字段值赋给实体类的哪个属性。一般用于联合查询|单表查询但是字段名和属性名不一致的时候。在查询标签上使用ResultMap属性声明要引入的规则。
注意:如果是单表查询的自定义注入可以只声明字段名和属性名不同的注入。但是如果是多表联合查询,不管字段名和属性名是否一致都要全部自定义注入。
概念:很多时候,查询需要的结果分布在多张表中。以前我们使用联合查询语句一次性将数据查询出来。但是,其实多表联合查询可以分开成多个单表查询,比如查询学生及其教师,可以分为两个步骤:①查询所有的学生②根据查询的学生的教师编号查询教师信息。我们将以上的思路实现在业务层,将这种方式称为业务装配。
好处:将多表联合查询转成了单表查询,便于书写SQL语句。
缺陷:①提升了业务层的代码量。②对数据库的IO操作非常频繁,SQL语句被执行了N+1次。
问题:在学习了业务装配方式后,其实就是将多表联合查询拆分单表查询,然后在业务层将数据根据表关系进行填充装配。那么,能不能把在业务层装配的动作发生在数据库呢?这样简化了业务层的代码压力。
解决:使用ResultMap N+1方式
概念:因为数据库层的代码是基于SqlSession对象动态生成的,所以需要我们手动声明业务装配的注入规则。使用resultMap标签声明规则,把此种方式称为ResultMap N+1方式。
概念:所谓注解其实就是在代码中使用特殊的配置方式来替换我们在XML文件中的配置信息。
好处:①简单好用②保护原始配置文件③提升开发效率
缺点:配置信息和代码的耦合性变高了。注解配置的信息一旦复杂,阅读性降低。
使用时机:建议单表的增删改查,而且不经常迭代更新的代码,可以考虑使用注解,注解和配置文件的区别。注解是用来替换XML的配置信息,同一个配置信息要么使用注解,要么使用XML配置。在同一个项目中注解和XML配置是可以同时存在的。
public interface FlowerMapper{
//查询注解
@Select("select * from flower")
List getFlowerInfo();
@Select("select * from flower where id=#{0} and name=#{1}")
Flower getFlowerById(int id,String name);
//增加注解
@Insert("insert into flower values (default,#{0},#{1},#{2})")
int insFlower(String name,double price,String production)
//修改注解
@Update("update flower set name=#{1} where id=#{0}")
int upFlower(int id,String name);
//删除注解
@Delete("delete from flower where id=#{0}")
int delFlower(int id);
}
缓存的概念:所谓缓存其实就是将经常被操作的数据临时的存储到当前应用程序的内存空间中,提升数据的读取效率。数据是临时数据,应用程序关闭,则数据丢失,除非在应用程序关闭的时候进行数据的持久化存储。
缓存文件:存储缓存数据的持久化文件,删除不会影响程序的正常使用,数据存储的位置:电脑内存。
特点:缓存的数据不会永久保存,除非数据持久化。缓存的空间是有上限的,达到上限后,新的数据会覆盖原有数据。缓存可以提升数据的读取效率,降低IO操作的次数。
缓存由接口Cache定义,整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache永久缓存(永久缓存:缓存能随意的写入硬盘,允许昂贵的创建数据来保持缓存,甚至能让应用重启)实现。
一级缓存是基于PerpetualCache的HashMap本地缓存,其存储作用域为SqlSession,当SqlSession flush或close之后,该SqlSession中所有Cache就将清空。同一个SqlSession多次调用同一个Mapper和同一个方法的同一个参数,只会进行一次数据库查询,然后把数据缓存到缓存中,以后直接先从缓存中取出数据,不会直接去查询数据库,提升了数据的读取效率。因为不同的SqlSession都是相互隔离的,所以如果使用的是不同的SqlSession对象操作相同的Mapper、参数和方法,他还是会再次发送到SQL到数据库去执行,返回结果。
存储作用域为SqlSession的原因:保存在执行器中,而执行器又在SqlSession中,所以一级缓存的生命周期与Sqlses-sion是相同的。
public class TestMybatis{
public static void main(String[] args) throws IOException{
//获取SqlSession对象
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession ss = factory.openSession();
SqlSession ss2 = factory.openSession();
//获取Mapper接口对象
FlowerMapper fm = ss.getMapper(FlowerMapper.class);
FlowerMapper fm2 = ss2.getMapper(FlowerMapper.class);
//一级缓存:SqlSession对象
//第一个SqlSession对象
List lf1 = fm.selF();
List lf2 = fm.selF();
ss.close();
//第二个SqlSession对象
List lf3 = fm2.selF();
List lf4 = fm2.selF();
ss2.close();
}
}
问题:不同的请求,服务器都会创建一个线程进行处理,每个线程都会创建一个SqlSession对象,如果请求的是相同的数据,则该数据会被缓存多次,造成空间浪费。
解决:开启二级factory缓存。
二级缓默认也是采用PerpetualCache的HashMap缓存,不同在于其存储作用域为mapper,并且可自定义存储源,如Ech-ache。每个Mapper享有同一个二级缓存域。二级缓存以namespace名称空间为其唯一标示,被保存在configuration核心配置对象中,因此二级缓存的生命周期与SqlSessionFactory是相同的。在创建每个MapperedStatement对象时,都会根据其所属的namespace名称空间,给其分配Cache缓存对象。
public class Configuration{
//(StricMap是Mybatis底层创建Map的一个类,封装了Map的创建)
protected final Map x = new StricMap("Caches collection");
}
二级缓存是需要配置来开启的,在Mybatis.xml配置文件中加上以下代码:
然后在Mapper映射文件中添加一行:
redOnly:只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势,可读写的缓存会返回缓存对象的副本(通过序列化),这会慢一些,但是安全,因此默认是false。这里的缓存对象指的是:MapperStatement对象。
flushInterval(刷新间隔)可以被设置为任意的正整数,而且他们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目,默认值是1024。readOnly(只读)属性可以被设置为true或false。
如果已经开启二级缓存的mapper里面的某个查询不需要缓存,可以使用useCache=“false”禁用二级缓存。
在mapper的同一个namespace中,如果有其他C/U/D操作后都需要执行刷新缓存操作,来避免脏读。无论是一级缓存还是二级缓存,C/U/D操作commit提交前会清空缓存区域,使缓存失效。(commit会清空一级缓存二级缓存,flush会清空一二级缓存执行SQL语句,但是不会将当前事务加进事物管理中,)合理利用二级缓存可以提高系统性能,减少数据库压力。但是,如果使用不当可能会出现缓存一致性问题,对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度。
mybatis二级缓存对细粒度的数据级别的缓存实现不好,对同时缓存较多条数据的缓存,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其他商品的信息,因为mybatis的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。针对性缓存在service和dao中需要缓存时,只用AOP进行拦截,创建缓存层,这是AOP思想的实现,实现缓存与业务的解耦。
我们系统为了提高系统开发、性能,一般对项目进行分布式部署。缓存在各个服务单独存储,不方便开发,所以要使用分布式缓存对缓存数据进行集中管理,mybatis无法实现分布式缓存,所以需要用到redis(实现了Mybatis的自定义缓存)、EhCache、merncache等。
flush做了两件事:①清空缓存②执行SQL。这样可以避免脏读。会清空一级缓存,不会影响二级缓存。但是这个时候的SQL语句,只是从缓存中放到了事务中,并没有提交到数据库。
close()会将其一级缓存放进二级缓存中,SqlSession关了,一级缓存就不存在了。执行close时,如果autoCommit为false,只会清空自身的缓存,为true,会清空二级缓存。
commit()会将其一级缓存放进二级缓存中,并清空二级缓存。会清空二级缓存,清空一级缓存(这是执行增删改SQL的效果)。
在解决多线程安全问题的时候,为了让线程共享资源,必须小心的对共享资源进行同步,同步带来一定的效能延迟,而另一个方面,在处理同步的时候,又要注意对象的锁定与释放,避免产生死结,种种因素都使得多线程程序变得困难。尝试从另一个角度来思考多线程共享资源的问题,既然共享资源这么困难,那么就干脆不要共享,为每个线程创造一个资源的副本。将每个线程存取数据的行为加以隔离,实现的方法就是给予每个线程一个特定空间来保管该线程所独享的资源。
ThreadLocal的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值得副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
ThreadLocal的原理:在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。比如下面的示例实现:
public class ThreadLocal{
//synchronizedMap这里用了装饰者模式,包装了hashmap,基类是不同步的,包装器是同步的
private Map values = Collections.synchronizedMap(new HashMap());
public Object get(){
//currenThread()返回对当前正在执行的线程对象的引用
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
//如果map集合中的值o为null 并且 map集合不包含curThread这个键 那么执行if里面的语句
if(o == null && values.containsKey(curThread)){
o = initialValue();
values.put(curThread,o);
}
return o;
}
//将当前线程和线程安全的变量放进map集合
public void set(Object newValue){
values.put(Thread.currenThread(),newValue);
}
//初始化Map的值 也就是ThreadLocal想要实现线程安全的变量
public Object initialValue(){
return null;
}
}
问题:目前我们使用Mybatis进行数据库操作,在业务层书写的代码,会造成,每个请求都会创建SqlSessionFactory对象,这样造成factory的缓存(二级缓存)起不了作用。
解决:①不同的线程需要使用同一个SqlSessionfactory对象,使factory线程共享。②不同的线程需要创建不同的SqlSession对象,使SqlSession非线程共享。③同一个线程内获取同一个SqlSession对象,使SqlSession对象线程内共享。(针对第三点:传参也可以保住获取的是同一个对象,但是麻烦,如果我们使用一个封装好的类,对外提供一个静态的方法来获取该对象,也可以保证对象的唯一性。在下面的封装中,使用的是单例模式)
(1)MybatisUtil
public class MybatisUtil{
//声明静态的factory属性
private static SqlSessionFactory factory;
//声明静态的ThreadLocal属性
//static修饰的对象在内存中只会有一个拷贝,所以static修饰的
private static ThreadLocal ts = new ThreadLocal();
//声明静态代码块
static{
//获取流对象
try{
InpuStream is = Resources.getResourceAsStream("mybatis.xml");
factory = new SqlSessionFactoryBuilder().build(is);
}catch(IOException e){
e.printStackTrace();
}
}
//封装方法获取SqlSession对象
public static SqlSession getSqlSession(){
SqlSession ss = ts.get();
if(ss == null){
/**
* openSession()通过传参设置autoCommit的值,模式为false,关闭自定提交。
* false:当前所有操作作为一个事务,当执行增删改时,发生异常时,
* 会执行rollback(),之前所有对数据库的操作全部取消。
* true:自动事务,后面对数据库的操作发生异常,不会影响前面对
* 数据库的操作。rollback只会回滚当前一个事务。
*/
ss = fatory.openSession();
ts.set(ss);
}
return ts.get();
}
//关闭SqlSession对象
public static void closeSqlSession(){
SqlSeesion ss = ts.get();
if(ss != null){
ss.close();
ts.set(null);
}
}
}
(2)过滤器调用实现
@WebFilter("/*")
public class MyFilter implements Filter{
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest req,ServletResponse resp,FilterChain chain)
throws IOException,ServletException{
//放行
try{
//设置编码格式
chain.doFilter(req,resp);
//提交
MybatisUtil.getSqlSession().commit();
//关闭SqlSession对象
MybatisUtil.closeSqlSession();
}catch(Exception e){
MybatisUtil.getSqlSession().rollback();
MybatisUtil.closeSqlSession();
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {}
}
责任链概念:(23种设计模式中的一种,是行为型模式)一个功能的完整处理,需要多个对象之间的联动操作,而每个对象负责的处理内容不同,或者说,责任不同,并且形成了一个链条式的执行机制,我们把该链条称为责任链。
基于MVC的Web开发的责任链的特点:一个请求,一个线程,一条责任链。