1 Mybatis
1.1 概述
MyBatis 源于 Apache 的一个开源项目 iBatis,而 iBatis 一词则来源于“internet”和“abatis”的组合,2010年这个项目由 Apache Software Foundation 迁移到了 Google Code,并且改名为MyBatis ,2013年11月其又迁移到 Github。Mybatis是一款基于Java的半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低。相比于基于面向对象使用 HQL 语言的 Hibernate 框架,MyBatis 则基于 SQL 面向结果集,因此其效率更高。
1.2 ORM介绍
ORM即Object Relation Mapping,对象关系映射。对象指的就是Java里的对象,关系指的就是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系。就比如Java里面的一个汽车Car类,就对应数据库中的汽车car表,类中的属性和表中的字段一对一对应(汽车Car类里面如果有汽车颜色color属性,那么就应该与汽车car数据表里面的汽车颜色color字段对应)。Student类就对应student表,一个Student对象就对应student表中的一行数据(一个对象里包含对象的所有属性,一行数据包含数据表里的所有字段)。
1.3 为什么Mybatis是半自动的ORM持久层框架?
因为用mybatis进行开发,我们还需要手动编写SQL语句。而全自动的ORM框架,如hibernate,则不需要编写SQL语句。用hibernate开发,只需要定义好ORM映射关系,就可以直接进行CRUD操作了。但也是由于mybatis需要手写SQL语句,所以它mybatis有较高的灵活性,可以根据需要,自由地对SQL进行定制,也因为要手写SQL,当要切换数据库时,SQL语句可能就要重写,因为不同的数据库有不同的方言(Dialect),所以mybatis的数据库无关性低。虽然mybatis需要手写SQL,但相比JDBC,它提供了输入映射和输出映射,可以很方便地进行SQL参数设置,以及结果集封装。并且还提供了关联查询和动态SQL等功能,极大地提升了开发的效率。
1.4 Mybatis优点与缺点
优点:
- 基于SQL语言编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理,提供XML标签,支持编写动态SQL语句,并可重用。
- 与JDBC相比,减少了50%以上代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。
- 很好的与各种数据库兼容(因为mybatis使用了JDBC来连接数据库,所以只要jdbc支持的数据库,mybatis同样也支持)。
- 能够与Spring很好的集成。
- 提供映射标签,支持对象与数据库的ORM字段关系映射,提供对象关系映射标签,支持对象关系组件维护。
缺点:
- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
- SQL语句依赖于数据库,导致数据库移植性较差,不能随意更换数据库。
1.5 Mybatis内部组件结构图
1.6 操作步骤
- 编写全局配置文件
- 编写mapper映射文件
- 加载全局配置文件,生成SqlSessionFactory
- 创建SqlSession,调用mapper映射文件中的SQL语句来执行CRUD操作
2 入门案例
2.1 XML映射方式
2.1.1 项目结构图
这里准备了两个类,一个是User类,一个是Dept类,前面已经提过,类中的属性应与数据表中的字段一一对应,所以还需准备两个数据表,并且数据表里的字段要与类中的属性成对应关系。两个数据表的结构与内容如下:
user表:
dept表:
2.1.2 添加依赖包
打开本module项目的pom.xml文件,向里面添加依赖,主要添加两个依赖包,一个是连接数据库的jdbc依赖包,一个是mybatis依赖包。
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
2.1.3 创建核心配置文件
在resources目录下创建核心配置文件mybatis-config.xml,即全局配置文件,里面包含了连接数据库的url、用户名、密码等信息,内容如下:
<?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">
<!-- mybatis的核心配置文件 -->
<configuration>
<typeAliases>
<!--给对应类的配置文件中的resultType参数起别名-->
<typeAlias type="cn.tedu.pojo.User" alias="User"></typeAlias>
<typeAlias type="cn.tedu.pojo.Dept" alias="Dept"></typeAlias>
</typeAliases>
<!--可以配置多个数据库连接的环境,使用default指定默认用哪个-->
<environments default="test">
<!--配置了具体连接数据库的参数-->
<environment id="test">
<!--使用的事物管理器-->
<transactionManager type="JDBC"></transactionManager>
<!--数据源:就是制定了数据库连接时的参数-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf8&serverTimezone=Asia/Shanghai" />
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--引入对应类的配置文件-->
<mappers>
<mapper resource="UserMapper.xml"/>
<mapper resource="DeptMapper.xml"/>
</mappers>
</configuration>
2.1.4 创建对应类的配置文件
同样在resources目录下创建对应类的配置文件,编写mapper映射文件,文件里主要包含要用到的SQL语句。
UserMapper.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">
<!--namespace命名空间,值唯一,用来作为文件的唯一标识-->
<mapper namespace="hello">
<!--id表示SQL注入唯一标识,因为可能有多个SQL注入,内容代表指定参数-->
<sql id="par">
id,name,addr,age
</sql>
<!--id代表要执行的SQL,select标签代表查询,还有其它标签,如delete删除标签-->
<!--resultType结果集,用来完成orm,把表里的字段自动映射给指定类里的属性,这里在核心配置文件中使用了alias对resultType的参数起了别名User,否则使用全路径cn.tedu.pojo.User-->
<select id="qurAll" resultType="User">
select <include refid="par"></include>
from user
</select>
<select id="getByName" resultType="User">
select * from user where name = #{name}
</select>
<select id="getCountByAddr" resultType="Integer">
select count(1) from user where addr = #{addr}
</select>
<select id="qurById" resultType="User">
select
<include refid="par"></include>
from user where
<if test="id > 2">id = ${id}</if>
<!--使用了if条件判断,如果传入的id>2才执行该SQL语句-->
</select>
</mapper>
2.1.5 测试结果
在test目录下创建测试类TestUser。
package cn.tedu.test;
import cn.tedu.pojo.User;
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.InputStream;
import java.util.List;
public class TestUser {
@Test
public void get() throws Exception{
//1,创建SqlSessionFactory对象,线程非安全,用来产生SqlSession
//2,创建SqlSession,用来执行sql
//3, 定位SQL: namespace的值+id的值
//4,解析结果并打印
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory session = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlsession = session.openSession();
//查询user表里的所有内容
List<User> list = sqlsession.selectList("hello.qurAll");
for (User u:list){
System.out.println(u);
}
//查询name=“xiongda”的所有内容
User ur = sqlsession.selectOne("hello.getByName","xiongda");
System.out.println(ur);
//结果可能不止一个
List<User> list3 = sqlsession.selectList("hello.getByName","xiongda");
for (User u:list3){
System.out.println(u);
}
//查询addr=“上海”的内容的数量
int count = sqlsession.selectOne("hello.getCountByAddr", "上海");
}
}
相关结果:
20:38:46.314 [main] DEBUG hello.qurAll - ==> Preparing: select id,name,addr,age from user
20:38:46.356 [main] DEBUG hello.qurAll - ==> Parameters:
20:38:46.391 [main] DEBUG hello.qurAll - <== Total: 3
User{id=1, name='hanmeimei', addr='北京', age=28}
User{id=2, name='xiongda', addr='上海', age=20}
User{id=3, name='xiaonger', addr='上海', age=19}
20:38:46.393 [main] DEBUG hello.getByName - ==> Preparing: select * from user where name = ?
20:38:46.393 [main] DEBUG hello.getByName - ==> Parameters: xiongda(String)
20:38:46.395 [main] DEBUG hello.getByName - <== Total: 1
User{id=2, name='xiongda', addr='上海', age=20}
20:38:46.396 [main] DEBUG hello.getCountByAddr - ==> Preparing: select count(1) from user where addr = ?
20:38:46.396 [main] DEBUG hello.getCountByAddr - ==> Parameters: 上海(String)
20:38:46.398 [main] DEBUG hello.getCountByAddr - <== Total: 1
2
20:38:46.427 [main] DEBUG hello.qurById - ==> Preparing: select id,name,addr,age from user where id = 3
20:38:46.428 [main] DEBUG hello.qurById - ==> Parameters:
20:38:46.429 [main] DEBUG hello.qurById - <== Total: 1
User{id=3, name='xiaonger', addr='上海', age=19}
tips:
- 编写mapper.xml,书写SQL,并定义好SQL的输入参数,和输出参数
- 编写全局配置文件,配置数据源,以及要加载的mapper.xml文件
- 通过全局配置文件,创建SqlSessionFactory
- 每次进行CRUD时,通过SqlSessionFactory创建一个SqlSession
- 调用SqlSession上的selectOne,selectList,insert,delete,update等方法,传入mapper.xml中SQL标签的id,以及输入参数
2.2 mapper.xml的SQL语句中的占位符${}和#{}
一般会采用#{},#{}在mybatis中,最后会被解析为?,其实就是Jdbc的PreparedStatement中的?占位符,它有预编译的过程,会对输入参数进行类型解析(如果入参是String类型,设置参数时会自动加上引号),所以#{}可以防止SQL注入,如果parameterType属性指定的入参类型是简单类型的话(简单类型指的是8种java原始类型再加一个String),#{}中的变量名可以任意,如果入参类型是pojo,比如是Student类
public class Student{
private String name;
private Integer age;
//setter/getter
}
那么#{name}表示取入参对象Student中的name属性,#{age}表示取age属性,这个过程是通过反射来做的,这不同于${},${}取对象的属性使用的是OGNL(Object Graph Navigation Language)表达式。而${},一般会用在模糊查询的情景,比如SELECT * FROM student WHERE name like '%${name}%';它的处理阶段在#{}之前,它不会做参数类型解析,而仅仅是做了字符串的拼接,若入参的Student对象的name属性为zhangsan,则上面那条SQL最终被解析为SELECT * FROM student WHERE name like '%zhangsan%';而如果此时用的是SELECT * FROM student WHERE name like '%#{name}%'; 这条SQL最终就会变成SELECT * FROM student WHERE name like '%'zhangsan'%'; 所以模糊查询只能用${},虽然普通的入参也可以用${},但由于${}不会做类型解析,就存在SQL注入的风险,比如SELECT * FROM user WHERE name = '${name}' AND password = '${password}'我可以让一个user对象的password属性为'OR '1' = '1,最终的SQL就变成了SELECT * FROM user WHERE name = 'yogurt' AND password = ''OR '1' = '1',因为OR '1' = '1'恒成立,这样攻击者在不需要知道用户名和密码的情况下,也能够完成登录验证。另外,对于pojo的入参,${}中获取对象属性的语法和#{}几乎一样,但${}在mybatis底层是通过OGNL表达式语言进行处理的,这跟#{}的反射处理有所不同。对于简单类型(8种java原始类型再加一个String)的入参,${}中参数的名字必须是value。
#{}之所以能防止SQL注入的原因主要是因为它不会产生字符串拼接,它会遍历传入的每个字符,对特殊字符会进行转义操作,遍历完之后会对整体字符串加单引号(其实是在遍历前后分别加了一个单引号)。所以不会产生注入问题。