认识Mybatis
在前面JDBC的学习中,虽然我们能够通过JDBC来连接和操作数据库,但是哪怕只是完成一个SQL语句的执行,都需要编写大量的代码,更不用说如果我还需要进行实体类映射,将数据转换为我们可以直接操作的实体类型,JDBC很方便,但是还不够方便,我们需要一种更加简洁高效的方式来和数据库进行交互。
再次强调:学习厉害的框架或是厉害的技术,并不是为了一定要去使用它,而是它们能够使得我们在不同的开发场景下,合理地使用这些技术,以灵活地应对需要解决的问题。
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
我们依然使用传统的jar依赖方式,从最原始开始讲起,不使用Maven,有关Maven内容我们会在后面统一讲解!全程围绕官方文档讲解!
这一块内容很多很杂,再次强调要多实践!
XML语言概述
在开始介绍Mybatis之前,XML语言发明最初是用于数据的存储和传输,它可以长这样:
<?xml version="1.0" encoding="UTF-8" ?>
<outer>
<name>Kplusone</name>
<desc>写bug</desc>
<inner type="1">
<age>10</age>
<sex>男</sex>
</inner>
</outer>
如果你学习过前端知识,你会发现它和HTML几乎长得一模一样!但是请注意,虽然它们长得差不多,但是他们的意义却不同,HTML主要用于通过编排来展示数据,而XML主要是存放数据,它更像是一个配置文件!当然,浏览器也是可以直接打开XML文件的。
一个XML文件存在以下的格式规范:
- 必须存在一个根节点,将所有的子标签全部包含。
- 可以但不必须包含一个头部声明(主要是可以设定编码格式)
- 所有的标签必须成对出现,可以嵌套但不能交叉嵌套
- 区分大小写
- 标签中可以存在属性,比如上面的
type="1"
就是inner
标签的一个属性,属性的值由单引号或双引号包括。
XML文件也可以使用注释:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 注释内容 -->
通过IDEA我们可以使用Ctrl
+/
来快速添加注释文本(不仅仅适用于XML,还支持很多种类型的文件)
那如果我们的内容中出现了<
或是>
字符,那该怎么办呢?我们就可以使用XML的转义字符来代替:
如果嫌一个一个改太麻烦,也可以使用CD来快速创建不解析区域:
<test>
<name><![CDATA[我看你<><><>是一点都不懂哦>>>]]></name>
</test>
那么,我们现在了解了XML文件的定义,现在该如何去解析一个XML文件呢?比如我们希望将定义好的XML文件读取到Java程序中,这时该怎么做呢?
JDK为我们内置了一个叫做org.w3c
的XML解析库,我们来看看如何使用它来进行XML文件内容解析:
// 创建DocumentBuilderFactory对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder对象
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document d = builder.parse("file:mappers/test.xml");
// 每一个标签都作为一个节点
NodeList nodeList = d.getElementsByTagName("test"); // 可能有很多个名字为test的标签
Node rootNode = nodeList.item(0); // 获取首个
NodeList childNodes = rootNode.getChildNodes(); // 一个节点下可能会有很多个节点,比如根节点下就囊括了所有的节点
//节点可以是一个带有内容的标签(它内部就还有子节点),也可以是一段文本内容
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) //过滤换行符之类的内容,因为它们都被认为是一个文本节点
System.out.println(child.getNodeName() + ":" +child.getFirstChild().getNodeValue());
// 输出节点名称,也就是标签名称,以及标签内部的文本(内部的内容都是子节点,所以要获取内部的节点)
}
} catch (Exception e) {
e.printStackTrace();
}
当然,学习和使用XML只是为了更好地去认识Mybatis的工作原理,以及如何使用XML来作为Mybatis的配置文件,这是在开始之前必须要掌握的内容(使用Java读取XML内容不要求掌握,但是需要知道Mybatis就是通过这种方式来读取配置文件的)
不仅仅是Mybatis,包括后面的Spring等众多框架都会用到XML来作为框架的配置文件!
初次使用Mybatis
那么我们首先来感受一下Mybatis给我们带来的便捷,就从搭建环境开始,中文文档网站:
Mybatis
我们需要导入Mybatis的依赖,同样地放入到项目的根目录下,右键作为依赖即可!(依赖变多之后,我们可以将其放到一个单独的文件夹,不然会很繁杂)
依赖导入完成后,我们就可以编写Mybatis的配置文件了(现在不是在Java代码中配置了,而是通过一个XML文件去配置,这样就使得硬编码的部分大大减少,项目后期打包成Jar运行不方便修复,但是通过配置文件,我们随时都可以去修改,就变得很方便了,同时代码量也大幅度减少,配置文件填写完成后,我们只需要关心项目的业务逻辑而不是如何去读取配置文件)我们按照官方文档给定的提示,在项目根目录下新建名为mybatis-config.xml
的文件,并填写以下内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驱动类(含包名)}"/>
<property name="url" value="${数据库连接URL}"/>
<property name="username" value="${用户名}"/>
<property name="password" value="${密码}"/>
</dataSource>
</environment>
</environments>
</configuration>
我们发现,在最上方还引入了一个叫做DTD(文档类型定义)的东西,它提前帮助我们规定了一些标签,我们就需要使用Mybatis提前帮助我们规定好的标签来进行配置(因为只有这样Mybatis才能正确识别我们配置的内容)
通过进行配置,我们就告诉了Mybatis我们链接数据库的一些信息,包括URL、用户名、密码等,这样Mybatis就知道该链接哪个数据库、使用哪个账号进行登陆了(也可以不使用配置文件,其实,只要是能够用配置文件实现的配置,用代码也一定可以,因为配置文件也是要被读取到Java代码里的)
配置文件完成后,我们需要在Java程序启动时,让Mybatis对配置文件进行读取并得到一个SqlSessionFactory
对象:
public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
//暂时还没有业务
}
}
直接运行即可,虽然没有干什么事情,但是不会出现错误,如果之前的配置文件编写错误,直接运行会产生报错!那么现在我们来看看,SqlSessionFactory
对象是什么东西:
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,我们可以通过SqlSessionFactory
来创建多个新的会话,SqlSession
对象,每个会话就相当于我不同的地方登陆一个账号去访问数据库,你也可以认为这就是之前JDBC中的Statement
对象,会话之间相互隔离,没有任何关联。
而通过SqlSession
就可以完成几乎所有的数据库操作,我们发现这个接口中定义了大量数据库操作的方法,因此,现在我们只需要通过一个对象就能完成数据库交互了,极大简化了之前的流程。
我们来尝试一下直接读取实体类,读取实体类肯定需要一个映射规则,比如类中的哪个字段对应数据库中的哪个字段,在查询语句返回结果后,Mybatis就会自动将对应的结果填入到对象的对应字段上。首先编写实体类,,直接使用Lombok是不是就很方便了:
import lombok.Data;
@Data
public class Student {
int sid; //名称最好和数据库字段名称保持一致,不然可能会映射失败导致查询结果丢失
String name;
String sex;
}
在根目录下重新创建一个mapper文件夹,新建名为TestMapper.xml
的文件作为我们的映射器,并填写以下内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>
其中namespace就是命名空间,每个Mapper都是唯一的,因此需要用一个命名空间来区分,它还可以用来绑定一个接口。我们在里面写入了一个select标签,表示添加一个select操作,同时id作为操作的名称,resultType指定为我们刚刚定义的实体类,表示将数据库结果映射为Student
类,然后就在标签中写入我们的查询语句即可。
编写好后,我们在配置文件mybatis-config中添加这个Mapper映射器:
<mappers>
<mapper url="file:mappers/TestMapper.xml"/>
<!-- 这里用的是url,也可以使用其他类型,我们会在后面讲解 -->
</mappers>
最后在程序中使用我们定义好的Mapper即可:
public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}
我们会发现,Mybatis非常智能,我们只需要告诉一个映射关系,就能够直接将查询结果转化为一个实体类!
配置Mybatis
在了解了Mybatis为我们带来的便捷之后,现在我们就可以正式地去学习使用Mybatis了!
由于SqlSessionFactory
一般只需要创建一次,因此我们可以创建一个工具类来集中创建SqlSession
,这样会更加方便一些:
public class MybatisUtil {
//在类加载时就进行创建
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取一个新的会话
* @param autoCommit 是否开启自动提交(跟JDBC是一样的,如果不自动提交,则会变成事务操作)
* @return SqlSession对象
*/
public static SqlSession getSession(boolean autoCommit){
return sqlSessionFactory.openSession(autoCommit);//SqlSession就相当于JDBC的statement用来执行SQL语句的。
}
}
现在我们只需要在main方法中这样写即可查询结果了:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}
之前我们演示了,如何创建一个映射器来将结果快速转换为实体类,但是这样可能还是不够方便,我们每次都需要去找映射器对应操作的名称(select标签的id),而且还要知道对应的返回类型,再通过SqlSession
来执行对应的方法(selectList),能不能再方便一点呢?
现在,我们可以通过namespace
来把mapper.xml文件绑定到一个接口上,利用接口的特性,我们可以直接指明方法的行为,而实际实现则是由Mybatis来完成。
//mapper.xml文件就相当是它的实现类
public interface TestMapper {
List<Student> selectStudent();
}
将Mapper文件的命名空间修改为我们的接口(全类名),建议同时将其放到同名包中,作为内部资源:
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>
作为内部资源后,我们需要修改一下配置文件mybatis-config中的mapper定义,不使用url而是resource表示是Jar内部的文件:
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>
现在我们就可以直接通过SqlSession
获取对应的实现类,通过接口中定义的行为来直接获取结果:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);//获取到接口的实现类
List<Student> student = testMapper.selectStudent();
student.forEach(System.out::println);
}
}
那么肯定有人好奇,TestMapper明明是一个我们自己定义接口啊,Mybatis也不可能提前帮我们写了实现类啊,那这接口怎么就出现了一个实现类呢?我们可以通过调用getClass()
方法来看看实现类是个什么:
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
System.out.println(testMapper.getClass());
我们发现,实现类名称很奇怪,名称为com.sun.proxy.$Proxy4
,它是通过动态代理生成的,相当于动态生成了一个实现类,而不是预先定义好的,有关Mybatis这一部分的原理,我们放在最后一节进行讲解。
接下来,我们再来看配置文件,之前我们并没有对配置文件进行一个详细的介绍:
<configuration>
<environments default="development">//设置默认环境,可以按照需求切换
<environment id="development">//表示这是开发环境,如果要上线就是生产环境
<transactionManager type="JDBC"/>//使用JDBC规范(java.sql)
<dataSource type="POOLED">//数据库连接池
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>//mysql驱动包里的针对JDBC规范做的实现类,项目启动要去加载这个类
<property name="url" value="jdbc:mysql://localhost:3306/study"/>//数据库软件运行的地址
<property name="username" value="test"/>//用户
<property name="password" value="123456"/>//密码
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>//必须将mapper文件写到配置文件里
</mappers>
</configuration>
首先就从environments
标签说起,一般情况下,我们在开发中,都需要指定一个数据库的配置信息,包含连接URL、用户、密码等信息,而environment
就是用于进行这些配置的!实际情况下可能会不止有一个数据库连接信息,比如开发过程中我们一般会使用本地的数据库,而如果需要将项目上传到服务器或是防止其他人的电脑上运行时,我们可能就需要配置另一个数据库的信息,因此,我们可以提前定义好所有的数据库信息,该什么时候用什么即可!
在environments
标签上有一个default属性,来指定默认的环境,当然如果我们希望使用其他环境,可以修改这个默认环境,也可以在创建工厂时选择环境:
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"), "环境ID");//虽然可以在代码里换数据库,但是推荐在配置文件里
我们还可以给类型起一个别名,以简化Mapper的编写:
<!-- 需要在environments的上方 -->//规范大于配置
<typeAliases>
<typeAlias type="com.test.entity.Student" alias="Student"/>
</typeAliases>
现在Mapper就可以直接使用别名了:
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="Student">
select * from student
</select>
</mapper>
如果这样还是很麻烦,我们也可以直接让Mybatis去扫描一个包,并将包下的所有类自动起别名(别名为首字母小写的类名)
<typeAliases>
<package name="com.test.entity"/>
</typeAliases>
也可以为指定实体类添加一个注解,来指定别名(凡是可以写配置文件的,都可以换成代码的方法,要不就设置配置类的属性,要不就是注解的方式):
@Data
@Alias("lbwnb")
public class Student {
private int sid;
private String name;
private String sex;
}
当然,Mybatis也包含许多的基础配置,通过使用:
<settings>
<setting name="" value=""/>
</settings>
所有的配置项可以在中文文档处查询(阅读官方文档是学习技术的最好的方式),本文不会进行详细介绍,在后面我们会提出一些比较重要的配置项。
有关配置文件的介绍就暂时到这里为止,我们讨论的重心应该是Mybatis的应用,而不是配置文件,所以省略了一部分内容的讲解。
增删改查
在了解了Mybatis的一些基本配置之后,我们就可以正式来使用Mybatis来进行数据库操作了!
在前面我们演示了如何快速进行查询,我们只需要编写一个对应的映射器既可以了:
<mapper namespace="com.test.mapper.TestMapper">
<select id="studentList" resultType="Student">
select * from student
</select>
</mapper>
当然,如果你不喜欢使用实体类,那么这些属性还可以被映射到一个Map上:
<select id="selectStudent" resultType="Map">
select * from student
</select>
public interface TestMapper {
List<Map> selectStudent();
}
Map中就会以键值对的形式来存放这些结果了。
list[0]=map,map={sid=0,sex=男,name=bob......(键值对)}//大概格式就是这样
通过设定一个resultType
属性,让Mybatis知道查询结果需要映射为哪个实体类,要求字段名称保持一致。那么如果我们不希望按照这样的规则来映射呢?我们可以自定义resultMap
来设定映射规则:
写在mapper文件里。
<resultMap id="Test" type="Student">
<result column="sid" property="sid"/>
<result column="sex" property="name"/>
<result column="name" property="sex"/>
</resultMap>
通过指定映射规则,我们现在名称和性别一栏就发生了交换,因为我们将其映射字段进行了交换。
如果一个类中存在多个构造方法,那么很有可能会出现这样的错误:
### Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
### The error may exist in com/test/mapper/TestMapper.xml
### The error may involve com.test.mapper.TestMapper.getStudentBySid
### The error occurred while handling results
### SQL: select * from student where sid = ?
### Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
...
这时就需要使用constructor
标签来指定构造方法:
<resultMap id="test" type="Student">
<constructor>
<arg column="sid" javaType="Integer"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap>
值得注意的是,指定构造方法后,若此字段被填入了构造方法作为参数,将不会通过反射给字段单独赋值,而构造方法中没有传入的字段,依然会被反射赋值,有关resultMap
的内容,后面还会继续讲解(先学会最小必要要知识,不要陷入细节的泥潭,因为作为一个技术,他的体系一定是完备的,但是不代表你要一口气学完,边工作边学习才是正确打开方式)。
如果数据库中存在一个带下划线的字段,我们可以通过设置让其映射为以驼峰命名的字段,比如my_test
映射为myTest
(因为java类的属性命名规范就是驼峰式命名,还是变相的让数据库字段和属性名字保持一致)
在mybatis-config里配置
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
如果不设置,默认为不开启,也就是默认需要名称保持一致。
我们接着来看看条件查询,既然是条件查询,那么肯定需要我们传入查询条件,比如现在我们想通过sid字段来通过学号查找信息:
//接口中方法的定义
Student getStudentBySid(int sid);
<select id="getStudentBySid" parameterType="int" resultType="Student">
select * from student where sid = #{sid}
</select>
我们通过使用#{xxx}
或是${xxx}
来填入我们给定的属性,实际上Mybatis本质也是通过PreparedStatement
首先进行一次预编译,有效地防止SQL注入问题,但是如果使用${xxx}
就不再是通过预编译,而是直接传值,因此我们一般都使用#{xxx}
来进行操作。【挖个坑:#和$的区别是什么】
使用parameterType
属性来指定参数类型(非必须,可以不用,推荐不用)
接着我们来看插入、更新和删除操作,其实与查询操作差不多,不过需要使用对应的标签,比如插入操作:
<insert id="addStudent" parameterType="Student">
insert into student(name, sex) values(#{name}, #{sex})
</insert>
int addStudent(Student student);
我们这里使用的是一个实体类,我们可以直接使用实体类里面对应属性替换到SQL语句中,只需要填写属性名称即可,和条件查询是一样的。
复杂查询
一个老师可以教授多个学生,那么能否一次性将老师的学生全部映射给此老师的对象(1:N)呢,比如:
@Data
public class Teacher {//1
int tid;
String name;
List<Student> studentList;//N
}
映射为Teacher对象时,同时将其教授的所有学生一并映射为List列表,显然这是一种一对多的查询,那么这时就需要进行复杂查询了。而我们之前编写的都非常简单,直接就能完成映射,因此我们现在需要使用resultMap
来自定义映射规则:
三张表
student
sid | sex | name |
---|---|---|
1 | 男 | 张三 |
2 | 男 | 李四 |
3 | 男 | aaa |
4 | 女 | bbb |
5 | 女 | ccc |
teacher
tid | name |
---|---|
1 | 王 |
2 | 胡 |
3 | 李 |
teach
id | tid | sid |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 3 |
4 | 2 | 4 |
5 | 3 | 5 |
下面的sql联表结果就是:
sid | sex | name | tid | teacher.name |
---|---|---|---|---|
1 | 男 | 张三 | 1 | 王 |
2 | 男 | 李四 | 1 | 王 |
3 | 男 | aaa | 2 | 胡 |
4 | 女 | bbb | 2 | 胡 |
5 | 女 | ccc | 3 | 李 |
<select id="getTeacherByTid" resultMap="asTeacher">//注意这里写的是返回resultMap,然后通过resultMap标签去定义
select *, teacher.name as tname
from student inner join teach on student.sid = teach.sid #将教学表和学生表内联
inner join teacher on teach.tid = teacher.tid where teach.tid = #{tid}
</select>
//注意这里的匹配关系就完全是和连表查询后的字段一一对应,查到的字段有学生的所有字段、tid、teacher.name(tname),然后和teacher的属性对应起来。
<resultMap id="asTeacher" type="Teacher">
<id column="tid" property="tid"/>//注意id字段使用的是id标签,主键就是特殊哈
<result column="tname" property="name"/>
<collection property="studentList" ofType="Student">
<id column="sid" property="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
</collection>
</resultMap>
可以看到,我们的查询结果是一个多表联查的结果,而联查的数据就是我们需要映射的数据(比如这里是一个老师有N个学生,联查的结果也是这一个老师对应N个学生的N条记录),其中id
标签用于在多条记录中辨别是否为同一个对象的数据,比如上面的查询语句得到的结果中,tid
这一行始终为1
,因此所有的记录都应该是tid=1
的教师的数据,而不应该变为多个教师的数据,如果不加id进行约束,那么会被识别成多个教师的数据!
通过使用collection来表示将得到的所有结果合并为一个集合,比如上面的数据中每个学生都有单独的一条记录,因此tid相同的全部学生的记录就可以最后合并为一个List,得到最终的映射结果,当然,为了区分,最好也设置一个id,只不过这个例子中可以当做普通的result
使用。
了解了一对多,那么多对一又该如何查询呢,比如每个学生都有一个对应的老师,现在Student新增了一个Teacher对象,那么现在又该如何去处理呢?
一对多和多对一其实是相反的过程:上面例子是老师是1,学生是多。多对一也是学生是多,老师是1,只不过这次是查学生。(多个学生对应一个老师)
现在我们希望的是,每次查询到一个Student对象时都带上它的老师,同样的,我们也可以使用resultMap
来实现(先修改一下老师的类定义,不然会很麻烦,因为多对一和一对多其实是相同的表,但是类的设计不一样罢了,换句话说,多对一和一对多只要学会一个就行):
@Data
@Accessors(chain = true)
public class Student {
private int sid;
private String name;
private String sex;
private Teacher teacher;
}
@Data
public class Teacher {
int tid;
String name;
}
<resultMap id="test2" type="Student">
<id column="sid" property="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
<association property="teacher" javaType="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
</association>
</resultMap>
<select id="selectStudent" resultMap="test2">
select *, teacher.name as tname from student left join teach on student.sid = teach.sid
left join teacher on teach.tid = teacher.tid
</select>
多对一的SQL语句和之前讲的一对多完全一样(只不过我随便设计的表看不出内联和左联的区别,如果稍微改一下教学表:不是所有学生多对应有老师,那么就能体现这两种联表的差别。),所以参考上面的表结构。
通过使用association
进行关联,形成多对一的关系,实际上和一对多是同理的,都是对查询结果的一种处理方式罢了。
事务操作
我们可以在获取SqlSession
关闭自动提交来开启事务模式,和JDBC其实都差不多:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(false)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
testMapper.addStudent(new Student().setSex("男").setName("小王"));
testMapper.selectStudent().forEach(System.out::println);
}
}
我们发现,在关闭自动提交后,我们的内容是没有进入到数据库的,现在我们来试一下在最后提交事务:
sqlSession.commit();
在事务提交后,我们的内容才会被写入到数据库中。现在我们来试试看回滚操作:
try (SqlSession sqlSession = MybatisUtil.getSession(false)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
testMapper.addStudent(new Student().setSex("男").setName("小王"));
testMapper.selectStudent().forEach(System.out::println);
sqlSession.rollback();
sqlSession.commit();
testMapper.selectStudent().forEach(System.out::println);
}
回滚操作也印证成功。
动态sql
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
在实际开发中的SQL语句没有之前的这么简单,很多时候需要根据传入的参数情况动态的生成SQL语句。Mybatis提供了动态SQL相关的标签让我们使用。
if
可以使用if标签进行条件判断,条件成立才会把if标签中的内容拼接进sql语句中。
例如:
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
where id = #{id}
<if test="username!=null">
and username = #{username}
</if>
</select>
如果参数username为null则执行的sql为:select * from user where id = ?
如果参数username不为null则执行的sql为:select * from user where id = ? and username = ?
注意:在test属性中表示参数的时候不需要写#{},写了会出问题。
trim
可以使用该标签动态的添加前缀或后缀,也可以使用该标签动态的消除前缀或后缀。
prefixOverrides属性
用来设置需要被清除的前缀,多个值可以用|分隔,注意|前后不要有空格。例如: and|or
例如:
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<trim prefixOverrides="and|or" >
and
</trim>
</select>
最终执行的sql为: select * from user
suffixOverrides属性
用来设置需要被清除的后缀,多个值可以用|分隔,注意|前后不要有空格。例如: and|or
例如:
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<trim suffixOverrides="like|and" >
where 1=1 like
</trim>
</select>
最终执行的sql为: select * from user 去掉了后缀like
prefix属性
用来设置动态添加的前缀,如果标签中有内容就会添加上设置的前缀
例如:
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<trim prefix="where" >
1=1
</trim>
</select>
最终执行的sql为:select * from user where 1=1 动态增加了前缀where
suffix属性
用来设置动态添加的后缀,如果标签中有内容就会添加上设置的后缀
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<trim suffix="1=1" >
where
</trim>
</select>
最终执行的sql为:select * from user where 1=1 动态增加了后缀1=1
动态添加前缀where 并且消除前缀and或者or
User findByCondition( Integer id, String username);
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<trim prefix="where" prefixOverrides="and|or" >
<if test="id!=null">
id = #{id}
</if>
<if test="username!=null">
and username = #{username}
</if>
</trim>
</select>
调用方法时如果传入的id和username为null则执行的SQL为:select * from user ,这里trim标签没有起作用,因为trim标签里面没有东西。
调用方法时如果传入的id为null,username不为null,则执行的SQL为:select * from user where username = ?,这时候标签里面有第二个if里的,就会加上前缀where,然后去掉and。
where
where标签等价于:
<trim prefix="where" prefixOverrides="and|or" ></trim>
可以使用where标签动态的拼接where并且去除前缀的and或者or。其实就是上面的效果,不过这个时候直接用where标签
例如:
<select id="findByCondition" resultType="com.sangeng.pojo.User">
select * from user
<where>
<if test="id!=null">
id = #{id}
</if>
<if test="username!=null">
and username = #{username}
</if>
</where>
</select>
如果id和username都为null,则执行的sql为:select * from user
如果id为null,username不为null,则执行的sql为:select * from user where username = ?
set
set标签等价于
<trim prefix="set" suffixOverrides="," ></trim>
可以使用set标签动态的拼接set并且去除后缀的逗号。
例如:
<update id="updateUser">
UPDATE USER
<set>
<if test="username!=null">
username = #{username},
</if>
<if test="age!=null">
age = #{age},
</if>
<if test="address!=null">
address = #{address},
</if>
</set>
where id = #{id}
</update>
如果调用方法时传入的User对象的id为2,username不为null,其他属性都为null则最终执行的sql为:UPDATE USER SET username = ? where id = ?
foreach
可以使用foreach标签遍历集合或者数组类型的参数,获取其中的元素拿来动态的拼接SQL语句。
例如:
方法定义如下
List<User> findByIds(@Param("ids") Integer[] ids);//首先这个方法定义在接口里,不是接受前端请求那个,别搞混了。@Param("ids")表示如果形参名和SQL语句的不一致,可以使用注解统一。
如果期望动态的根据实际传入的数组的长度拼接SQL语句。例如传入长度为4个数组最终执行的SQL为:
select * from User WHERE id in( ? , ? , ? , ? ) ;
则在xml映射文件中可以使用以下写法
<select id="findByIds" resultType="com.sangeng.pojo.User">
select * from User
<where>
<foreach collection="ids" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
collection:表示要遍历的参数。
open:表示遍历开始时拼接的语句
item:表示给当前遍历到的元素的取的名字,与填入占位符的名字相同。(这里是id)
separator:表示每遍历完一次拼接的分隔符
close:表示最后一次遍历完拼接的语句
注意:如果方法参数是数组类型,默认的参数名是array,如果方法参数是list集合默认的参数名是list。建议遇到数组或者集合类型的参数统一使用@Param注解进行命名,就是为了统一罢了
choose、when、otherwise
当我们不想使用所有的条件,而只是想从多个条件中选择一个使用时。可以使用choose系列标签。类似于java中的switch。
例如:
接口中方法定义如下
List<User> selectChose(User user);
期望:
如果user对象的id不为空时就通过id查询。
如果id为null,username不为null就通过username查询。
如果id和username都会null就查询id为3的用户。
xml映射文件如下
<select id="selectChose" resultType="com.sangeng.pojo.User">
select * from user
<where>
<choose>
<when test="id!=null">
id = #{id}
</when>
<when test="username!=null">
username = #{username}
</when>
<otherwise>
id = 3
</otherwise>
</choose>
</where>
</select>
-
choose类似于java中的switch
-
when类似于java中的case
-
otherwise类似于java中的dufault
一个choose标签中最多只会有一个when中的判断成立。从上到下去进行判断。如果成立了就把标签体的内容拼接到sql中,并且不会进行其它when的判断和拼接。如果所有的when都不成立则拼接otherwise中的语句。
SQL片段抽取
我们在xml映射文件中编写SQL语句的时候可能会遇到重复的SQL片段。这种SQL片段我们可以使用sql标签来进行抽取。然后在需要使用的时候使用include标签进行使用。
实现了语句的复用。
例如:
<sql id="baseSelect" >id,username,age,address</sql>
<select id="findAll" resultType="com.sangeng.pojo.User">
select <include refid="baseSelect"/> from user
</select>
最终执行的sql为: select id,username,age,address from user