11.Mybatis的数据插入、修改与删除的操作
在学习mybatis的写操作之前,想要学习一下数据库的事务。
MySQL相关知识拓展----事务
(1)事务简介
一个或一组SQL语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行。一个事务其实就是一个完整的业务逻辑。很多情况下,完成一个业务需要多条sql语句,每条sql语句是相互依赖的,如果单元中的某条sql语句受到影响或产生错误,整个业务单元将会回滚到以前的状态。只有DML语句(增、删、改)才有事务一说,其他语句没有事务。 只要有涉及到数据的增删改,那么就一定要考虑到安全问题。数据库事务是保证数据操作完整性的基础。
比如:
银行转账,A用户向B用户转账10000元。
业务逻辑:
将A用户的钱减去10000元(update语句) ; 将B用户的钱增加10000元(update语句)。
这就是一个完整的业务逻辑(即事务)。
以上的操作是一个最小的工作单元,不可再分,要么同时成功,要么同时失败。两个update语句必须同时成功,同时失败。
(2)事务的ACID属性(事务的特点)
原子性:原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性:事务必须使数据库中从一个状态变为另一个一致状态。
隔离性:一个事务不能被其他的事务干扰
持久性:事务被最终结束的一个保障。
(3)提交事务
在事务的执行过程中,每一条DML语句的操作都会记录到“事务性活动的日志文件”中。提交事务就是清空事务性活动的日志文件,将数据全部彻底地持久化到数据库中。提交事务标志着事务的结束。并且是一种全部成功的结束。MySQL在默认情况下自动提交事务,每执行一条DML语句,提交一次。
(4)回滚事务
将之前所有的DML语句的操作全部撤销,清空事务性活动的日志文件。回滚事务也表示事务的结束,并且是一种全部失败的结束。回滚只能回滚到上一次的提交点。
(5)事务之间的隔离级别
1. 读未提交 read uncommitted (最低的隔离级别)
事务A可以读取到事务B未提交的数据。会读到脏数据。这种隔离级别非常少用。(事务B没有提交就读到了)
2. 读已提交 read committed
事务A只能读取到事务B提交之后的数据。这种隔离级别解决了脏读的现象。但存在一些问题:不能重复读取数据。比如在事务开启后,第一次读取到的数据是3条,但是如果当前事务还没有,可能第二次再读取的时候,读到的数据是4条。(提交之后才能读到)
3. 可重复度 repeatable read
事务A开启之后,不管过多久,每一次在事务A中读取到的数据都是一致的。即使事务B将数据已经修改,并且提交了,事务A读取到的数据还是没有发生改变。(提交提交之后也读不到)
4. 序列化/串行化 serializable(最高隔离级别)
这是最高隔离级别,但效率最低。虽然解决了所有的问题。但事务排队,不能并发。
MySQL的默认隔离级别是repeatable read
好的,了解完MySQL的事务的相关知识点后,就可以学习mybatis的写操作了。mybatis的写操作包含3种,分别是插入<insert>,更新<update>和删除<delete>。
Mybatis数据插入操作
编写goods.xml文件,添加如下的代码:
<insert id="insert" parameterType="org.haiexijun.entity.Goods"> insert into t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery,category_id) values(#{title},#{subTitle},#{originalCost},#{currentPrice},#{discount},#{isFreeDelivery},#{categoryId}) <selectKey resultType="Integer" keyProperty="goodId" order="AFTER"> select last_insert_id() </selectKey> </insert>
insert标签有id和parameterType两个属性。然后里面添加insert语句,用 #{ } 来传入参数。
注意:insert into 后面的键名有下划线,而values()里面的#{ } 里面的字段名没有下划线,是驼峰的,不要写错了,不然会报错哦。
会发现,我们并没有在insert语句中传入主键goods_id ,要通过<selectKey>来插入主键goods_id.
我们一般用<selectKey>来设置主键。它里面会有一条select语句,意思是查询插入数据后的最后的id值。<selectKey>会将里面查询的id值赋值给goods_id.
selectKey 元素的属性
在goods.xml中编写好insert后,在MybatisTest测试类中编写测试方法:
@Test public void testInsert(){ SqlSession session=null; try { session=MybatisUtils.openSession(); Goods goods=new Goods(); goods.setTitle("测试商品"); goods.setSubTitle("测试商品的子标题"); goods.setOriginalCost(200f); goods.setCurrentPrice(100f); goods.setDiscount(0.5f); goods.setIsFreeDelivery(1); goods.setCategoryId(43); System.out.println(goods.getSubTitle()); //insert()方法的返回值代表本次成功插入的记录总数 int n=session.insert("goods.insert",goods); System.out.println(n); //提交事务 session.commit(); }catch (Exception e){ //回滚事务 if (session!=null){ session.rollback(); } e.printStackTrace(); }finally { MybatisUtils.closeSession(session); } }
案例使用的是sqlSession的insert方法来才对数据进行插入操作。里面传入2个参数,第一个参数是insert语句的映射,第二个参数是要插入的数据,传入一个Goods实例。 insert()方法的返回值代表本次成功插入的记录总数。
执行完insert语句后,如果成功要对事务进行提交(session.commit()),失败要在catch里面把事务回滚(session.rollback())。
useGeneratedKeys属性的用法
上面的案例用selectKey来给插入数据的主键值取值。其实还可以用useGenetatedKeys属性来完成。下面来介绍这个属性的用法。
<insert id="insert" parameterType="org.haiexijun.entity.Goods" useGeneratedKeys="true" keyProperty="goodId" keyColumn="goodId"> insert into t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery,category_id) values(#{title},#{subTitle},#{originalCost},#{currentPrice},#{discount},#{isFreeDelivery},#{categoryId}) </insert>
useGeneratedKeys属性仅适用于 insert 和 update,这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键,默认值:false。
keyProperty属性仅适用于 insert 和 update,指定能够唯一识别对象的属性(主键),如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn属性仅适用于 insert 和 update,设置生成键值在表中的列名,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。
selectKey标签与useGeneratedKeys标签的区别:
selectKey标签需要明确编写获取最新主键的SQL语句,useGeneratedKeys属性会自动根据驱动生成对应的SQL语句。二者的应用场景也不同:selectKey适用于所有的关系型数据库而useGeneratedKeys只支持“自增主键”的数据库。
数据的更新与删除操作
数据的更新与删除操作与前面的插入数据的操作非常的相似,只不过用的标签不一样罢了。其实用法非常简单啦。
更新操作(update)
先在goods.xml中配置update语句:
<update id="update" parameterType="org.haiexijun.entity.Goods"> update t_goods set title = #{title}, sub_title = #{subTitle}, original_cost = #{originalCost}, current_price = #{current_price}, discount = #{discount}, is_free_delivery = #{isFreeDelivery}, category_id = #{categoryId} where goods_id=#{goodId} </update>
然后在MybatisTest测试类中编写测试方法:
@Test public void testUpdate(){ SqlSession session=null; try { session=MybatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",2679); goods.setTitle("更新的测试商品"); goods.setSubTitle("更新的测试商品的子标题"); goods.setOriginalCost(300f); goods.setCurrentPrice(150f); int n=session.update("goods.update",goods); //提交事务 session.commit(); }catch (Exception e){ //回滚事务 if (session!=null){ session.rollback(); } e.printStackTrace(); }finally { MybatisUtils.closeSession(session); } }
删除操作(delete)
删除操作非常的简单,下面来演示一下具体操作:
先在goods.xml中配置delete语句:
<delete id="delete" parameterType="Integer"> delete from t_goods where goods_id=#{value}; </delete>
然后在MybatisTest测试类中编写测试方法:
@Test public void testDelete(){ SqlSession session=null; try { session=MybatisUtils.openSession(); session.delete("goods.delete",2683); //提交事务 session.commit(); }catch (Exception e){ //回滚事务 if (session!=null){ session.rollback(); } e.printStackTrace(); }finally { MybatisUtils.closeSession(session); } }
12预防SQL注入攻击
SQL注入攻击是指攻击者利用SQL漏洞,绕过系统约束,越权获取数据的攻击方式。
Mybatis的两种传值方式
1 . ${ }替换文本替换,未经任何处理对SQL文本进行替换
2 . #{ }预编译传值,使用预编译传值可以预防SQL注入(我们一直在使用的方式)
三.Mybatis进阶-----高级特性
1.Mybatis日志管理
日志:日志文件是用于记录系统操作事件的记录文件或文件集合。日志保存历史数据,是诊断问题以及理解系统活动的只要依据。
说起Java中的日志,我们就必须要说到上面这个特殊的结构了。
在java中,日志其实分成2部分,一个叫做日志门面,一个叫作日志实现。门面和具体的实现是什么意思呢?
举一个生活中的例子,家家户户都有插排,插排会有2个空的和3个孔的,在中国,所有插排的规格都是统一的,那么统一的这个插排的插孔的设计就是门面,但是对于不同的插排里面的电路结构的是不同的,比方说公牛和小米等等等。有的设计了安全保护电路,有的使用了其他材料。不同的品牌都有不同的设计。但是呢,如果作为最终的产品体现出来的话,他们的规格都是相同的。但是里面具体的实现是不同的。真是因为门面和实现进行了区分,通过统一的门面,屏蔽了底层复杂的实现,所以我们的插排在中国任何地方都可以通用。
诸如此类的设计在我们Java中也是存在的,作为日志来说也是一样。上面图中提到了两个不同的日志门面,SLF4J和Commons-Logging。他们的作用就像是我们插排的面板一样,为java提供了统一的日志调用接口。
SLF4J和Commons-Logging 是两个不同的厂商开发的,而且这两个门面组件在我们日常的开发中都使用得很广。所以呢,有的公司使用的是SLF4J的日志门面,有的公司的使用的是Commons-Logging 这个日志门面。
日志门面的下面,有各种日志门面的实现,我们都知道,在java中,有各种各样的组织开发了各种各样的产品,那对于日志来说也是这样的。诸如有log4j、logback、java.util.logging(jul)等等。这些组件提供了日志的打印、输出与管理。
正是因为在在日志中,基于门面和实现进行了彼此分开,所以给我们程序迁移提供了极大的便利。
举个例子,比方说,我现在开发了一个Java程序,底层使用了SLF4J作为日志门面,而在后面我使用了log4j这个日志组件。但是呢,随着技术的不断发展和延伸,logback从性能和设计上都比log4j要好,那我要做的就是把log4j这个jar包从系统中剔除,加入logback这个jar包就可以了。最为我们程序访问的门面不用做任何的调整,SLF4J自动的会完成从log4j到logback得迁移工作。也就是说,我们的程序当面向了统一的门面的时候,底层具体的实现对于程序的调用者来说已经不重要了。这个切换的工作是由日志门面自动帮我们完成的。
那对于实现来说,目前主流的日志组件有log4j、logback等,但是目前的主流是logback,log4j在早期非常有名,log4j的作者对log4j的改进是logback。
Mybatis底层就是通过SLF4J支持logback。
下面通过一个案例来演示如何让mybatis的logback的使用,来输出日志:
1.在项目的pom.xml中导入logback的maven依赖:
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <scope>test</scope> </dependency>
我们在没有导入logback依赖以前,在运行MybatisTest测试类中运行测试方法不会打印出日志,当我们到入logback的依赖后,就会看到运行测试方法后控制台会打印出很多日志信息。这些信息可以方便我们程序的调试。
下面呢,我们针对于日志的展现和日志的控制细则来进行讲解。
作为logback,它呢是允许对日志进行自定义的。具体的做法呢,是需要在项目的resources目录下新增加一个xml文件,这个xml文件强制要求叫作logback.xml 。这个xml文件用来对logback进行配置
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <appender name="trace" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <patten>%d{HH:mm:ss:SSS} [%thread] %-5level %logger{36} - %msg%n</patten> </encoder> </appender> <root level="trace"> <appender-ref ref="console"/> </root> </configuration>
对于logback.xml不做过多的解释,更多配置参考logback的官网学习。
2.Mybatis动态SQL
动态SQL是指根据参数数据动态组织SQL的技术。
动态SQL听起来似乎非常的高大上,但是在日常项目中,动态SQL的使用场景是非常普遍和广泛的。比如我们在淘宝上搜索笔记本电脑,搜索后,会显示所有的笔记本电脑,在此时我们还可以对电脑进行品牌的筛选等,这就会涉及到SQL语句的拼接了,此时就要用到动态SQL了。
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
下面写一个来体验一下动态SQL:
1.打开goods.xml,编写sql语句:
<select id="dynamicSQL" parameterType="java.util.Map" resultType="org.haiexijun.entity.Goods"> select * from t_goods where //添加1=1 //或包裹where标签 <where> <if test="categoryId != null"> and category_id =#{categoryId} </if> <if test="currentPrice != null"> and current_price < #{currentPrice} </if> </where> </select>
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分,用到了if 标签。如果有>和 <等一些特殊的符号,就要对符号进行转移。如:<要写成< 。1=1起占位作用,防止语法报错。或包裹一个where标签。where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
然后我们在MybatisTest测试类中编写测试方法:
@Test public void testDynamicSQL(){ SqlSession sqlSession=null; try { sqlSession=MybatisUtils.openSession(); Map param=new HashMap(); param.put("categoryId",44); param.put("currenPrice",500); List<Goods> list=sqlSession.selectList("goods.dynamicSQL",param); for (Goods g:list){ System.out.println(g.getTitle()); } }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } }
当然,实际项目中可能不会这样用了,这里只是了解了解它的基本用法。
具体怎么用,我在以后的项目里再慢慢体会。动态SQL除了if标签外还有其他的标签。下面来简单介绍一下其他标签。
choose、when、otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
<select id="choose" parameterType="java.util.Map" resultType="org.haiexijun.entity.Goods"> select * from t_goods where discount=1 <choose> <when test="categoryId != null"> and category_id =#{categoryId} </when> <when test="currentPrice != null"> and current_price < #{currentPrice} </when> <otherwise> and category_id =30 </otherwise> </choose> </select>
传入categoryId就加入查询category_id =#{categoryId}的条件,传入currentPrice,就加入查询current_price < #{currentPrice}的条件,如果都没有传入,就插入otherwise里面程序定义的条件。
3.Mybatis二级缓存
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行<cache/>
二级缓存概述
所谓缓存就是缓冲存储的意思,在mybatis中用于数据优化,提高程序执行效率的有效方式。这里说的有点晦涩。打个比方,比如我现在用SQL语句第一次查询时获取到了婴幼儿奶粉这个商品,紧接着,因为程序的需要,可能我还需要重新再获取一次婴幼儿奶粉。那这个时候,如果按照原始设计,它需要再次从数据库中,把婴幼儿奶粉的数据提取出来。可试想一下,MySQL是把数据存储在硬盘上的,硬盘提取数据的速度并不是很快。同时,我们第一次和第二次获取数据的时候,他们都返回了相同的数据。如果要对这种情况进行优化的话,可以把第一次查询的数据放在内存中的某个区域中,当在次获取这个数据的时候,不去读取数据库,而是直接从内存中读取数据就可以了。因为内存的速度是非常快的,所以可以极大地提高我们程序的执行效率。我们这种把数据存储在内存中的方式其实就是缓存的最基础和最底层的实现。那么,在mybatis中,缓存到底是如何设计的呢?
mybatis中存在二级缓存的设计:
一级缓存 :默认开启,缓存范围为sqlSession这个级别。也就是说,在一次sqlSession的处理的过程中,对同一个数据进行反复读取时,就会利用到这个缓存来提高我们程序的处理速度。
二级缓存 :要手动开始,缓存范围为mapper映射器的namespace命名空间内
下面用一个图来表述缓存的存储范围:
二级缓存的运行规则
二级缓存开启后,默认所有的查询操作均使用缓存。
写操作commit提交时,对该namespace命名空间下所有的缓存进行强制清空。
这么做是为了保证数据的一致性。假设第一个用户得到了一个商品的数据为婴幼儿奶粉,第二个用户把婴幼儿奶粉这个商品的名称改成了其他什么什么奶粉的名称。如果在进行写操作后不进行清空缓存,那么第一个用户再次读取那个婴幼儿奶粉的时候,读取到的数据还是婴幼儿奶粉,而不是更改后的奶粉。这样,我们得到的数据就和数据库存储的数据就不一致了。所以在用户写操作以后都会对该namespace命名空间下所有的缓存进行强制清空。
通过配置useCache=false可以不用缓存。
配置flushCache=true代表强制清空缓存。
案例演示
下面通过案例来体验:
1.我们先来体验一下一级缓存,以之前编写的selectById为例,在MybatisTest测试类中编写测试方法:
@Test public void testLv1Cache(){ SqlSession sqlSession=null; try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); Goods good2= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); System.out.println(good2.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } }
案例的测试方法里面有两条相同的select语句。运行后,查看日志我们会发现select语句只执行了一次。
还没完,这里只是验证了在同一个SqlSession中的情况,我们把这个方法里面的try-catch在下面复制一份。
@Test public void testLv1Cache(){ SqlSession sqlSession=null; try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); Goods good2= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); System.out.println(good2.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); Goods good2= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); System.out.println(good2.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } }
这里相当于创建了2个SqlSession对象,我们运行一下,查看日志后会发现select语句执行了两次,这也说明了一级缓存范围为sqlSession这个级别:
你认为这样就完了?还没有,我们还要验证一下写操作commit提交时,对该namespace命名空间下所有的缓存进行强制清空。我们在第一个select语句后添加一行代码sqlSession.commit(),对事务进行提交。
@Test public void testLv1Cache(){ SqlSession sqlSession=null; try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); sqlSession.commit(); Goods good2= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); System.out.println(good2.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } }
运行一下,发现缓存的确被清空了,日志显示执行了2个select语句:
2.接下来体验一下二级缓存,同样是以之前编写的selectById为例。
我们回到项目,打开goods.xml,对二级缓存进行配置。
我先写案例,后面再详细地进行解释:
在<mapper>标签内进行如下cache的配置
<mapper namespace="goods"> <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/> ························· </mapper>
然后对上面一级缓存的测试方法进行更改:
@Test public void testLv1Cache(){ SqlSession sqlSession=null; try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } try { sqlSession=MybatisUtils.openSession(); Goods good= sqlSession.selectOne("goods.selectById",1603); System.out.println(good.getTitle()); }catch (Exception e){ e.printStackTrace(); }finally { MybatisUtils.closeSession(sqlSession); } }
这个案例我们创建了两个sqlsession来执行相同的查询,由于我们开启了二级缓存,所以只执行一次select语句。
下面来对二级缓存进行详细解释:
上面的案例中,我们使用<cache eviction=“LRU” flushInterval=“600000” size=“512” readOnly=“true”/>来开启二级缓存。
eviction:是缓存的清除策略,当缓存对象的数量达到上限后,自动触发对应算法对缓存进行清除。
清除策略:LUR 指的是清除最近最久未使用的数据对象。FIFO 先进先出,按对象的进入缓存的顺序来清除他们。SOFT 软引用,基于垃圾回收器状态和软引用规则移除对象。WEAK 弱引用,更积极地基于垃圾收集器状态和弱引用规则移除对象。Mybatis默认的清除策略是 LRU。
flushInterva 刷新间隔,代表相隔多长时间自动清空缓存,单位毫秒。
size当前的二级缓存保存的数据对象的上限,最多缓存多少个对象。默认值是 1024
readOnly 可以设置true或false。当设置为true时,表示返回只读缓存,每次从缓存取出的是缓存对象本身,这种执行效率最高。当设置为false时,代表每次取出的是缓存对象的副本,每一次取出的对象都是不同的,这种安全性比较高。
我们可以对不同的select语句设置是否要使用缓存,只要在select语句中添加一个useCache属性为false。代表查询结果不会放到缓存。
<select id="selectById" parameterType="Integer" resultType="org.haiexijun.entity.Goods" useCache="false"> select * from t_goods where goods_id = #{x} </select>
有时候,在使用完insert等更新操作的语句后,只有当我们提交了事务后,才会清除缓存。如果你需要程序在执行完insert语句后就立马清除缓存而不是等commit后才清除缓存的话,就要在insert标签中设置flushCache属性的值为true。
<insert id="insert" parameterType="org.haiexijun.entity.Goods" useGeneratedKeys="true" keyProperty="goodId" keyColumn="goodId" flushCache="true"> insert into t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery,category_id) values(#{title},#{subTitle},#{originalCost},#{currentPrice},#{discount},#{isFreeDelivery},#{categoryId}) </insert>