MyBatis所有学习案例:https://gitee.com/pikachu2333/mybatis-simple
SLF4j与Logback
早期的Java项目中,基本都是使用的log4j,log4j和logback这两个日志管理实现都是由一个人开发的。英文log4j虽然经过多次更新迭代,仍然有些问题积重难返,所以作者另起炉灶开发了另一款日志管理实现logback,而且logback的性能要好的多。在MyBatis底层可以通过SLF4J支持logback!
案例:
pom.xml 通过maven增加logback依赖
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
在引用外部库中出现了2个底层依赖个slf4j的引用。
此时运行测试用例,即可在控制台打印日志信息。
**自定义日志信息:**在resources目录下新建强制要求名为logback.xml的文件
src/main/resources/logback.xml
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
encoder>
appender>
<root level="debug">
<appender-ref ref="console">appender-ref>
root>
configuration>
在我们使用SQL语句时,有的时候参数是不固定的。比如用户可以指定多个检索条件,也可能单独只指定一个检索条件。在这个时候,我们无法确定条件参数的数量,只能使用动态SQL完成。在实际的开发中,动态SQL的使用非常普遍。
动态SQL是指根据参数数据动态组织SQL的技术,它有些类似于对SQL执行拼接。
动态SQL的应用场景:
可以使用
标签和
组合使用,或是单独使用
标签来实现动态SQL。
goods.xml
<select id="dynamicSQL" parameterType="java.util.Map" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods
<where>
<if test="categoryId != null and categoryId!=''">
and category_id = #{categoryId}
if>
<if test="currentPrice != null and categoryId!=''">
and current_price < #{currentPrice}
if>
where>
select>
测试代码:
@Test
public void testDynamicSQL() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
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.closeSqlSession(session);
}
}
默认一级缓存开启,每一个SqlSession查询是不一样的对象,查询一次就被释放,使用率不高,而二级缓存为每一个SqlSession共享,提高利用率。
@Test
public void testLv1Cache() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
Goods goods1 = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
System.out.println(goods1.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
}
goods和goods1两个对象打印的哈希值地址是一样的,也就是两个对象指向同一块空间,所以已经缓存是属性SqlSession对象的。
继续验证:
负责两个session对象执行块,上面和下面执行的两个结果不一样。
@Test
public void testLv1Cache() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
Goods goods1 = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
System.out.println(goods1.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
Goods goods1 = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
System.out.println(goods1.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
}
但是如果执行commit之后,一级缓存就会被强制清空,下面两个对象的hashCode值不一样:
@Test
public void testLv1Cache() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
session.commit();
Goods goods1 = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
System.out.println(goods1.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
}
goods.xml 在mapper下配置:
<cache eviction="LRU" flushInterval="600000" size="512" readOnly="true">cache>
开启二级缓存后,两个对象地址相同:
@Test
public void testLv2Cache() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
try {
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("goods.selectById", 1603);
System.out.println(goods.hashCode());
} catch (Exception e) {
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
}
二级缓存是把对象存到命名空间的地址上,并不会随着session的开启与关闭销毁。
二级缓存详细配置:
<cache eviction="LRU" flushInterval="600000" size="512" readOnly="true">cache>
在其他的标签中也提供了一些响应的缓存设置:
useCache="false"代表不使用缓存,比如这种查询全部数据压力太大了
<select id="selectAll" resultType="mybatis.entity.Goods" useCache="false">
select * from t_goods order by goods_id desc limit 10
select>
flushCache="true"执行完当前sql之后立即清空缓存,可以用在insert语句,虽然insert,commit后也会清空缓存,但设置后是插入后立即清空缓存;也可以使用在select语句查询完后立即清空缓存,并且该条sql语句执行结果同样不会被放入缓存。
注意和多表关联查询不一样:
商品和详情对象是一对多的关系
t_goods_detail是商品详情表
mybatis/entity/GoodsDetail.java 新建GoodsDetail实体类
package mybatis.entity;
public class GoodsDetail {
private Integer gdId;
private Integer goodsId;
private String gdPicUrl;
private Integer gdOrder;
public Integer getGdId() {
return gdId;
}
public void setGdId(Integer gdId) {
this.gdId = gdId;
}
public Integer getGoodsId() {
return goodsId;
}
public void setGoodsId(Integer goodsId) {
this.goodsId = goodsId;
}
public String getGdPicUrl() {
return gdPicUrl;
}
public void setGdPicUrl(String gdPicUrl) {
this.gdPicUrl = gdPicUrl;
}
public Integer getGdOrder() {
return gdOrder;
}
public void setGdOrder(Integer gdOrder) {
this.gdOrder = gdOrder;
}
}
mappers/goods_detail.xml 在mappers下新建对应的xml文件,namespace改为goodsDetail,resultType指向刚创建的实体类
建号后在文件下
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="goodsDetail">
<select id="selectByGoodsId" parameterType="Integer" resultType="mybatis.entity.GoodsDetail">
select * from t_goods_detail where goods_id = #{value}
select>
mapper>
建好后别忘了在mybatis-config.xml文件下注册:
<mapper resource="mappers/goods_detail.xml"/>
Goods.java 打开Goods实体类,在对象关联时,Goods和GoodsDetail是一对多的关系,Goods是一,在Goods类中增加List
package mybatis.entity;
import java.util.List;
public class Goods {
private Integer goodsId;//商品编号
private String title;//标题
private String subTitle;//子标题
private Float originalCost;//原始价格
private Float currentPrice;//当前价格
private Float discount;//折扣率
private Integer isFreeDelivery;//是否包邮 ,1-包邮 0-不包邮
private Integer categoryId;//分类编号
private List<GoodsDetail> goodsDetails;
public List<GoodsDetail> getGoodsDetails() {
return goodsDetails;
}
public void setGoodsDetails(List<GoodsDetail> goodsDetails) {
this.goodsDetails = goodsDetails;
}
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;
}
}
测试两个对象在结构上就完成了关联,但是数据还没获取,需要在goods.xml也就是一的一方进行说明:
<resultMap id="rmGoods1" type="mybatis.entity.Goods">
<id column="goods_id" property="goodsId">id>
<collection property="goodsDetails" select="goodsDetail.selectByGoodsId"
column="goods_id"/>
resultMap>
<select id="selectOneToMany" resultMap="rmGoods1">
select * from t_goods limit 0,10
select>
测试:
@Test
public void testOneToMany() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
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.closeSqlSession(session);
}
}
只需要在多的一方实体类中增加一的一方的实体:
GoodsDetail.java 增加goods类属性和get,set方法
private Goods goods;
goods_detail.xml 多的一方添加,注意resultMap中用的标签变成了association,引用之前写好的goods.selectById,20条多数据对goods中多对一
<resultMap id="rmGoodsDetail" type="mybatis.entity.GoodsDetail">
<id column="gd_id" property="gdId">id>
<association property="goods" select="goods.selectById" column="goods_id">association>
resultMap>
<select id="selectManyToOne" resultMap="rmGoodsDetail">
select * from t_goods_detail limit 0,20
select>
测试:打印出多个详细信息图片和对应的一个标题
这里有个大坑,因为之前学习删除语句测试时把goods_id=739这条数据删掉了,然后就一致报空指针异常,找了好久仔细检查了所有属性和调用关系也没找到问题,打断点确实就是空(回头想想我测试了前20条数据,而其中的前14条对应的goods_id=739是null,后6条不是空的,可是我看了前几个都是null就没往后看到第15个,也没寻思能一个个点15个对象的属性啊,长记性了。。。,啊啊啊啊~),找了半天灵机一动想到了之前删过的这条数据,费半天劲塞回去还真是这个问题!!!。。。。。。。
@Test
public void testManyToOne() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
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.closeSqlSession(session);
}
}
通过在MyBatis中配置对象的一对多或多对一可以很大程度降低开发的工作量,所有的sql放在xml中,由MyBatis自动管理执行,降低了出错风险。
国人开发的分页插件,官方网站:
https://pagehelper.github.io/
PageHelper
与jsqlparser
pom.xml
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.2.1version>
dependency>
<dependency>
<groupId>com.github.jsqlparsergroupId>
<artifactId>jsqlparserartifactId>
<version>4.1version>
mybatis-config.xml setting标签后面增加
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
<property name="reasonable" value="true"/>
plugin>
plugins>
测试:
@Test
/**
* PageHelper分页查询
*/
public void testSelectPage() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
//startPage 自动将下一次查询进行分页
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.closeSqlSession(session);
}
}
select * from table limit 10,20;
rownum是伪列,无需声明可以直接使用的隐藏列;最内侧是核心查询语句,外面两层基本是固定的。12到20条数据
select t3.*from(
select t2.*,rownum as row_num from(
select * from table order by id asc
)t2 where rownum<=20
) t3
where t2.row_num>11
SQL Server 2000 16到18条数据
select top 3 * from table
where
id not in
(select top 15 id from table)
SQL Server 2012+ 第4行后5条,5,6,7,8,9条数据
select * from table order by id
offset 4 row fetch next 5 rows only
pom.xml 引入c3p0
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.5.2version>
dependency>
mybaits/datasource/C3P0DataSourceFactory.java 创建数据源工厂类
package mybatis.datasource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
/**
* C3P0与MyBatis兼容使用的数据源工厂类
*/
public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
public C3P0DataSourceFactory() {
this.dataSource = new ComboPooledDataSource();
}
}
mybatis-config.xml 将连接池类型修改为自己创建的数据源,并修改配置属性
<dataSource type="mybatis.datasource.C3P0DataSourceFactory">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/babytun?serverTimezone=UTC&characterEncoding=UTF-8"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
<property name="initialPoolSize" value="5"/>
<property name="maxPoolSize" value="20"/>
<property name="minPoolSize" value="5"/>
dataSource>
利用集合保持批处理数据,在利用批处理SQL一次性完成
goods.xml
<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
-- collection="list"代表迭代的数据源从哪来,一般情况下书写list,指代从外侧传来的List集合,这个名字是mybatis强制要求不能随意修改
-- item="item" 循环中的迭代遍历
-- indx="index" 循环的索引,当前是第几次循环
-- separator="," 分割器,生成文本时(("a","a1","a2"),("b","b1","b2"),...),每个记录用逗号分割
-- 批量删除中传入的list中包含的是每一个要删除的数据的编号,foreach标签中要加入open="(" close=")"
<foreach collection="list" item="item" index="index" separator=",">
(#{item.title},#{item.subTitle},#{item.originalCost},#{item.currentPrice},#{item.discount},#{item.isFreeDelivery},#{item.categoryId})
foreach>
insert>
测试:使用批处理方式插入10000条数据,用时4164毫秒
@Test
public void testBatchInsert() throws Exception {
SqlSession session = null;
try {
long st = new Date().getTime();
session = MyBatisUtils.openSqlSession();
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.closeSqlSession(session);
}
}
对比测试:直接插入10000条数据,同时9798毫秒,可以看到批处理方式快很多
@Test
public void testInsert1() throws Exception {
SqlSession session = null;
try{
long st = new Date().getTime();
session = MyBatisUtils.openSqlSession();
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()方法返回值代表本次成功插入的记录总数
session.insert("goods.insert" , goods);
}
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.closeSqlSession(session);
}
}
批量插入数据的局限,需要通过压力测试来调整
goods.xml
<delete id="batchDelete" parameterType="java.util.List">
DELETE FROM t_goods WHERE goods_id in
<foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
#{item}
foreach>
delete>
测试:
@Test
public void testBatchDelete() throws Exception {
SqlSession session = null;
try {
long st = new Date().getTime();
session = MyBatisUtils.openSqlSession();
List list = new ArrayList();
list.add(1920);
list.add(1921);
list.add(1922);
session.delete("goods.batchDelete", 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.closeSqlSession(session);
}
}
对于Mybatis而言,它提供了一种使用注解的开发方式,这种开发方式与使用xml进行开发比较,它更适合使用在一些小型敏捷的项目中
MyBatis常用注解
在之前的项目基础上新建一个项目:(pom.xml中依赖不变,工具类不变,logback自定义文件不变)
mybatis-config.xml中不在需要mapper对xml文件的引用,而是该成我们对接口的引用
<mappers>
<package name="com.dodoke.mybatisannotation.dao"/>
mappers>
在mybaits包下新建dao包,此包中创建一系列接口:
GoodsDAO.java
package mybatis.dao;
import mybatis.dto.GoodsDTO;
import mybatis.entity.Goods;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
/**
该接口用操作数据库
*/
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);
/**
对于注解开发来说,新增和删除,以及修改方法的返回值都要是int类型
@param goods
@return
*/
@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(statement = "select last_insert_id()" ,before=false,keyProperty = "goodsId" ,resultType = Integer.class)
public int insert(Goods goods);
//增加结果映射
@Select(" select * from t_goods order by goods_id desc limit 10")
//如果没有设置驼峰命令转换或者要设置数据转换类,或者多对一,一对多的时候,可以利用results注解
@Results({
//设置id=true,明确指示id属性
@Result(column = "goods_id",property = "goodsId" , id=true),
@Result(column = "title",property = "title" ),
@Result(column = "sub_title",property = "subTitle" ),
})
public List<GoodsDTO> selectLimit();
}
测试:利用GoodsDAO对象实现;查询测试,插入删除,修改都类似
@Test
public void testSelectByPriceRange() throws Exception {
SqlSession session = null;
try {
session = MyBatisUtils.openSqlSession();
GoodsDao goodsDao = session.getMapper(GoodsDao.class);
List<Goods> list = goodsDao.selectByPriceRange(100f, 500f, 20);
System.out.println(list.size());
} catch (Exception e) {
if (session != null) {
session.rollback();//回滚事务
}
throw e;
} finally {
MyBatisUtils.closeSqlSession(session);
}
}