先看看原生操作JDBC的步骤
*注册驱动,获取连接
*创建StateMent对象
*execute()方法执行SQL
*把结果集转换成POJO
*关闭资源
一看,存在大量的重复代码,繁琐过程,结果集的处理很复杂,数据库连接的管理也很麻烦。
所以,慢慢就出现了一些包装数据库操作的框架,springJDBC、dbUtils、heibernate、mybatis等。
先说说比较早的springJDBC和dbUtil,它们主要解决了数据源的封装和映射结果集的包装。
但是并没有真正解决SQL语句的硬编码(直接把sql写在代码中,业务操作和数据库操作耦合在一期);参数只能按顺序传入(占位符),不能就通过一个对象传进去来操作;没有实现实体类到数据库记录的映射;没有提供缓存等功能。
为了解决这些问题,真正的ORM级的框架就出现了。
Hibernate(全自动化,jpa是对hibernate的封装):
hbm.xml或者实体类注解(class对应表,字段对应列)
提供session对象来直接操作对象,比如session.save(bo)
问题:
1、不能指定部分字段(不能像sql样只查部分)
2、无法自定义SQL,优化困难
3、不支持动态SQL
MyBatis(半自动化):
1、使用连接池对连接进行管理
2、SQL和代码分离,集中管理
3、参数映射和动态和动态SQL
4、结果集映射
5、缓存管理
6、重复SQL的提取,sql的标签,可以在其它地方来引用
7、插件机制
实际开发建议:
*业务简单的项目可以使用Hibernate
*需要灵活的SQL,可以使用,MyBatis
*对性能要求高,可以使用JDBC
*Spring JDBC可以和ORM框架混用
去除Spring容器,我们就用java和Mybatis来耦合,看看纯MyBatis是怎么编程的。
1、mybatis和MySQL.jar包依赖
2、全局配置文件mybatis-config.xml
3、映射器Mapper.xml
4、Mapper接口
/**
* 使用MyBatis API方式
* @throws IOException
*/
@Test
public void testStatement() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog) session.selectOne("com.darker.mapper.BlogMapper.selectBlogById", 1);
System.out.println(blog);
} finally {
session.close();
}
}
/**
* 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
* @throws IOException
*/
@Test
public void testSelect() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
SqlSessionFactoryBuiler 创建工厂类(产生了工厂类对象就可以丢弃,方法内部)
SqlSessionFactory 创建会话(需要一直存在,不停的创建会话,应该是单例一直存在的)
SqlSession 会话(一个请求的方法里面存在)
Mapper映射(单个方法)
首先看到我们的xml,发现有个root的标签configuration,既然有这个根标签,马上就能想到MyBatis中是不是会有个Configruation配置类。
发现了,果然有这个配置类,所以一直说配置源于代码,比如spring的各种配置一样也能在源码中找到(好多小伙伴一直感觉各个版本的配置标签不统一,可能会有更改,不好查询,其实直接看源码就可以),下面我们就来好好分析下这些标签。
*properties,把一些需要重复引用的属性放到这个标签中, 比如数据库连接得配置。
*settings,mybatis核心行为得控制。
*typeAliases别名配置,简化类型全路径得拼写(一般会换成包扫描)
*typeHandler,类型处理器,为什么mybatis可以把数据库得varchar类型对应到我们java的string类型
它也支持自定义
public class MyTypeHandler extends BaseTypeHandler {
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
// 设置 String 类型的参数的时候调用,Java类型到JDBC类型
// 注意只有在字段上添加typeHandler属性才会生效
// insertBlog name字段
System.out.println("---------------setNonNullParameter1:"+parameter);
ps.setString(i, parameter);
}
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
// 注意只有在字段上添加typeHandler属性才会生效
System.out.println("---------------getNullableResult1:"+columnName);
return rs.getString(columnName);
}
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// 根据下标获取 String 类型的参数的时候调用
System.out.println("---------------getNullableResult2:"+columnIndex);
return rs.getString(columnIndex);
}
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("---------------getNullableResult3:");
return cs.getString(columnIndex);
}
}
*objectFactory,对象工厂,通过反射创建java对象。
*plugin,允许你在映射语句执行过程中的某一点进行拦截调用。
*environments,配置环境以及里面的熟悉,数据源,事务等。
*mappers,映射器,定义 SQL 映射语句。
if标签的使用
choose(when,otherwise)标签的使用
trim(where,set)标签的使用
insert into tbl_emp
emp_id,
emp_name,
gender,
email,
d_id,
#{empId,jdbcType=INTEGER},
#{empName,jdbcType=VARCHAR},
#{gender,jdbcType=CHAR},
#{email,jdbcType=VARCHAR},
#{dId,jdbcType=INTEGER},
foeach 标签的使用,批量操作比java层面节省性能
delete from tbl_emp where emp_id in
#{item.empId,jdbcType=VARCHAR}
上面动态标签foreach可以批量操作,但如果出现要批量删除或者新增非常大量的数据,就会导致这个sql可能会非常长,而MyBatis对这种sql的大小是有限制的,默认是4m,那么这种情况应该怎么解决呢。
这里就需要用到MyBatis里面提供的批量操作执行器,在xml中的
simple:默认,对应Statement。
reuse:可重用的执行器,就是会把sql缓存起来,第二次用就会再去拿到,对应PrepareStatement。
batch:可重用,且还会执行批量操作,依然是对应PrepareStatement。
在JDBC代码层面对应
/**
* 原生JDBC的批量操作方式 ps.addBatch()
* @throws IOException
*/
@Test
public void testJdbcBatch() throws IOException {
Connection conn = null;
PreparedStatement ps = null;
try {
// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true", "root", "123456");
ps = conn.prepareStatement(
"INSERT into blog values (?, ?, ?)");
for (int i = 1000; i < 101000; i++) {
Blog blog = new Blog();
ps.setInt(1, i);
ps.setString(2, String.valueOf(i));
ps.setInt(3, 1001);
ps.addBatch();
}
ps.executeBatch();
// conn.commit();
ps.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (ps != null) ps.close();
} catch (SQLException se2) {
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
记得用Hibernate的时候,可能一个实体类中会用到其它实体类的属性,那么一般会在那个实体类中加字段或者集合来映射,但这样做却等于污染了实体类。
那如果我们不想污染实体类,MyBatis是怎么操作的呢,举个例子,首先有两个实体类。
public class Author implements Serializable {
Integer authorId; // 作者ID
String authorName; // 作者名称
public Integer getAuthorId() {
return authorId;
}
public void setAuthorId(Integer authorId) {
this.authorId = authorId;
}
public String getAuthorName() {
return authorName;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
}
public class Blog implements Serializable{
Integer bid; // 文章ID
String name; // 文章标题
Integer authorId; // 文章作者ID
public Integer getBid() {
return bid;
}
public void setBid(Integer bid) {
this.bid = bid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAuthorId() {
return authorId;
}
public void setAuthorId(Integer authorId) {
this.authorId = authorId;
}
@Override
public String toString() {
return "Blog{" +
"bid=" + bid +
", name='" + name + '\'' +
", authorId='" + authorId + '\'' +
'}';
}
}
第一种方式:嵌套结果
先建一个包装对象,然后把结果返回这个包装对象中,里面包含了作者实体类,且在resultMap中用association定义了作者实体类的全部属性,这样就等于直接把结果放到了resultMap中。
public class BlogAndAuthor implements Serializable {
Integer bid; // 文章ID
String name; // 文章标题
Author author; // 作者
public Integer getBid() {
return bid;
}
public void setBid(Integer bid) {
this.bid = bid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
@Override
public String toString() {
return "BlogAndAuthor{" +
"bid=" + bid +
", name='" + name + '\'' +
", author=" + author +
'}';
}
}
第二种方式:嵌套查询
依然使用上面的包装类,但是resultMap中不直接定义全部的属性,而是用association定义一个select的查询器,也就是说当用到这个查询的时候,会去触发这个select方法。
在这种方式里面就存在一个N+1的问题,举个例子,当我们查询这个列表的时候,可能我只是需要显示博客的内容,并不需要一开始就显示作者,但是它依然会把每个selectAuthor方法走一遍,这样就等于每次查询都要查两个SQL,显然是不利于效率的,如果不调用这个属性的话,我们并不希望它多查一次SQL。
解决办法当然也是有的,那就是开启延迟加载,上面早前说到的全局配置中settings,里面有个懒加载开关。
这里用代码调用来说明下区别:
当我关闭了延迟加载的时候,使用这个方法
/**
* 一对一,一篇文章对应一个作者
* 嵌套查询,会有N+1的问题
*/
@Test
public void testSelectBlogWithAuthorQuery() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
BlogAndAuthor blog = mapper.selectBlogWithAuthorQuery(1);
System.out.println("-----------:"+blog.getClass());
// 如果开启了延迟加载,会在使用的时候才发出SQL
// equals,clone,hashCode,toString也会触发延迟加载
// System.out.println("-----------调用toString方法:"+blog);
//System.out.println("-----------getAuthor:"+blog.getAuthor().toString());
// 如果 aggressiveLazyLoading = true ,也会触发加载,否则不会
//System.out.println("-----------getName:"+blog.getName());
}
得出的答案你会发现,我并没有调用这个实体类中的作者,但它依然执行了selectAuthor这套SQL语句。
当我开启延迟加载的时候,使用这个方法 ,你会发现它只执行了一条sql。
当我再次在方法中调用这个对象getAuthor方法的时候,它才会去执行下一条SQL,也就是说只要用到的时候才去查询。
当你开了全局延迟加载以后,你在某个地方又不想要延迟加载怎么办呢?
你可以在你想要的方法上加上aggressiveLazyLoading属性,默认关闭,可以用过select标签中的fetchtype来覆盖。
延迟加载的原理
显然能增强类的功能,使得每次只有getAuthor方法的时候才去调用SQL,那么原理只能是它改造了我们的getAuthor方法,能做到这个的只能是代理了。
那么通过debug你可以看到,你的对象的class其实变了。
变成了一个javassist代理对象。
Mybatis可以支持两种代理方式,一种是默认的java方法,一种是cglib方式,可以在配置种设置。
逻辑分页(假分页)和物理分页(真分页,使用数据库支持的语法)
逻辑分页(假分页):
Mybatis提供对象RowBounds
/**
* 逻辑分页
* @throws IOException
*/
@Test
public void testSelectByRowBounds() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
int start = 0; // offset
int pageSize = 5; // limit
RowBounds rb = new RowBounds(start, pageSize);
List list = mapper.selectBlogList(rb); // 使用逻辑分页
for(Blog b :list){
System.out.println(b);
}
} finally {
session.close();
}
}
按数据库和sql来说,必然式应该查出6条,但我传了个 RowBounds对象后,却完成了逻辑分页,那么它是怎么做到的呢,它会先把数据全部放到内存中,然后定位到偏移量offset,也就是起始值,把之前的全部去掉,然后在定位到limit也就是结尾值,然后把这部分取出来,所以,这样是非常浪费性能的。
物理分页(真分页)
最原始的方法,java层面计算分页点传入
插件形式,自动帮我们生成物理分页数据,pagehelper
MaBatis-generator,自动生成映射bo,已经一些通用的curd
问题:字段表发生了变化怎么办?
思路:
(1)、继承生成的mapper文件(好维护但多了两个文件,一个继承的接口文件和一个XML)
/**
*
* 扩展类继承了MBG生成的接口和Statement
*
*/
public interface BlogMapperExt extends BlogMapper {
/**
* 根据名称查询文章
* @param name
* @return
*/
public Blog selectBlogByName(String name);
}
(2)通用Mapper(tk mybatis,mybatisplus)
通用curd
条件构造
代码生成
翻页
批量操作
ps:
附上参考网站MyBatis中文网
MyBatis中文网