Java之MyBatis
MyBatis中文文档:https://mybatis.org/mybatis-3/zh/index.html
MyBatis是一款优秀的持久化框架,它支持定制化SQL、存储过程以及高级映射。
MyBatis避免了几乎所有的JDBC代码和手动配置参数以及结果集。
MyBatis可以使用简单的XML或注解来配置和映射原生类型、接口和Java的POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录。
MyBatis是java中的一个ORM框架。
MyBatis是优秀的持久层框架。所谓持久,就是将内存中的数据保存在数据库中,以便数据的丢失。
MyBatis使用XML将SQL与程序解耦,便于维护。
MyBatis学习简单,执行高效,是JDBC的延伸。
1.基本使用
MyBatis使用xml(mybatis-config.xml)的形式保存配置信息。
MyBatis环境配置标签<environment>。
environment包含数据库驱动、URL、用户名和密码。
(1)创建项目,引入依赖
推荐使用maven来构建项目。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ikidana</groupId> <artifactId>mybatis</artifactId> <version>1.0-SNAPSHOT</version> <!--使用阿里云包源,提高下载速度--> <repositories> <repository> <!--创建私服的地址--> <id>aliyun</id> <name>aliyun</name> <url>https://maven.aliyun.com/repository/public</url> </repository> </repositories> <dependencies> <!--引入mybatis框架--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!--底层使用mysql驱动,所以需要使用mysql-JDBC驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!--单元测试组件--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> </project>
(2)在resource下面,创建核心配置文件mybatis-config.xml
这个文件的名称是固定,这样命名才可以自动加载,否则需要修改配置。
<?xml version="1.0" encoding="UTF-8" ?> <!--XML申明,每一个XML文件都需要--> <!--MyBatis DTD文档约束--> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--在MyBatis中可以配置多套环境,然后通过default来控制采用哪套环境,让配置变得灵活--> <environments default="dev"> <!--配置测试环境,不同的环境不同的id名字--> <environment id="dev"> <!--采取JDBC方式对数据库事务进行commit/rollback--> <transactionManager type="JDBC"/> <!--采用连接池方式管理数据库连接--> <dataSource type="POOLED"> <!--数据库驱动--> <property name="driver" value="com.mysql.jdbc.Driver"/> <!--IP、端口、库、字符集--> <!--需要注意的是&在XML中是有意义的,需要使用amp;进行转义--> <property name="url" value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&characterEncoding=utf8"/> <!--用户名和密码--> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> <environment id="prod"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://148.70.251.110:3306/babytun?useUnicode=yes&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="931548241"/> </dataSource> </environment> </environments> </configuration>
(3)引入测试类
package com.ikidana.mybatis; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; import java.io.Reader; import java.sql.Connection; public class MyBatisTestor { @Test public void testSqlSessionFactory() throws IOException { //getResource读取配置文件 AsReader按照字符流的方式进行读取 //getResourceAsReader返回Reader,Reader包含XML的文本信息 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); //利用构造者模式来初始化SqlSessionFactory对象,同时解析mybatis-config.xml配置文件 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); //测试SqlSessionFactory是否初始化成功,并不意味着已经连接数据库 //System.out.println("加载成功"); SqlSession sqlSession = null; //创建SqlSession对象,SqlSession是JDBC的扩展类,用于与数据库进行交互 try { sqlSession = sqlSessionFactory.openSession(); //在正常开发时,MyBatis会自动帮我们完成来连接动作,此处是测试使用 Connection connection = sqlSession.getConnection(); System.out.println(connection); //com.mysql.jdbc.JDBC4Connection@370736d9,连接已创建 } catch (Exception e) { e.printStackTrace(); } finally { if (sqlSession != null) { //如果type="POOLED",代表使用连接池,close则是将连接回收到连接池中 //如果type="UNPOOLED",代表直连,close则会调用Connection.close()来关闭连接 sqlSession.close(); } } } }
(4)封装工具类
package com.ikidana.mybatis.utils; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.Reader; /** * MyBatisUtilsgoon工具类,创建全局唯一的SqlSessionFactory对象 * */ public class MyBatisUtils { //利用static(静态)属于类不属于对象,且全局唯一,static属性本身就属于全局唯一 private static SqlSessionFactory sqlSessionFactory = null; //利用静态块在初始化时实例化sqlSessionFactory static { Reader reader = null; try { reader = Resources.getResourceAsReader("mybatis-config.xml"); } catch (IOException e) { e.printStackTrace(); //初始化遇到错误时,将异常抛给调用者 throw new ExceptionInInitializerError(e); } sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); } //定义返回SqlSession对象的方法 public static SqlSession openSession(){ return sqlSessionFactory.openSession(); } //释放SqlSession对象 public static void closeSession(SqlSession session){ if (session != null) { session.close(); } } }
调用示例:
@Test public void testMyBatisUtils() throws Exception { SqlSession sqlSession = null; try { //一句话完成SqlSession的初始化工作 sqlSession = MyBatisUtils.openSession(); //执行数据库操作 //sqlSession.insert() //sqlSession.update() Connection connection = sqlSession.getConnection(); System.out.println(connection); } catch (Exception e) { throw e; } finally { //关闭数据连接 MyBatisUtils.closeSession(sqlSession); } }
2.增删改查
1)MyBatis数据查询
MyBatis数据查询步骤
• 创建实体类(Entity)
• 创建Mapper XML
• 编写<select>SQL标签
• 开启驼峰命名映射
• 新增<mapper>
• SqlSession执行select语句
之前我们我们已经通过SqlSessionFactory来创建SqlSession连接对象,但是我们该如何查找数据了?
(1)创建实体类
实体类对应SQL语句:
create table t_goods ( goods_id int auto_increment comment '商品编号' primary key, title varchar(128) not null comment '商品名称', sub_title varchar(256) null comment '子标题', original_cost float not null comment '原价', current_price float not null comment '折后价', discount float not null comment '折扣(0~1)', is_free_delivery int not null comment '是否包邮', category_id int default 0 not null ) charset = utf8;
实体类:
package com.ikidana.mybatis.entity; public class Goods { private Integer goodsId; private String title; private String subTitle; private Float originalCost; private Float currentPrice; private Float discount; private Integer isFreeDelivery; private Integer categoryId; public Integer getGoodsId() { return goodsId; } public void setGoodsId(Integer goodsId) { this.goodsId = goodsId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSubTitle() { return subTitle; } public void setSubTitle(String subTitle) { this.subTitle = subTitle; } public Float getOriginalCost() { return originalCost; } public void setOriginalCost(Float originalCost) { this.originalCost = originalCost; } public Float getCurrentPrice() { return currentPrice; } public void setCurrentPrice(Float currentPrice) { this.currentPrice = currentPrice; } public Float getDiscount() { return discount; } public void setDiscount(Float discount) { this.discount = discount; } public Integer getIsFreeDelivery() { return isFreeDelivery; } public void setIsFreeDelivery(Integer isFreeDelivery) { this.isFreeDelivery = isFreeDelivery; } public Integer getCategoryId() { return categoryId; } public void setCategoryId(Integer categoryId) { this.categoryId = categoryId; } }
实体类的属性与表字段名一一对应。同时需要创建每个属性的getter和setter方法。
实体类存在的价值在于,以对象的形式对SQL语句的查询结果进行封装,
以便后续可以像操作类一样操作数据。
创建实体类主要是为了将查询结果封装成对象,以便后续操作的遍历。
(2)创建Mapper XML文件
对于Mybatis这个持久化框架来说,本质就是保存SQL语句的地方,方便我们将SQL代码与逻辑代码分开,方便管理。
一般会在resourses目录下面,创建mappers目录,专门管理映射文件。
常规做法是,对于常用实例类都会一一创建mapper文件。
下面创建一条查询t_goods这个表的所有数据的SQL。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="goods"> <!--namespace对应某个命名空间,这样就知道去那个文件查找关联--> <!--resultType:在SQL语句执行完成之后,会将每一个结果包装成指定的对象--> <select id="selectAll" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods order by goods_id desc limit 10 </select> </mapper>
这个是MyBatis的核心文件,在这里会完成SQL查询,并将结果进行封装
(3)在mybatis-config.xml中注册Mapper映射文件
由于这个目录和文件都是我们自定义的,因此需要注册。
<mappers> <mapper resource="mappers/goods.xml"/> </mappers>
(4)在逻辑代码中就可以直接通过id来调用结果
public void testSelectAll() throws Exception{ SqlSession session = null; try { session = MyBatisUtils.openSession(); List<Goods> list = session.selectList("goods.selectAll"); for (Goods g:list) { System.out.println(g.getTitle()); } } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
(5)开启驼峰命名映射
在创建实例类的使用,我们会将字段名(带下划线)original_cost转化为字段名originalCost驼峰语法。
Java并不是不允许带下划线的变量名,更多的是使用习惯的问题。
如果强制将带下划线的字段名,转化为驼峰语法,那么可能会无法获取信息。
因为你在查询的时候,去数据库中查找originalCost根本找不到。
需要在mybatis-config.xml开启驼峰语法,进行自动转换。
<settings> <!--如果遇到下划线,就会将下划线后的第一个字母转换为大写,并删除下划线--> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
设置之后就可以获取全部数据:
2)SQL传参
(1)传单个参数
<select id="selectById" parameterType="Integer" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods where goods_id = #{value} </select>
parameterType是传参的类型,#{value}是SQL语句中固定的写法.
public void testSelectId() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); System.out.println(goods.getCategoryId()); } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
(2)传多个参数
parameterType虽然只允许出现一次,但是我们可以传入一个引用数据类型.
<select id="selectByPriceRange" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods where current_price between #{min} and #{max} order by current_price limit 0,#{limit} </select>
Map属于字典类型,使用键值对来存储数据.
public void testSelectByPriceRange() throws Exception{ SqlSession session = null; try { session = MyBatisUtils.openSession(); Map param = new HashMap(); param.put("min",100); param.put("max",500); param.put("limit",10); List<Goods> list = session.selectList("goods.selectByPriceRange",param); for (Goods g:list){ System.out.println(g.getTitle()); } } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
3)数据插入
在数据插入的时候最好关闭事务的自动提交。
public static SqlSession openSession(){ //默认SqlSession对自动提交事务数据(commit) //设置false代表关闭自动提交,改为手动提交事务 return sqlSessionFactory.openSession(false); }
XML需要添加的代码:
<insert id="insert" parameterType="com.ikidana.mybatis.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}); <!--如果需要获取最后插入数据的ID,需要添加如下代码--> <!--resultType返回值类型,keyProperty返回值绑定属性,order执行顺序,在插入语句之前还是之后--> <selectKey resultType="Integer" keyProperty="goodsId" order="AFTER"> <!--当前连接最后产生的ID号,回填到goodsId属性中--> select last_insert_id() </selectKey> </insert>
测试调用:
public void testInsert() throws Exception{ 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); int num = session.insert("goods.insert",goods); session.commit(); //提交事务数据 System.out.println(goods.getGoodsId()); } catch (Exception e) { if (session != null) { session.rollback(); //如果出现异常,回滚事务 } throw e; } finally { if (session != null) { session.close(); } } }
需要注意的是:
a.手动提交事务
b.如果出现异常,需要回滚数据
4)selectKey与useGeneratedKeys的区别
selectKey与useGeneratedKeys都用于在插入数据之后返回最新的主键值.
(1)selectKey属于insert标签的子标签,必须写在insert标签内
<insert id="insert" parameterType="com.ikidana.mybatis.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="goodsId" order="AFTER"> <!--当前连接最后产生的ID号,回填到goodsId属性中--> select last_insert_id() </selectKey> </insert>
(2)useGeneratedKeys
<!--useGeneratedKeys是否会自动的获取主键,默认为false--> <!--keyProperty代表那个属性对应主键--> <!--keyColumn字段名--> <insert id="insert" parameterType="com.ikidana.mybatis.entity.Goods" useGeneratedKeys="true" keyProperty="goodsId" keyColumn="goods_id"> 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>
二者区别:
selectKey标签需要明确编写获取最新主键的SQL语句.
useGeneratedKeys属性会自动根据驱动生成对应的SQL语句.
应用场景不同:
selectKey适用于所有的关系型数据库.
useGeneratedKeys只支持"自增主键"类型的数据库,使用简单.
selectKey标签是通用方案,适用于所有数据库,但编写麻烦.
5)改
<update id="update" parameterType="com.ikidana.mybatis.entity.Goods"> UPDATE t_goods SET title = #{title}, sub_title = #{subTitle}, original_cost = #{originalCost}, current_price = #{currentPrice}, discount = #{discount}, is_free_delivery = #{isFreeDelivery}, category_id = #{categoryId} where goods_id = #{goodsId} </update>
虽然更新可能需要传入多个参数,但是并不需要像select那样,使用map,因为更新就是以对象传入
测试:
public void testUpdate() throws Exception{ SqlSession session = null; try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",739); goods.setSubTitle("更新测试商品"); int num = session.update("goods.update",goods); session.commit(); //提交 } catch (Exception e) { if (session != null) { session.rollback(); //回滚 } throw e; } finally { MyBatisUtils.closeSession(session); } }
稍微比较麻烦的是,更新之前需要先找出需要被更新的对象,不能一步到位.
6)删除
<delete id="delete" parameterType="Integer"> delete from t_goods where goods_id = #{value} </delete>
测试:
public void testDelete() throws Exception{ SqlSession session = null; try { session = MyBatisUtils.openSession(); int num = session.delete("goods.delete", 739); session.commit(); //提交 } catch (Exception e) { if (session != null) { session.rollback(); //回滚 } throw e; } finally { MyBatisUtils.closeSession(session); } }
7)SQL注入攻击
SQL注入是指攻击者利用SQL漏洞,绕过系统约束,越权获取数据的攻击方式.
MyBatis两种传值方式
${}文本替换,未经任何处理对SQL文本替换.
#{}预编译传值,使用预编译传值可以预防SQL注入.
意思就是使用${}可能会带之SQL注入
<select id="selectByTitle" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods where title = ${title} </select>
正常传参应该是这样:
param.put("title","'测试商品'"); //select * from t_goods where title = 测试商品
如果修改参数:
param.put("title","''or 1=1 or title='测试商品'");
那么就会返回数据库中的所有数据。
因为SQL语句已经变成这样:
select * from t_goods where title = ‘’ or 1 = 1 or title = ‘测试商品’;
这条语句在任何情况下都成立。
但是如果使用#{},就会不存在这个问题。
之所以需要提供${}文本替换这种方式,主要是有时候你希望传入的是SQL语句,而不是字符串。
比如 select * from t_goods ${order}
此时你希望传入的是order by goods_id desc,此时你希望传入SQL子句。
8)MyBatis工作流程
3.MyBatis动态SQL
动态SQL是指根据参数数据动态组织SQL的技术。
<select id="dynamicSQL" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods <where> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="currentPrice != null"> and current_price < #{currentPrice} </if> </where> </select>
动态SQL会根据传入的参数来动态的组建SQL语句。
public void testDynamicSQL() throws Exception{ SqlSession session = null; try { session = MyBatisUtils.openSession(); Map param = new HashMap(); param.put("categoryId", 44); param.put("currentPrice", 500); List<Goods> list = session.selectList("goods.dynamicSQL",param); for (Goods g: list) { System.out.println(g.getTitle() + " : " + g.getCategoryId() + " : " + g.getCurrentPrice()); } } catch (Exception e){ throw e; } finally { MyBatisUtils.closeSession(session); } }
<where>标签是可以组建动态SQL语句的核心。
4.MyBatis二级缓存
一级缓存默认开启,缓存范围SqlSession会话。
二级缓存手动开启,属于范围Mapper Namespace。
二级缓存运行规则:
二级开启后默认所有查询操作均使用缓存。
写操作commit提交时对该namespace缓存强制清空。
配置useCache=false可以使某一条SQL不用缓存。
配置flushCache=true代表执行某一条SQL之后,对该namespace下所有缓存强制清空。
public void testLvCache() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); Goods goods1 = session.selectOne("goods.selectById",1602); System.out.println(goods.hashCode() + " : " +goods1.hashCode()); //867148091 : 867148091 } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } //查看执行过程可以看出,其实只执行了一次SQL语句,由于缓存的缘故 //从内存地址一致,可以知道只查询了一次 try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); Goods goods1 = session.selectOne("goods.selectById",1602); System.out.println(goods.hashCode() + " : " +goods1.hashCode()); //815674463 : 815674463 } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } //这是另外一个SqlSession对象,因此又执行了一次查询 //因此可以看出,一级缓存的生命周期就在一个SqlSession之内 }
下面实例验证commit会清空当前namespace下所有缓存:
public void testLvCache() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); session.commit(); Goods goods1 = session.selectOne("goods.selectById",1602); System.out.println(goods.hashCode() + " : " +goods1.hashCode()); //2051853139 : 815674463 } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
可以看出一级缓存的缓存时间很短,因此MyBatis提供了二级缓存。
public void testLvCache2() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); //733943822 System.out.println(goods.hashCode()); } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } try { session = MyBatisUtils.openSession(); Goods goods = session.selectOne("goods.selectById",1602); //733943822 System.out.println(goods.hashCode()); } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
可以看出,即使是不同的对象也会调用一个namespace下面的缓存。
二级缓存会将对象存储在namespace下面,而不会保存在SqlSession下面,
这样就延长对象的声明周期,降低了数据库的查询频率,这样降低了数据库的压力。
在mapper文件中,添加如下配置可以开启二级缓存:
<mapper namespace="goods"> <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/> <!--省略--> </mapper>
参数解释:
eviction:缓存的清楚策略,当缓存对象梳理达到上限之后,自动触发对应算法对缓存对象清楚。
(1)LRU:最近最少使用的,移除最长时间不被使用的对象。
(2)FIFO:先进先出,按对象进入缓存的顺序来移除它们。
(3)SOFT:软引用,移除基于垃圾回收器和软引用规则的对象。
(4)WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval:代表间隔多长时间自动清除缓存,单位毫秒,60000毫秒=10分钟。利用这个配置可以有效的让我们对内存进行回收。
siez:缓存存储上限,用于保存对象或集合(1个集合算1个对象)的数量上限。size的不能太小,不然命中率很低。
readOnly:设置为true,代表返回只读缓存,每次从缓存中取出的是缓存对象本身,这种执行效率高。
设置为flase,代表每次取出的是缓存对象的“副本”,每一次取出的对象都是不同的,这种安全性较高,因为缓存的值可能被修改。
上面的参数是每个实例类的空间设置,还有一些针对某一条SQL的设置:
(1)useCache是否使用缓存
<select id="selectByTitle" parameterType="java.util.Map" resultType="com.ikidana.mybatis.entity.Goods" useCache="flase"> select * from t_goods where title = #{title} </select>
对于这种返回一个表的所有数据,如果全部存储到内存,可能会非常占用资源,而且命中率不会很高。
(2)flushCache是否立即清空缓存
<insert id="insert" parameterType="com.ikidana.mybatis.entity.Goods" useGeneratedKeys="true" keyProperty="goodsId" keyColumn="goods_id" 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>
虽然commit能够提交事务,然后清除缓存,但是有时候我们想立即清除缓存,比如新插入一条语句。
设置了flushcache之后,这条SQL的查询结果也不会被放入缓存。
5.oneToMang对象关联查询
表级联查询:通过一个对象,获取与其关联的另一个对象,执行多条SQL语句。
多表关联查询:两个表通过主外键在一条SQL语句中完成数据的提取。
数据库中常见的表结构对应关系:
(1)一对多
可以看到一个商品对应多个商品信息图片,那么如何获取一个对象的多个关联信息了。
创建实体类:
public class GoodsDetail { private Integer gdId; private Integer goodsId; private String gdPicUrl; private Integer goOrder; //set和get方法省略 }
一对多查询,所以我们需要给商品实例类添加一个属性,好存储这个多个关联对象:
public class Goods { private Integer goodsId; private String title; private String subTitle; private Float originalCost; private Float currentPrice; private Float discount; private Integer isFreeDelivery; private Integer categoryId; private List<GoodsDetail> goodsDetails; //创建一个集合类 }
创建goods_detail的mapper文件,设置通过goods_id(外键)查询商品信息的语句。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="goodsDetail"> <select id="selectByGoodsId" parameterType="Integer" resultType="com.ikidana.mybatis.entity.GoodsDetail"> select * from t_goods_detail where goods_id = #{value} </select> </mapper>
注册:
<mapper resource="mappers/goods_detail.xml"/>
下面是关键部分,如何关联到一起了?
<resultMap id="rmGoods" type="com.ikidana.mybatis.entity.Goods"> <!--column主键字段 property对应对象中属性名--> <id column="goods_id" property="goodsId"></id> <!--collection集合的意思,描述数据的来源--> <!--property填充到goods对象的那个属性--> <!--select使用那个关联查询语句--> <!--column关联列--> <collection property="goodsDetails" select="goodsDetail.selectByGoodsId" column="goods_Id"/> </resultMap> <!--resultMap结果映射,然后找个这个resultMap--> <select id="selectOneToMany" resultMap="rmGoods"> select * from t_goods limit 0,5 </select>
其实中间涉及到了两条SQL语句,拿到goods_id再去查询对应的商品信息。最后存储到goodsDetails这个集合属性中。
最后调用测试:
public void testOneToMany() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); List<Goods> list = session.selectList("goods.selectOneToMany"); for (Goods goods: list) { System.out.println(goods.getTitle() + goods.getGoodsDetails().size()); } } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
调用这一个selectOneToMany方法,就可以获取到其关联的信息了。
查看执行过程:
[main] 00:52:40.295 DEBUG goods.selectOneToMany - ==> Preparing: select * from t_goods limit 0,5 [main] 00:52:40.328 DEBUG goods.selectOneToMany - ==> Parameters: [main] 00:52:40.475 DEBUG goodsDetail.selectByGoodsId - ====> Preparing: select * from t_goods_detail where goods_id = ? [main] 00:52:40.477 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 740(Integer) [main] 00:52:40.570 DEBUG goodsDetail.selectByGoodsId - <==== Total: 11 [main] 00:52:40.571 DEBUG goodsDetail.selectByGoodsId - ====> Preparing: select * from t_goods_detail where goods_id = ? [main] 00:52:40.571 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 741(Integer) [main] 00:52:40.656 DEBUG goodsDetail.selectByGoodsId - <==== Total: 6 [main] 00:52:40.658 DEBUG goodsDetail.selectByGoodsId - ====> Preparing: select * from t_goods_detail where goods_id = ? [main] 00:52:40.659 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 742(Integer) [main] 00:52:40.759 DEBUG goodsDetail.selectByGoodsId - <==== Total: 22 [main] 00:52:40.760 DEBUG goodsDetail.selectByGoodsId - ====> Preparing: select * from t_goods_detail where goods_id = ? [main] 00:52:40.760 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 743(Integer) [main] 00:52:40.852 DEBUG goodsDetail.selectByGoodsId - <==== Total: 14 [main] 00:52:40.854 DEBUG goodsDetail.selectByGoodsId - ====> Preparing: select * from t_goods_detail where goods_id = ? [main] 00:52:40.855 DEBUG goodsDetail.selectByGoodsId - ====> Parameters: 744(Integer) [main] 00:52:40.945 DEBUG goodsDetail.selectByGoodsId - <==== Total: 12 [main] 00:52:40.946 DEBUG goods.selectOneToMany - <== Total: 5 爱恩幼 孕妇护肤品润养颜睡眠面膜 100g11 斯利安 孕妈专用 洗发水 氨基酸表面活性剂 舒缓头皮 滋养发根 让你的秀发会喝水 品质孕妈6 亲恩 孕妇护肤品 燕窝补水保湿6件套 孕期安全温和 补水保湿套装22 优美孕 补水保湿 黄金果水润嫩肤三件套(中样装 洁面乳50g 水50ml 乳液50ml)14 雅滋美特 孕妇护肤品天然叶酸补水保湿三件化妆品套装12
我们不难发现,获取到goods_id之后,再通过另一条SQL语句获取查询目标信息。
(2)多对一
对象多对一的关联查询与一对多是类似的。
在GoodsDetail实例类中添加属性private Goods goods;
在goods_detail.xml文件中添加selectManyToOne多对一查询方法。
<resultMap id="rmGoodsDetail" type="com.ikidana.mybatis.entity.GoodsDetail"> <id column="gd_id" property="gdId"/> <!--主键--> <association property="goods" select="goods.selectById" column="goods_id"/> <!--通过那个字段来关联--> </resultMap> <select id="selectManyToOne" resultMap="rmGoodsDetail"> select * from t_goods_detail limit 20,21 </select>
测试:
public void testManyToOne() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); List<GoodsDetail> list = session.selectList("goodsDetail.selectManyToOne"); for (GoodsDetail gd:list){ System.out.println(gd.getGdPicUrl() + " : " + gd.getGoods().getTitle()); } } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
5.PageHelper分页
在之前的代码编写中,分页多是根据前端参数参数来计算起始点。
而PageHelper直接在后端逻辑层面帮我们完成了分页的事项。
PageHelper使用流程:
maven引入PageHelper与jsqlparser
mybatis-config.xml增加Plugin配置
代码中使用PageHelper.startPage()自动分页
(1)在pom.xml中引入项目依赖
<!--分页组件--> <!--pagehelper是在原有SQL基础上,进行分析,自动生成分页等语句--> <!--jsqlparser用于对原始SQL的解析工作--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.10</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>2.0</version> </dependency>
(2)在mybatis-config.xml中增加项目配置
<!--配置拦截器--> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!--helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。此处配置使用mysql数据库--> <!--即使不配置这样一项,也是自动分页,因为JDBC已经配置了--> <property name="helperDialect" value="mysql"/> <!--分页合理化--> <property name="reasonable" value="true"/> </plugin> </plugins>
(3)使用PageHelper.startPage()自动分页
增加SQL查询方法:
<select id="selectPage" resultType="com.ikidana.mybatis.entity.Goods"> select * from t_goods where current_price < 1000 </select>
添加Java查询逻辑:
public void testSelectPage() throws Exception { SqlSession session = null; try { session = MyBatisUtils.openSession(); PageHelper.startPage(2,10); Page<Goods> page = (Page) session.selectList("goods.selectPage"); System.out.println("总页数:" + page.getPages()); System.out.println("总记录数:" + page.getTotal()); System.out.println("开始行号:" + page.getStartRow()); System.out.println("结束行号:" + page.getEndRow()); System.out.println("当前页面:" + page.getPageNum()); List<Goods> data = page.getResult(); //当前页数据 for (Goods g: data) { System.out.println(g.getTitle()); } } catch (Exception e) { throw e; } finally { MyBatisUtils.closeSession(session); } }
查看执行过程:
[main] 18:41:24.755 DEBUG goods.selectPage_COUNT - ==> Preparing: SELECT count(0) FROM t_goods WHERE current_price < 1000 [main] 18:41:24.807 DEBUG goods.selectPage_COUNT - ==> Parameters: [main] 18:41:24.889 DEBUG goods.selectPage_COUNT - <== Total: 1 [main] 18:41:24.898 DEBUG goods - Cache Hit Ratio [goods]: 0.0 [main] 18:41:24.898 DEBUG goods.selectPage - ==> Preparing: select * from t_goods where current_price < 1000 LIMIT ?, ? [main] 18:41:24.899 DEBUG goods.selectPage - ==> Parameters: 10(Integer), 10(Integer) [main] 18:41:24.986 DEBUG goods.selectPage - <== Total: 10
其实底层自己帮我们做了分页查询。
6.MyBatis整合C3P0连接池
MyBatis提供了自带的连接池,但是其使用效果并不像C3P0那样优异。下面讲述如何在MyBatis引用C3P0
(1)添加C3P0依赖包
<dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.4</version> </dependency>
(2)添加工厂类
package com.ikidana.mybatis.datasource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
/*
* C3P0与MyBatis兼容使用的数据源工厂类
* */
public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
public C3P0DataSourceFactory(){
//数据源由C3P0负责创建
this.dataSource = new ComboPooledDataSource();
}
}
(3)修改mybatis-config.xml配置文件
<!-- <dataSource type="POOLED">--> <!-- <property name="driver" value="com.mysql.jdbc.Driver"/>--> <!-- <property name="url"--> <!-- value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&characterEncoding=utf8"/>--> <!-- <property name="username" value="root"/>--> <!-- <property name="password" value="123456"/>--> <!-- </dataSource>--> <dataSource type="com.ikidana.mybatis.datasource.C3P0DataSourceFactory"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://148.70.251.10:3306/babytun?useUnicode=yes&characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="123456"/> <property name="initialPoolSize" value="5"/> <!--初始数据库连接数量--> <property name="maxPoolSize" value="20"/> <!--最大连接数量--> <property name="minPoolSize" value="5"/> <!--最小连接数量--> </dataSource>
上面其实修改了datasource的应用地址
随意执行一个SQL语句,查看日志:
[main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/mchange-commons.properties' could not be found. Skipping. [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/mchange-log.properties' could not be found. Skipping. [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier '/c3p0.properties' could not be found. Skipping. [main] 20:19:22.784 DEBUG com.mchange.v2.cfg.MConfig - The configuration file for resource identifier 'hocon:/reference,/application,/c3p0,/' could not be found. Skipping. [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - com.mchange.v2.resourcepool.BasicResourcePool@4135c3b config: [start -> 5; min -> 5; max -> 20; inc -> 3; num_acq_attempts -> 30; acq_attempt_delay -> 1000; check_idle_resources_delay -> 0; max_resource_age -> 0; max_idle_time -> 0; excess_max_idle_time -> 0; destroy_unreturned_resc_time -> 0; expiration_enforcement_delay -> 0; break_on_acquisition_failure -> false; debug_store_checkout_exceptions -> false; force_synchronous_checkins -> false] [main] 20:19:22.786 DEBUG c.m.v.c.i.C3P0PooledConnectionPoolManager - Created new pool for auth, username (masked): 'ro******'. [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - acquire test -- pool size: 0; target_pool_size: 5; desired target? 1 [main] 20:19:22.786 DEBUG c.m.v.resourcepool.BasicResourcePool - awaitAvailable(): [unknown] [main] 20:19:23.711 DEBUG o.a.i.t.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@2a4fb17b [wrapping: com.mysql.jdbc.JDBC4Connection@5c6648b0]] [main] 20:19:23.802 DEBUG goods.selectPage_COUNT - ==> Preparing: SELECT count(0) FROM t_goods WHERE current_price < 1000
7.MyBatis批处理
如果我们需要向数据库中添加一大批数据,一条的进行插入肯定会非常缓慢,所以可以使用一些批处理的技巧。
(1)添加批量删除的SQL语句
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO t_goods(title,sub_title,original_cost,current_price,discount,is_free_delivery,category_id) VALUES <foreach collection="list" item="item" index="index" separator=","> (#{item.title}, #{item.subTitle}, #{item.originalCost}, #{item.currentPrice}, #{item.discount}, #{item.isFreeDelivery}, #{item.categoryId}) </foreach> </insert>
(2)逻辑语句
public void testBatchInsert() throws Exception { SqlSession session = null; try { long st = new Date().getTime(); session = MyBatisUtils.openSession(); List list = new ArrayList(); for (int i = 0; i < 10000; i++) { Goods goods = new Goods(); goods.setTitle("测试商品"); goods.setSubTitle("测试子标题"); goods.setOriginalCost(200f); goods.setCurrentPrice(100f); goods.setDiscount(0.5f); goods.setIsFreeDelivery(1); goods.setCategoryId(43); //insert()方法返回值代表本次成功插入的记录总数 list.add(goods); } session.insert("goods.batchInsert", list); session.commit();//提交事务数据 long et = new Date().getTime(); System.out.println("执行时间:" + (et - st) + "毫秒"); // System.out.println(goods.getGoodsId()); } catch (Exception e) { if (session != null) { session.rollback();//回滚事务 } throw e; } finally { MyBatisUtils.closeSession(session); } }
批量插入数据的局限:
(1)无法获得插入数据的id
(2)批量生成的SQL太长,可能被服务器拒绝
8.MyBatis注解开发
在Java中合理使用注解,可以不需要编写XML文件,从而就可以操作数据。
比如我们现在需要查询某一范围内的数据,流程如下:
(1)编辑接口文件GoodsDAO.java
public interface GoodsDAO { @Select("select * from t_goods where current_price between #{min} and #{max} order by current_price limit 0,#{limt}") public List<Goods> selectByPriceRange(@Param("min") Float min ,@Param("max") Float max ,@Param("limt") Integer limt); }
(2)在mybatis-config.xml中注册
<mappers> <!--以下两种注册方式都有效--> <!--<mapper class="com.imooc.mybatis.dao.GoodsDAO"/>--> <package name="com.imooc.mybatis.dao"/> </mappers>
(3)调用
public void testSelectByPriceRange() throws Exception { SqlSession session = null; try{ session = MyBatisUtils.openSession(); //获取映射器,GoodsDAO是接口 //这里的GoodsDAO虽然是接口,在实际运行的时候,session会根据goodsDAO里面的配置信息来动态生成goodsDAO的实现类 //之后直接用goodsDAO里面的方式来操作数据即可 GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class); List<Goods> list = goodsDAO.selectByPriceRange(100f, 500f, 20); System.out.println(list.size()); }catch (Exception e){ throw e; } finally { MyBatisUtils.closeSession(session); } }
如果需要插入数据,可以这样定义:
@Insert("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> @SelectKey(statement = "select last_insert_id()" , before = false , keyProperty = "goodsId" , resultType = Integer.class) public int insert(Goods goods);
然后这样调用:
public void testInsert() throws Exception { 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); GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class); //insert()方法返回值代表本次成功插入的记录总数 int num = goodsDAO.insert(goods); session.commit();//提交事务数据 System.out.println(goods.getGoodsId()); }catch (Exception e){ if(session != null){ session.rollback();//回滚事务 } throw e; }finally { MyBatisUtils.closeSession(session); } }
那么结果映射该如何实现了?
@Select("select * from t_goods") //<resultMap> @Results({ //<id> @Result(column = "goods_id" ,property = "goodsId" , id = true) , //<result> @Result(column = "title" ,property = "title"), @Result(column = "current_price" ,property = "currentPrice") }) public List<GoodsDTO> selectAll();
调用:
public void testSelectAll() throws Exception { SqlSession session = null; try{ session = MyBatisUtils.openSession(); GoodsDAO goodsDAO = session.getMapper(GoodsDAO.class); List<GoodsDTO> list = goodsDAO.selectAll(); System.out.println(list.size()); }catch (Exception e){ throw e; } finally { MyBatisUtils.closeSession(session); } }