界面层 Controller -> SpringMVC
User interface layer
,表示层,视图层,接受用户数据显示请求结果。使用web页面和用户交互,如jsp、html、servlet。
业务逻辑层 Service -> Spring
Business logic layer
,接收表示层传递的数据,检查数据计算业务逻辑,调用数据访问层获取数据。
数据访问层 Dao1 -> MyBatis
Data access layer
, 持久层。与数据库打交道,实现对数据的增、删、改、查。将数据提供给业务层,将业务层处理的数据保存到数据库。
代码较多,开发效率低,重复代码多
需要关注Connection
Statement
ResultSet
对象的创建和销毁
对查询结果需要手动进行封装
业务代码和数据库操作混合
早期叫做ibatis
,MyBatis - MyBatis SQL Mapper Framework for Java
,是一个SQL映射框架,提供数据库的操作能力,是增强的JDBC。
SQL Mapper - sql映射
可以把数据库表中的一行数据映射为一个 java 对象
Data Access Objects - DAOs
数据访问,对数据库执行增删改查
准备
依赖项
org.mybatis
mybatis
3.5.7
mysql
mysql-connector-java
8.0.26
主配置文件 - mybatis.xml
JDBC配置文件 - jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root
DAO层
以下代码中部分内容为 MyBatis 具体使用内容,将在下文进行详解。
/**
* 持久层 与数据库交互
* */
public interface StudentDao {
Student selectStudentById(@Param("id") int id);
List selectStudentIf(Student student);
List selectStudentWhere(Student student);
List selectStudentForEach(List list);
List selectStudents();
@MapKey("id")
HashMap selectStudentsMap();
int insertStudent(Student student);
int count();
}
定义数据类
public class Student {
private int id;
private String name;
private String email;
private int age;
//constructer getter setter toString ...
}
原始使用
在原始调用中我们需要遵守以下规则:
加载主配置文件(mybaties.xml
)
生成SqlSession
接口工厂类的生产类(SqlSessionFactoryBuilder
)
利用生产类创建工厂类实例(SqlSessionFactory
)
利用工厂类创建操作类实例,与数据库进行交互(SqlSession
)
public static void main(String[] args) throws Exception {
InputStream input = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = factoryBuilder.build(input);
SqlSession sqlSession = factory.openSession();
// TODO JDBC operations
sqlSession.close();
}
为了使用SqlSession
,我们需要创建对应Dao类的配置文件StudentDao.xml
(一般放置在同级目录下),在此处我们先简单创建一个文件并在其中定义一个查询全表的操作。并在主配置文件中注册该Dao配置文件。
一般情况下我们约定命名空间namespace为抽象类的全限定名称,id为接口对应的方法名。而对于返回的类型resultType则必须使用数据类的全限定名称
配置完成后,我们在主函数中可以使用sqlId来调用xml中配置好的方法。sqlId=namespace+"."+id
String sqlId = "me.illtamer.maventest.mybatis.dao.StudentDao" + "." + "selectStudents";
List students = sqlSession.selectList(sqlId);
System.out.println(students);
sqlSession.close();
优化使用
分析创建SqlSession
的创建过程
InputStream
的创建需要捕获异常且进行关闭
SqlSessionFactoryBuilder
仅为创建SqlSessionFactory
实例使用
对于每个SqlSession
,我们都只需一个SqlSessionFactory
来创建
据此,我们可以创建MyBatisUtil
来简化SqlSession
的获取过程
public class MyBatisUtil {
private static SqlSessionFactory factory;
static {
try (InputStream input = Resources.getResourceAsStream("mybatis.xml")) {
factory = new SqlSessionFactoryBuilder().build(input);
} catch (Exception e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession() {
return factory.openSession();
}
}
故主函数可演化成以下形式
public static void main(String[] args) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
String sqlId = "me.illtamer.maventest.mybatis.dao.StudentDao" + "." + "selectStudents";
List students = sqlSession.selectList(sqlId);
System.out.println(students);
}
正常调用
以上两种使用方法虽然可以实现目的,但是有一点很不好,我们在代码中没有看到StudentDao
的作用。
众所周知我们使用接口类就是为了对实现进行管理与解耦合,但目前为止我们并没有看到StudentDao
的作用,基于此我们不免会想到对该接口进行对应实现,例如创建以下实现类
public class StudentDaoImpl implements StudentDao {
@Override
public List selectStudents() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
String sqlId = "me.illtamer.maventest.mybatis.dao.StudentDao.selectStudents";
List students = sqlSession.selectList(sqlId);
sqlSession.close();
return students;
}
}
那么主函数可以演变为以下形式
public static void main(String[] args) {
StudentDao dao = new StudentDaoImpl();
List students = dao.selectStudents();
System.out.println(students);
}
工程调用
上述示例演化到 正常调用 部分似乎看起来已经很完备了,但是分析 StudentDaoImpl 中的代码我们不难可以发现基于我们在开始时指定的命名规则下,sqlId = Dao类的全限定名称 + "." + 调用的方法名称,对于每个执行的方法我们都需要对SqlSession执行关闭操作(确保线程安全性),都需要返回结果。
对于一个两个方法来说似乎没什么问题,但如果该实现类中有20个类似方法呢?重复代码是不是太多了?
基于以上考虑,MyBatis为开发者提供了相应接口来简便开发,我们可以通过 Sqlession#getMapper(Class)
来获取MyBatis基于Proxy生成的实现型代理类。至此我们完全可以将刚才写的StudentDaoImpl
删除,将主函数改写成以下形式。
public static void main(String[] args) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
List students = dao.selectStudents();
System.out.println(students);
}
mybatis.xml配置
在先前的配置文件中已经有涉及一部分,在此将以注解形式进行描述
数据分页
数据分页需要安装 MyBatis 的拓展插件,该功能实现较为简便,以下是具体步骤
引入依赖
com.github.pagehelper
pagehelper
5.3.0
配置 mybatis.xml
调用
public static void main(String[] args) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
PageHelper.startPage(1, 3);
List students = dao.selectStudents();
System.out.println(students);
}
此处调用
PageHelper#startPage(pageNum, pageSize)
,显示以三行为一个的第一页元素
在调用具体方法查询前使用 PageHelper 的静态方法设置分页,根据 log 输出可以看出此处实现为在 sql 语句后增加了 LIMIT ? 限制查询条数。
配置别名
resultType别名
配置mybatis.xml
在配置包扫描后,Dao.xml中的 resultType 可简便成配置包路径下的类名
sql语句别名
配置对应Dao.xml
以上述查找所有 student 表中的数据为例,我们可以将可通用部分设置别名重新利用
SELECT `id`, `name`, `email`, `age` FROM student
配置传参
上述示例都是返回表中的全部元素,如果我们要编写以下一串 sql 语句,那么对应的条件参数该如何传递呢?
SELECT
id
,name
,age
FROM student WHEREid
=XX
基本类型传参
如果我们只需要查询某个 id 的数据,那么使用 #{数据名} / ${数据名} 即可获取方法中传入的参数,例如对于 StudentDao#selectStudentById(int id)
,我们仅需使用 #{id} / ${id} 即可获取传递的参数,构成SQL语句进行查询。
注意此处的 #{id} 内的id默认为形式参数名称,如果需要指定传入参数名称需要使用 @Param注解
/**
* 参数传递用 @Param 指定参数名称
* */
Student selectStudentById(@Param("id") int id);
引用类型传参
一般工程中,我们通常传入一个既定的data数据对象(例如本文中的 Student 类),通过调用对象的getter方法我们可以不改动的获取多个成员变量对结果进行选择。
Student selectStudentById(Student stu);
对于以上方法,我们可以通过stu对象调用成员变量值
事实上 # 与 $ 的不同之处在于 # 修饰的参数值会预编译进入语句,相当于JDBC的 PreparStatement,而 $ 则是类似有字符串拼接,只有实际传递参数值的时候才能生成完整的 SQL 语句。因此在进行一些敏感操作时,我们通常使用 # ,而 # 的效率也较之更高。
条件SQL
在实际开发中, 我们可能会面临各种各样的条件分支问题,此时要求的SQL语句也是多变的,为此MyBatis实现了一种由传入参数条件动态构建SQL语句的功能,即为条件SQL。
IF
以 Student 对象为例,当我们在查找时需要在传入 name
不为空的前提下查找所有age
大于 18 的数据值,那么可以使用 if 约束:
注意此处为什么 WHERE 后要加 1=1
因为 if 约束下如果条件成立,其内部的 SQL 语句才会被拼接,如果出现
name
=null,age
>10 的条件,则语句将会直接变成 WHERE ORage
>#{age} 从而产生错误,因此我们可以添加一个无关紧要的式子来将其转换为 WHERE 1=1 ORage
>#{age} 从而避免异常。
WHERE
where 约束需与 if 约束组合使用,其与直接使用 if 的区别在与 where 约束会自动帮助开发者山区无用语句例如上述中可能出现的多余 OR,故在开发时相较于 if 更常用 where。
FOREACH
我们先前已经介绍过如何以唯一id值查找表中对应数据,但当我们需要查找两个id分别对应的数据时候怎么办?是执行两次selectStudentById
还是重新编写一个参数为id1, id2的方法并新写SQL语句?别担心,foreach 约束可以帮助你!
List selectStudentForEach(List list);
对于一个以统一列值查找数据的操作,我们可以将其封装在一个方法内并以以下语法在配置文件中预构建SQL语句,其中有几个属性值再次需提前介绍下:
collection - 传入集合的属性,默认可选 list / array
item - foreach 中枚举的每一元素的别名
open - 起始位置的字符串
close - 终止位置的字符串
separator - 元素间的分隔符
以上约束执行结果为
由此可见整个 foreach 的作用就是以我们指定的规则与我们传入的参数数量拼接成一个 IN (...) 的SQL语句并附加在主SQL语句中
resultMap
在 select 约束中,我们的返回属性并不只有 resultType 一种,resultMap 也是相当常用的一种,他能根据你制定的命名规则将数据库中的查询结果映射到你指定的数据类中并返回(即使是另一个拥有相同属性的不同数据类!)
要注意的是 resultType 和 resultMap 属性二者不能同时配置,应选取一个
举例说明:这是我们原始的函数定义与其相关配置
List selectStudentsMap();
我们可以通过 resultMap 将查询数据映射到另一个数据类中
[1] Data Access Objects