# MyBatis入门
[TOC]
## 导学
MyBatis是一个大名鼎鼎的ORM框架,对于我们进行数据库开发有着非常优秀的支持。
首先我们要了解,**什么是框架**?
>[info]框架,即 framework。其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。
打个比方,Java 框架跟建筑中的框架式结构是一样的。使用了框架(钢筋+混凝土)以后,你所专著的只是业务(非承重墙构建不同格局),当然是在遵守框架的协议上开发业务。
**为什么要使用框架**?
因为软件系统发展到今天已经很复杂了,特别是服务器端软件,涉及到的知识,内容,问题太多。在某些方面使用别人成熟的框架,就相当于让别人帮你完成一些基础工作,你只需要集中精力完成系统的业务逻辑设计。而且框架一般是成熟,稳健的,你可以处理系统很多细节问题,比如,事物处理,安全性,数据流控制等问题。还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它是不断升级的,你可以直接享受别人升级代码带来的好处。
比如,我们可以自己DIY一台电脑,这就是因为我们可以使用一个现成的主板,在这个主板上有着许多规范的接口可供其他设备加入。
![](https://img.kancloud.cn/5e/1d/5e1de8ae9cb81a1003fdae9f5a28bd2a_389x332.png)
**软件开发中的框架**
* 框架是可被应用开发者定制的应用骨架
* 框架是一种规则,保证开发者遵守相同的方式开发程序
* 框架提倡“不要重复造轮子”,对基础功能进行封装
**使用软件框架的优点总结**
* 极大的提高了开发的效率
* 统一的编码规则,利于团队管理
* 灵活配置的应用,拥有更好的维护性
**SSM框架介绍**
![](https://img.kancloud.cn/f8/52/f852647603e5c86c8bf2a956616f2547_737x248.png)
>[success]1. Spring 对象容器框架,提供底层的对象管理,是框架的框架,其他的框架都要基于该框架进行开发。
>2. Spring MVC web开发框架,用于替代servlet,提供Web底层的交互,进行更有效的web开发。
>3. Mybatis 数据库框架,用于简化数据库操作,对JDBC进行了封装及扩展,提供了数据库的增删改查的便捷操作
>[info]补充介绍:SSH框架其实指的是Spring+Struts2+Hibernate框架,该框架更贴近于我们之前的Java Web学习内容,较为老旧,需要较多的配置文件,并不怎么方便。
>[warning]补充介绍:常用的数据库框架其实还有MyBatis Plus和iBatis框架等。
## MyBatis框架介绍
* MyBatis是优秀的持久层框架 --将内存中的数据保存在数据库中
* MyBatis使用XML将SQL与程序解耦,便于维护
* MyBatis学习简单,执行高效,是JDBC的延伸
>[info]对象的两种状态:
>1. 瞬时状态:程序中运行的对象,对象保存在内存中,当程序中断或者结束(计算机关闭或重启),该状态对象不会保留。
>2. 持久化状态:把对象数据保留在文件中,文件存储在永久的存储介质里(光盘、硬盘),当程序中断或者计算机重启断电,该状态的对象会永久保留。
>所谓的持久化就是把瞬时状态的对象转换为持久化状态的对象。
**MyBatis开发流程-非xml形式**
* 引入MyBatis依赖
* 创建核心配置文件
* 创建实体(Entity)类
* 创建Mapper映射文件
* 初始化SessionFactory
* 利用SqlSession对象操作数据
**ORM框架**
O:java Object 即 Java 中的对象;
R:relationship 即关系数据库;
M:mapping 将JAVA中的对象映射成关系型数据库中的表;
>[info] MyBatis 框架是一个可以自定义 SQL 和 OR 映射的持久化框架;
> 框架抛弃了大部分的 JDBC 代码,也不需要手工设置参数以及结果集的操作;
> 框架使用简单的 XML 配置或者注解来映射数据类型和关系,相对于 Hibernate 框架,MyBatis 是一种半自动化的 ORM 实现。
## MyBatis配置
在本课程中,MyBatis将依赖于Maven进行管理。
在MyBatis中,使用xml进行配置,有一个约定俗成的文件名叫做`mybatis-config.xml`,它是mybatis的一个核心配置文件。
~~~Markdown
1. Mybatis采用xml文件配置数据库环境信息
2. Mybatis环境配置标签<environment>
3. environment配置包含数据库驱动,URL,用户名和密码
~~~
![](https://img.kancloud.cn/10/c0/10c057843baf0e8eb187a22fcdcd5625_832x500.png)
**前期准备-新建项目**
pom.xml
~~~
<?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.dodoke</groupId>
<artifactId>mybatis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<!-- 可能阿里云仓库的地址会发生变化,需要查找最新的地址 -->
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<dependencies>
<!-- 数据库驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
</dependencies>
</project>
~~~
**前期准备-数据库设计**
下载地址
https://pan.baidu.com/s/1xgxXH9tPn0O_Qf5QfmmbBg
提取码
mso3
**设置idea连接数据库**
![](https://img.kancloud.cn/d4/6e/d46eb18f700d96ef6c0d232c48a2cbf8_816x677.png)
**resources目录下设置mybatis的核心配置文件mybatis-config.xml**
~~~xml
<?xml version="1.0" encoding="utf-8" ?>
<!-- 官网复制DTD约束 -->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 设置根节点 -->
<configuration>
<!-- 设置环境配置总标签,会有一些必填项出现 -->
<!-- 通过改变environments的default属性的值来选择使用什么环境 -->
<environments default="dev">
<!-- 环境配置标签,id属性为唯一标识 -->
<!-- 说明:在我们的开发中,可能会用到很多的数据库 -->
<!-- 比如在开发环境中是一套数据库,在实际生产环境中又是另一套数据库 -->
<!-- environment就能指明要使用的不同数据库信息 -->
<!-- id属性不能相同 -->
<!-- 比如这个环境配置标签就可以代表开发环境 -->
<environment id="dev">
<!-- 采用JDBC方式对数据库事务进行管理-commit/rollback -->
<!-- 其实是指,由jdbc来决定提交事务或者回滚事务 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 数据源节点,设置type="POOLED"采用数据库连接池的方式管理数据库连接 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/babytun?serverTimezone=UTC&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
<!-- 比如下面这个环境标签就代表生产环境 -->
<environment id="prd">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.1.155:3306/babytun?serverTimezone=UTC&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
~~~
## SqlSessionFactory & SqlSession
`SqlSessionFactory`是`MyBatis`中的一个重要的对象,它是用来创建`SqlSession`对象的,而`SqlSession`用来操作数据库的。
**介绍:**
* `SqlSessionFactory`是`MyBatis`的核心对象
* `SqlSessionFactory`用于初始化`MyBatis`,读取配置文件。是一个工厂方法,用于创建`SqlSession`对象。
* 要保证`SqlSessionFactory`在应用全局中只存在唯一的对象,通常会使用静态类的方式对其进行初始化。
`SqlSession`是`MyBatis`用来操作数据库的一个核心对象,不那么严谨的说,可以将SqlSession看做类似于我们之前学习过的JDBC的连接接口对象(`Connection`)执行接口对象(`PreparedStatement`)的组合,用来执行CRUD操作。
**介绍:**
* `SqlSession`是`MyBatis`操作数据库的核心对象
* `SqlSession`使用JDBC的方式与数据库交互
* `SqlSession`对象提供了数据表的CRUD方法
**示例**
引入Junit组件进行测试应用
依赖:
~~~
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
~~~
测试代码:
~~~
package com.dodoke.mybatistest;
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;
/**
* Junit单元测试用例类
* 规范存放在maven项目的test文件夹中
*/
public class MyBatisTest {
@Test
public void sqlSessionFactoryTest() throws IOException {
//通过MyBatis提供的资源类,获取对应的配置文件作为字符流读取
//getResourceAsReader方法会默认的从当前classpath类路径下加载文件
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
//初始化SqlSessionFactory,并同时解析mybatis-config.xml文件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
System.out.println("SqlSessionFactory对象加载成功");
//创建SqlSession对象,用于与数据库产生交互,注意SqlSession它是JDBC的扩展类
SqlSession sqlSession = null;
try {
sqlSession = sqlSessionFactory.openSession();
//在SqlSession对象底层存在Connection(java.sql)连接对象,可以通过getConnection方法得到该对象
Connection connection = sqlSession.getConnection();
//该connection对象的创建仅做演示测试用,在mybatis中,无需使用任何与JDBC有关的类
System.out.println(connection);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(sqlSession != null) {
//在mybatis-config.xml文件中,dataSource节点type属性:
//如果type="POOLED",代表使用连接池,close则是将连接回收到连接池中
//如果type="UNPOOLED",代表直连,close则会调用Connection.close()方法关闭连接
//这是配置带来的底层处理机制的不同
sqlSession.close();
}
}
}
}
~~~
## 设置MybatisUtils工具类
在之前的课程中,我们提到需要保证`SqlSessionFactory`在全局中保证唯一,那么如何保证该`SqlSessionFactory`在应用全局保证唯一呢?
通过额外创建的工具类`MybatisUtils`对`SqlSessionFactory`对象的初始化以及`SqlSession`对象的创建和释放方法进行封装 。
**说明:**
1. 一般工具类放在`utils`包下;
2. 用`static`代码块对静态对象进行初始化;
3. 这边我们在异常捕获后将类的初始化的过程中产生的异常抛出,为了外界能捕获到这个异常信息并进行后续处理,而不是直接终止运行程序,我们需要将异常抛出;
4. 提供`SqlSession`对象的创建与释放方法,工具类的大多数方法要使用`static`进行描述。
**工具类代码**
~~~
package com.dodoke.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 java.io.IOException;
import java.io.Reader;
/**
* MyBatisUtils工具类,创建全局唯一的SqlSessionFactory对象
*/
public class MyBatisUtils {
//设置私有静态属性,因为静态内容属于类而不属于对象,且拥有全局唯一的特性
private static SqlSessionFactory sqlSessionFactory = null;
//利用静态代码块在初始化类时实例化sqlSessionFactory属性
static {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
//需要抛出初始化的异常,并且传入捕捉到的异常,形成一条完整的异常链
//以便于通知调用者
throw new ExceptionInInitializerError(e);
}
}
/**
* 获取数据库交互SqlSession
* @return SqlSession对象
*/
public static SqlSession openSqlSession() {
return sqlSessionFactory.openSession();
}
/**
* 释放一个有效的SqlSession对象
* @param sqlSession 准备释放的SqlSession对象
*/
public static void closeSqlSession(SqlSession sqlSession) {
if(sqlSession != null) {
sqlSession.close();
}
}
}
~~~
**测试类单元测试代码**
~~~
/**
* MyBatisUtils使用指南
* @throws Exception
*/
@Test
public void testMyBatisUtils() throws Exception {
SqlSession sqlSession = null;
try {
sqlSession = MyBatisUtils.openSqlSession();
Connection connection = sqlSession.getConnection();
System.out.println(connection);
}catch (Exception e){
throw e;
} finally {
MyBatisUtils.closeSqlSession(sqlSession);
}
}
~~~
## MyBatis数据查询
在MyBatis中,虽然我们可以使用MyBatis之前的旧形式,写出如同JDBC那样Java代码和SQL代码混合的数据操作命令,但是我们不建议大家这么做!
对于MyBatis数据查询,可以总结出如下的步骤:
**1. 创建实体类(Entity)**
在`main/java`下创建`com.dodoke.mybatis.entity`包,`entity`包下创建数据库中`t_goods`表对应的`Goods`商品实体类,将数据表中的字段对应在实体类中增加一系列的私有属性及getter/setter方法,属性采用驼峰命名。
![](https://img.kancloud.cn/f7/9f/f79ff233297127287930a9ae3d8dcfda_913x287.png)
~~~
/**
* 数据库t_goods表对应映射的实体类
*/
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;//分类编号
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;
}
}
~~~
**2. 创建Mapper XML说明SQL语句**
第二步,第三步结合使用,具体内容在第三步中。
**3. 在Mapper XML中增加SQL语句对应标签**
在`main/resources`下创建新的子目录`mappers`,`mappers`代表映射器,里面存放的都是xml文件。创建`GoodsMapper.xml`文件来说明实体类和数据表的对应关系(和哪个数据表对应,属性和字段怎么对应)。
说明:
`A.` 根节点通过增加不同的命名空间`namespace`来区分不同的`mapper`文件,通常我们会将针对一张表操作的`SQL`语句放置在一个`mapper`文件中。
`B.` 语句节点的`id`属性为别名,相当于`SQL`名称,同一个`namespace`下`id`要唯一,不同的`namespace`可以重名;因此`namespace`的设置就很有必要,不然调用`SQL`的时候分不清哪个`id`
`C.` 语句节点的`resultType`属性代表返回的对象是什么,为实体类的完整路径,在`SQL`语句执行完后会自动的将得到的每一条记录包装成对应的实体类的对象;
~~~
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 将之前的config DTD约束改为mapper DTD约束 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 为这个mapper指定一个唯一的namespace -->
<!-- 注意每个mapper文件的namespace是不能相同的 -->
<!-- namespace非常类似于Java类的包,同样也是用于区分每个不同的mapper文件 -->
<!-- 所以namespace的值,习惯上设置成包名+sql映射文件名,这样就能够保证namespace的值是唯一的 -->
<!-- 例如namespace="com.dodoke.mybatis.resources.mappers.GoodsMapper" -->
<!--就是com.dodoke.mybatis.resources.mappers(包名/文件夹名)+GoodsMapper(GoodsMapper.xml文件去除后缀) -->
<mapper namespace="com.dodoke.mybatis.resources.mappers.GoodsMapper">
<!-- id属性相当于为SQL语句起了个名称 -->
<!-- 在一个mapper文件中是不允许出现相同的id属性值的 -->
<!-- resultType代表返回结果的类型,它会将SQL语句执行完的每一条结果包装为对应的属性值指定的对象 -->
<select id="selectAll" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods order by goods_id desc limit 10
</select>
</mapper>
~~~
**4. 在mybatis-config.xml中增加Mapper XML文件声明**
其实就是让`MyBatis`认识新创建的`GoodsMapper.xml`:
在`mybatis-config.xml`中添加`mappers`标签,这样`MyBatis`在初始化的时候才知道这个`GoodsMapper.xml`的存在。
![](https://img.kancloud.cn/a2/0e/a20e9c382d0fe2cfeca286f76b9502ff_846x222.png)
~~~
<mappers>
<!-- 注册GoodsMapper.xml文件 -->
<mapper resource="mappers/GoodsMapper.xml"></mapper>
</mappers>
~~~
**5. 利用SqlSession执行Mapper XML中的SQL语句**
~~~
/**
* select查询语句执行
* @throws Exception
*/
@Test
public void testSelectAll() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
//selectList代表查询多条数据,selectOne代表查询一条结果
List<Goods> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectAll");
for(Goods g : list){
System.out.println(g.getTitle());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
对于这样的查询,其实获取到的数据是存在数据丢失的,这是因为我们的查询结果类型字段和表中字段名不能匹配!
**6. 在mybatis-config.xml中开启驼峰命名映射**
其实第六步应该放在第五步之前,这里只是给大家作为演示。
![](https://img.kancloud.cn/ad/12/ad1201823038079d0ac344755b912a3a_959x293.png)
~~~
<settings>
<!-- 驼峰命名转化设置 -->
<!-- 该设置表示将数据库中表的字段,比如goods_id => goodsId -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
~~~
## MyBatis的SQL传参
在实现CRUD等操作的时候,有很多的SQL条件数据其实是通过接受前台动态传递过来的参数决定的。那么如何设置这些SQL语句的参数呢?
在数据操作节点中,可以添加`parameterType`属性指定参数类型,并采用`#{param}`的形式接受传入的参数。
示例:
GoodsMapper.xml
~~~xml
<!-- 单参数传递,使用parameterType指定参数的数据类型即可,SQL中#{value}提取参数-->
<select id="selectById" parameterType="Integer" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods where goods_id = #{value}
</select>
<!-- 多参数传递时,使用parameterType指定Map接口,SQL中#{key}提取参数 -->
<select id="selectByPriceRange" parameterType="java.util.Map" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods
where
current_price between #{min} and #{max}
order by current_price
limit 0,#{limt}
</select>
~~~
测试:
~~~
/**
* 传递单个SQL参数
* @throws Exception
*/
@Test
public void testSelectById() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
//传入的参数类型需要和对应数据操作节点中指明的参数类型一致
Goods goods = session.selectOne("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectById" , 1603);
System.out.println(goods.getTitle());
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
/**
* 传递多个SQL参数
* @throws Exception
*/
@Test
public void testSelectByPriceRange() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
Map param = new HashMap();
//map中的key-value的key值,需要和数据操作节点中参数名一致
param.put("min",100);
param.put("max" , 500);
param.put("limt" , 10);
List<Goods> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectByPriceRange", param);
for(Goods g:list){
System.out.println(g.getTitle() + ":" + g.getCurrentPrice());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
## 多表关联查询
在之前的学习中,我们针对的都是一个表的查询,那么如何针对多表进行联合查询呢?
其实我们可以将返回的结果变为`Map`类型,这样`MyBatis`就会将结果封装为`Map`集合中对应的键值对
~~~
<select id="selectGoodsMap" resultType="java.util.Map" flushCache="true">
select g.* , c.category_name from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
~~~
/**
* 利用Map接收关联查询结果
* @throws Exception
*/
@Test
public void testSelectGoodsMap() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
List<Map> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectGoodsMap");
for(Map map : list){
System.out.println(map);
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
我们可以看到,该方法返回的结果为数据库中表对应的原始字段名为key值,而且查询到的结果的顺序是混乱的。
为了保证我们等到的结果的顺序和数据库中的顺序一致,我们需要使用`LinkedHashMap`。
1. `LinkedHashMap`是采用链表形式的`HashMap`,他在进行数据提取的时候是按照插入数据时的顺序进行提取保存的,不会出现乱序的情况。
2. 使用`LinkedHashMap`来接收数据是常用的,因为公司的数据结构较为复杂,需要多表关联查询,`LinkedHashMap`可以有效进行数据的扩展,非常灵活。
3. 缺点:太过灵活,任何查询结果都会被`LinkedHashMap`包装在内,相比较实体类而言,缺少了编译时检查,是很容易出错的。
~~~
<!-- 利用LinkedHashMap保存多表关联结果
MyBatis会将每一条记录包装为LinkedHashMap对象
key是字段名 value是字段对应的值 , 字段类型根据表结构进行自动判断
优点: 易于扩展,易于使用
缺点: 太过灵活,无法进行编译时检查
-->
<!-- 新增一个test字段看一下结果 -->
<select id="selectGoodsMap" resultType="java.util.LinkedHashMap">
select g.* , c.category_name,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
其实针对于这样的多表查询,我们还可以通过修改实体类来实现,显得不够灵活,但是却可以保证在编译的时候进行检查。具体选用哪种方式可以根据实际情况进行选择。
PS:注意,在之前我们的学习中,我们是利用在`mybaits-config.xml`文件中设置驼峰映射的方式,来解决字段和实体类属性名称不能匹配的问题的,但是我们也可以设置在查询的时候起别名的方式,解决这个问题。
## ResultMap结果映射
介绍:
* ResultMap可以将查询结果映射为复杂类型的Java对象。
* ResultMap适用于Java对象保存多表关联结果
* ResultMap是MyBatis关联的核心所在,支持对象关联查询等高级特性
在上节课程中,我们也提到过,可以为了查询结果去修改实体类。但是,这种方式在标准的mybatis开发下是不太建议的。实体类仅仅和数据表对应就好,不要添加一些冗余的属性,但是**在实际开发中,我们有时为了方便,实际上较多的还是采用修改实体类的形式**。
但是,采用DTO,数据扩展类开发的形式,我们同学们必须掌握。
在`com.dodoke.mybatis`包下面新建一个dto包,新建`GoodsDTO`类。
>[info]DTO是一个特殊的JavaBean,数据传输对象。对原始对象进行扩展,用于数据保存和传递。
~~~
/**
* 扩展类,数据传输对象
*/
public class GoodsDTO {
private Goods goods = new Goods();
private String categoryName;
private String test;
public Goods getGoods() {
return goods;
}
public void setGoods(Goods goods) {
this.goods = goods;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
~~~
使用resultMap属性,添加结果映射
~~~
<!--结果映射,通常id属性设置为rm+...,type指明要转化为的DTO -->
<resultMap id="rmGoods" type="com.dodoke.mybatis.dto.GoodsDTO">
<!--设置主键字段与属性映射,必写-->
<!-- property=goods.goodsId指的是,每次查到的goods_id字段的数据会为GoodsDTO类中goods属性对象的goodsId属性进行赋值 -->
<id property="goods.goodsId" column="goods_id"></id>
<!--设置非主键字段与属性映射-->
<result property="goods.title" column="title"></result>
<result property="goods.originalCost" column="original_cost"></result>
<result property="goods.currentPrice" column="current_price"></result>
<result property="goods.discount" column="discount"></result>
<result property="goods.isFreeDelivery" column="is_free_delivery"></result>
<result property="goods.categoryId" column="category_id"></result>
<result property="categoryName" column="category_name"></result>
<result property="test" column="test"/>
</resultMap>
<select id="selectGoodsDTO" resultMap="rmGoods">
select g.* , c.*,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
测试
~~~
/**
* 利用ResultMap进行结果映射
* @throws Exception
*/
@Test
public void testSelectGoodsDTO() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
List<GoodsDTO> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectGoodsDTO");
for (GoodsDTO g : list) {
System.out.println(g.getGoods().getTitle());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
其实我们可以继续扩展,比如我现在不仅仅想要得到`category_name`产品名称,还想要获得其他属性,那么我们该怎么办呢?
新建`t_category`表的实体类
~~~
package com.dodoke.mybatis.entity;
public class Category {
private Integer categoryId;
private String categoryName;
private Integer parentId;
private Integer categoryLevel;
private Integer categoryOrder;
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public Integer getCategoryLevel() {
return categoryLevel;
}
public void setCategoryLevel(Integer categoryLevel) {
this.categoryLevel = categoryLevel;
}
public Integer getCategoryOrder() {
return categoryOrder;
}
public void setCategoryOrder(Integer categoryOrder) {
this.categoryOrder = categoryOrder;
}
}
~~~
修改DTO数据对象
~~~
package com.dodoke.mybatis.dto;
import com.dodoke.mybatis.entity.Category;
import com.dodoke.mybatis.entity.Goods;
/**
* 扩展类,数据传输对象
*/
public class GoodsDTO {
private Goods goods = new Goods();
private Category category = new Category();
private String test;
public Goods getGoods() {
return goods;
}
public void setGoods(Goods goods) {
this.goods = goods;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
~~~
修改映射结果集
~~~
<!--结果映射,通常id属性设置为rm+...,type指明要转化为的DTO -->
<resultMap id="rmGoods" type="com.dodoke.mybatis.dto.GoodsDTO">
<!--设置主键字段与属性映射,必写-->
<!-- property=goods.goodsId指的是,每次查到的goods_id字段的数据会为GoodsDTO类中goods属性对象的goodsId属性进行赋值 -->
<id property="goods.goodsId" column="goods_id"></id>
<!--设置非主键字段与属性映射-->
<result property="goods.title" column="title"></result>
<result property="goods.originalCost" column="original_cost"></result>
<result property="goods.currentPrice" column="current_price"></result>
<result property="goods.discount" column="discount"></result>
<result property="goods.isFreeDelivery" column="is_free_delivery"></result>
<result property="goods.categoryId" column="category_id"></result>
<result property="category.categoryId" column="category_id"></result>
<result property="category.categoryName" column="category_name"></result>
<result property="category.parentId" column="parent_id"></result>
<result property="category.categoryLevel" column="category_level"></result>
<result property="category.categoryOrder" column="category_order"></result>
<result property="test" column="test"/>
</resultMap>
<select id="selectGoodsDTO" resultMap="rmGoods">
select g.* , c.*,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
## MyBatis数据写入
在之前的课程中,我们实现了MyBatis的数据查询工作,接下来,我们来看看如何实现数据的新增,修改和删除工作。
### 数据库事务
提到数据库的写入操作,就离不开数据库的事务。
**数据库事务是保证数据操作完整性的基础**
所有从客户端发来的新增修改删除操作,都会被事务日志所记录,我们形象的将事务日志看成流水账,它记录客户端发来的所有写操作的前后顺序,
当客户端向`MySQL`服务器发起了一个`commit`提交命令的时候,事务日志才会将这三个数据同时的写入到数据表中,在`commit`的时候才是真正的往数据表写入的过程,当这三条数据都被成功写入到数据表中后,刚才所产生的事务日志都会被清空掉。
![](https://img.kancloud.cn/2f/5a/2f5a95e5c02ca2e7c739a41cd244b721_640x216.png)
![](https://img.kancloud.cn/cd/df/cddf973517926ed0de48705121e68ff5_661x244.png)
假设如果客户端在处理这些数据的时候,数据1和数据2执行成功,数据3因为各种原因没有执行成功的话,客户端会发起一个`rollback`回滚命令,当`MySQL`收到了`rollback`回滚命令后,当前事务日志中的所有已经产生的数据都会被清除,这就意味着前面已经产生的数据1和数据2不会放入到数据表中,只有当所有数据都完成的时候,在由客户端发起`commit`提交,数据才能成功的写入。
**要么数据全部写入成功,要么中间出现了任何问题,全部回滚,保证了数据的完整性**
### 案例
修改`MyBatisUtils`
~~~
/**
* 获取数据库交互SqlSession
* @return SqlSession对象
*/
public static SqlSession openSqlSession() {
//默认SqlSession对自动提交事务数据(commit)
//设置false代表关闭自动提交,改为手动提交事务数据
return sqlSessionFactory.openSession(false);
}
~~~
**新增**
~~~
<insert id="insert" parameterType="com.dodoke.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})
<!-- 该语句可以不写 -->
<!-- 该语句表示插入goods对象后,获得插入后自动生成的主键值,并将该值保存到插入的goods对象的goodsId中 -->
<selectKey resultType="Integer" keyProperty="goodsId" order="AFTER">
select last_insert_id()
</selectKey>
</insert>
~~~
~~~
/**
* 新增数据
* @throws Exception
*/
@Test
public void testInsert() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
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()方法返回值代表本次成功插入的记录总数
int num = session.insert("com.dodoke.mybatis.resources.mappers.GoodsMapper.insert", goods);
session.commit();//提交事务数据
System.out.println(goods.getGoodsId());
}catch (Exception e){
if(session != null){
session.rollback();//回滚事务
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
我们在上述代码中可以利用`selectKey`标签获得对应的新增主键,其实我们还可以利用另外一个属性`userGenerateKeys`实现获得新增主键,它们的区别在哪里呢?
1. `SelectKey`适用于所有数据库,但需要根据不同的数据库编写对应的获得最后改变主键值得查询语句
2. `userGenerateKeys`只支持“自增主键”的数据库(DB2,Oracle等没有自增主键约束),但使用简单,会根据不同的数据库驱动自动编写查询语句,以下是该属性的使用方法
~~~
<insert id="insert" parameterType="com.dodoke.mybatis.entity.Goods" userGenerateKeys="true" keyProperty="goodsId" keyColumn="goods_id">
insert 语句
</insert>
~~~
如果要在Oracle中获得新增后的主键,需要借助序列来实现,其实是通过序列在执行新增语句之前生成一个新的序列值并保存到主键字段中。
![](https://img.kancloud.cn/9c/23/9c23b83b4869f75094d893eb250ef24f_710x157.png)
**更新与删除**
~~~
<update id="update" parameterType="com.dodoke.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>
<delete id="delete" parameterType="Integer">
delete from t_goods where goods_id = #{value}
</delete>
~~~
~~~
/**
* 更新数据
* @throws Exception
*/
@Test
public void testUpdate() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectById", 739);
goods.setTitle("更新测试商品");
int num = session.update("com.dodoke.mybatis.resources.mappers.GoodsMapper.update" , goods);
session.commit();//提交事务数据
}catch (Exception e){
if(session != null){
session.rollback();//回滚事务
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
/**
* 删除数据
* @throws Exception
*/
@Test
public void testDelete() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
int num = session.delete("com.dodoke.mybatis.resources.mappers.GoodsMapper.delete" , 739);
session.commit();//提交事务数据
}catch (Exception e){
if(session != null){
session.rollback();//回滚事务
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
## 预防SQL注入攻击
在之前的学习中,我们了解到什么是SQL注入攻击,并且在JDBC课程中也去实现了如何预防SQL注入攻击。那么,在MyBatis中如何去进行SQL注入攻击的预防呢?
其实,SQL注入攻击的原理非常简单,就在在接收用户输入的时候,不对接收的数据进行任何的判断和转义,导致在接收时可能全盘接收到诸如单引号、or等一些SQL关键字。
所以,预防SQL注入攻击需要的是对接收数据进行判断和转义。在MyBatis中,这些工作其实早就已经为我们准备好了。
**MyBatis两种传值方式**
* `${}`文本替换,未经任何处理对SQL文本替换
* `#{}`预编译传值,使用预编译传值可以预防SQL注入
在我们的实际使用中,更多的还是通过`#{}`的形式进行传值