文章目录
Mybatis
概述
-
mybatis是什么?有什么特点?
它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低
-
什么是ORM?
Object Relation Mapping,对象关系映射。对象指的是Java对象,关系指的是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系,比如用一个Java的Student类,去对应数据库中的一张student表,类中的属性和表中的列一一对应。Student类就对应student表,一个Student对象就对应student表中的一行数据
-
为什么mybatis是半自动的ORM框架?
用mybatis进行开发,需要手动编写SQL语句。而全自动的ORM框架,如hibernate,则不需要编写SQL语句。用hibernate开发,只需要定义好ORM映射关系,就可以直接进行CRUD操作了。由于mybatis需要手写SQL语句,所以它有较高的灵活性,可以根据需要,自由地对SQL进行定制,也因为要手写SQL,当要切换数据库时,SQL语句可能就要重写,因为不同的数据库有不同的方言(Dialect),所以mybatis的数据库无关性低。虽然mybatis需要手写SQL,但相比JDBC,它提供了输入映射和输出映射,可以很方便地进行SQL参数设置,以及结果集封装。并且还提供了关联查询和动态SQL等功能,极大地提升了开发的效率。并且它的学习成本也比hibernate低很多
-
快速入门
只需要通过如下几个步骤,即可用mybatis快速进行持久层的开发
- 编写全局配置文件
- 编写mapper映射文件
- 加载全局配置文件,生成SqlSessionFactory
- 创建SqlSession,调用mapper映射文件中的SQL语句来执行CRUD操作
原生开发示例
-
在本地虚拟机mysql上创建一个库yogurt,并在里面创建一张student表
-
打开IDEA,创建一个maven项目
-
导入依赖的jar包
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 解决打包时候资源无法引入问题-->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
-
创建一个po类
package com.yogurt.po; import lombok.*; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString public class Student { private Integer id; private String name; private Integer score; private Integer age; private Integer gender; } 1234567891011121314151617181920212223
-
编写mapper映射文件(编写SQL)
<!-- StudentMapper.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="test"> <select id="findAll" resultType="com.yogurt.po.Student"> SELECT * FROM student; </select> <insert id="insert" parameterType="com.yogurt.po.Student"> INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender}); </insert> <delete id="delete" parameterType="int"> DELETE FROM student WHERE id = #{id}; </delete> </mapper> 12345678910111213141516171819
-
编写数据源properties文件
db.url=jdbc:mysql://192.168.183.129:3306/yogurt?characterEncoding=utf8 db.user=root db.password=root db.driver=com.mysql.jdbc.Driver 1234
-
编写全局配置文件(主要是配置数据源信息)
<?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> <!-- 配置文件信息 --> <properties resource="properties/db.properties"></properties> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!-- 从配置文件中加载属性 --> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"/> <property name="username" value="${db.user}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <mappers> <!-- 加载前面编写的SQL语句的文件 --> <mapper resource="StudentMapper.xml"/> </mappers> </configuration> 123456789101112131415161718192021222324252627
-
编写dao类
package com.yogurt.dao; import com.yogurt.po.Student; 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.InputStream; import java.util.List; public class StudentDao { private SqlSessionFactory sqlSessionFactory; public StudentDao(String configPath) throws IOException { InputStream inputStream = Resources.getResourceAsStream(configPath); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } public List<Student> findAll() { SqlSession sqlSession = sqlSessionFactory.openSession(); List<Student> studentList = sqlSession.selectList("findAll"); sqlSession.close(); return studentList; } public int addStudent(Student student) { SqlSession sqlSession = sqlSessionFactory.openSession(); int rowsAffected = sqlSession.insert("insert", student); sqlSession.commit(); sqlSession.close(); return rowsAffected; } public int deleteStudent(int id) { SqlSession sqlSession = sqlSessionFactory.openSession(); int rowsAffected = sqlSession.delete("delete",id); sqlSession.commit(); sqlSession.close(); return rowsAffected; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445
-
测试
public class SimpleTest { private StudentDao studentDao; @Before public void init() throws IOException { studentDao = new StudentDao("mybatis-config.xml"); } @Test public void insertTest() { Student student = new Student(); student.setName("yogurt"); student.setAge(24); student.setGender(1); student.setScore(100); studentDao.addStudent(student); } @Test public void findAllTest() { List<Student> all = studentDao.findAll(); all.forEach(System.out::println); } } 12345678910111213141516171819202122232425
总结:
- 编写mapper.xml,书写SQL,并定义好SQL的输入参数,和输出参数
- 编写全局配置文件,配置数据源,以及要加载的mapper.xml文件
- 通过全局配置文件,创建SqlSessionFactory
- 每次进行CRUD时,通过SqlSessionFactory创建一个SqlSession
- 调用SqlSession上的
selectOne
,selectList
,insert
,delete
,update
等方法,传入mapper.xml中SQL标签的id,以及输入参数
万能的Map
场景:有时候一个表有很多个字段,但是你只想用其中的某几个字段,这时候就可以用Map
思路二:使用万能的Map
1、在接口方法中,参数直接传递Map;
User selectUserByNP2(Map<String,Object> map);
2、编写sql语句的时候,需要传递参数类型,参数类型为map
<select id="selectUserByNP2" parameterType="map" resultType="com.kuang.pojo.User">
select * from user where name = #{username} and pwd = #{pwd}
</select>
3、在使用方法的时候,Map的 key 为 sql中取的值即可,没有顺序要求!
Map<String, Object> map = new HashMap<String, Object>();
map.put("username","小明");
map.put("pwd","123456");
User user = mapper.selectUserByNP2(map);
总结:如果参数过多,我们可以考虑直接使用Map实现,如果参数比较少,直接传递参数即可
注意要点
-
全局配置文件中,各个标签要按照如下顺序进行配置,因为mybatis加载配置文件的源码中是按照这个顺序进行解析的
<configuration> <!-- 配置顺序如下 properties settings typeAliases typeHandlers objectFactory plugins environments environment transactionManager dataSource mappers --> </configuration> 12345678910111213141516171819202122
各个子标签说明如下
-
<properties>
一般将数据源的信息单独放在一个properties文件中,然后用这个标签引入,在下面environment标签中,就可以用
${}
占位符快速获取数据源的信息 -
<settings>
用来开启或关闭mybatis的一些特性,比如可以用
<setting name="lazyLoadingEnabled" value="true"/>
来开启延迟加载,可以用<settings name="cacheEnabled" value="true"/>
来开启二级缓存 -
<typeAliases>
在mapper.xml中需要使用
parameterType
和resultType
属性来配置SQL语句的输入参数类型和输出参数类型,类必须要写上全限定名,比如一个SQL的返回值映射为Student类,则resultType
属性要写com.yogurt.po.Student
,这太长了,所以可以用别名来简化书写,比如<typeAliases> <typeAlias type="com.yogurt.po.Student" alias="student"/> </typeAliases> 123
之后就可以在
resultType
上直接写student
,mybatis会根据别名配置自动找到对应的类。当然,如果想要一次性给某个包下的所有类设置别名,可以用如下的方式
<typeAliases> <package name="com.yogurt.po"/> </typeAliases> 123
如此,指定包下的所有类,都会以简单类名的小写形式,作为它的别名
另外,对于基本的Java类型 -> 8大基本类型以及包装类,以及String类型,mybatis提供了默认的别名,别名为其简单类名的小写,比如原本需要写
java.lang.String
,其实可以简写为string
-
<typeHandlers>
用于处理Java类型和Jdbc类型之间的转换,mybatis有许多内置的TypeHandler,比如StringTypeHandler,会处理Java类型String和Jdbc类型CHAR和VARCHAR。这个标签用的不多
-
<objectFactory>
mybatis会根据
resultType
或resultMap
的属性来将查询得到的结果封装成对应的Java类,它有一个默认的DefaultObjectFactory,用于创建对象实例,这个标签用的也不多 -
<plugins>
可以用来配置mybatis的插件,比如在开发中经常需要对查询结果进行分页,就需要用到pageHelper分页插件,这些插件就是通过这个标签进行配置的。在mybatis底层,运用了责任链模式+动态代理去实现插件的功能
<!-- PageHelper 分页插件 --> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> </plugin> </plugins> 123456
-
<environments>
用来配置数据源
-
<mappers>
用来配置mapper.xml映射文件,这些xml文件里都是SQL语句
-
-
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 } 12345
那么
#{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
,例子如下<select id="fuzzyCount" parameterType="string" resultType="int"> SELECT count(1) FROM `user` WHERE name like '%${value}%' </select> 123
为什么简单类型的变量名必须为value呢?因为mybatis源码中写死的value,哈哈
上面其实是比较原始的开发方式,我们需要编写dao类,针对mapper.xml中的每个SQL标签,做一次封装,SQL标签的id要以字符串的形式传递给SqlSession的相关方法,容易出错,非常不方便;为了简化开发,mybatis提供了mapper接口代理的开发方式,不需要再编写dao类,只需要编写一个mapper接口,一个mapper的接口和一个mapper.xml相对应,只需要调用SqlSession对象上的getMapper()
,传入mapper接口的class信息,即可获得一个mapper代理对象,直接调用mapper接口中的方法,即相当于调用mapper.xml中的各个SQL标签,此时就不需要指定SQL标签的id字符串了,mapper接口中的一个方法,就对应了mapper.xml中的一个SQL标签
基于Mapper代理的示例
全局配置文件和mapper.xml文件是最基本的配置,仍然需要。不过,这次我们不编写dao类,我们直接创建一个mapper接口
package com.yogurt.mapper;
import com.yogurt.po.Student;
import java.util.List;
public interface StudentMapper {
List<Student> findAll();
int insert(Student student);
int delete(Integer id);
List<Student> findByName(String value);
}
12345678910111213141516
而我们的mapper.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="com.yogurt.mapper.StudentMapper">
<select id="findAll" resultType="com.yogurt.po.Student">
SELECT * FROM student;
</select>
<insert id="insert" parameterType="com.yogurt.po.Student">
INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});
</insert>
<delete id="delete" parameterType="int">
DELETE FROM student WHERE id = #{id};
</delete>
<select id="findByName" parameterType="string" resultType="student">
SELECT * FROM student WHERE name like '%${value}%';
</select>
</mapper>
12345678910111213141516171819202122
mapper接口和mapper.xml之间需要遵循一定规则,才能成功的让mybatis将mapper接口和mapper.xml绑定起来
- mapper接口的全限定名,要和mapper.xml的namespace属性一致
- mapper接口中的方法名要和mapper.xml中的SQL标签的id一致
- mapper接口中的方法入参类型,要和mapper.xml中SQL语句的入参类型一致
- mapper接口中的方法出参类型,要和mapper.xml中SQL语句的返回值类型一致
测试代码如下
public class MapperProxyTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
}
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> studentList = mapper.findAll();
studentList.forEach(System.out::println);
}
}
1234567891011121314151617
结果如下
这个mapper接口,mybatis会自动找到对应的mapper.xml,然后对mapper接口使用动态代理的方式生成一个代理类
基于注解的示例
如果实在看xml配置文件不顺眼,则可以考虑使用注解的开发方式,不过注解的开发方式,会将SQL语句写到代码文件中,后续的维护性和扩展性不是很好(如果想修改SQL语句,就得改代码,得重新打包部署,而如果用xml方式,则只需要修改xml,用新的xml取替换旧的xml即可)
使用注解的开发方式,也还是得有一个全局配置的xml文件,不过mapper.xml就可以省掉了,具体操作只用2步,如下
-
创建一个Mapper接口
package com.yogurt.mapper; import com.yogurt.po.Student; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import java.util.List; public interface PureStudentMapper { @Select("SELECT * FROM student") List<Student> findAll(); @Insert("INSERT INTO student (name,age,score,gender) VALUES (#{name},#{age},#{score},#{gender})") int insert(Student student); } 1234567891011121314
-
在全局配置文件中修改
<mappers>
标签,直接指定加载这个类<?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> <properties resource="properties/db.properties"></properties> <typeAliases> <package name="com.yogurt.po"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"/> <property name="username" value="${db.user}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <mappers> <mapper class="com.yogurt.mapper.PureStudentMapper"/> </mappers> </configuration> 1234567891011121314151617181920212223242526
测试代码如下
public class PureMapperTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
PureStudentMapper mapper = sqlSession.getMapper(PureStudentMapper.class);
mapper.insert(new Student(10,"Tomcat",120,60,0));
sqlSession.commit();
List<Student> studentList = mapper.findAll();
studentList.forEach(System.out::println);
}
}
1234567891011121314151617181920
结果如下
注:当使用注解开发时,若需要传入多个参数,可以结合@Param
注解,示例如下
package org.mybatis.demo.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.mybatis.demo.po.Student;
import java.util.List;
public interface PureStudentMapper {
@Select("SELECT * FROM student WHERE name like '%${name}%' AND major like '%${major}%'")
List<Student> find(@Param("name") String name, @Param("major") String major);
}
12345678910111213
@Param
标签会被mybatis处理并封装成一个Map对象,比如上面的示例中,实际传入的参数是一个Map对象,@Param
标签帮忙向Map中设置了值,即它做了
Map<String,Object> map = new HashMap<>();
map.put("name", name);
map.put("major",major);
123
将方法形参中的name
和major
放到了map对象中,所以在@Select
标签中可以用${name}
和${major}
取出map对象中的值。
--------------------(我是分割线)
上面我们见到了在全局配置文件中,两种配置mapper的方式,分别是
<!-- 在mapper接口中使用注解 -->
<mappers>
<mapper class="com.yogurt.mapper.PureStudentMapper"/>
</mappers>
<!-- 普通加载xml -->
<mappers>
<mapper resource="StudentMapper.xml"/>
</mappers>
12345678910
而在实际工作中,一般我们会将一张表的SQL操作封装在一个mapper.xml中,可能有许多张表需要操作,那么我们是不是要在<mappers>
标签下写多个<mapper>
标签呢?其实不用,还有第三种加载mapper的方法,使用<package>
标签
<mappers>
<package name="com.yogurt.mapper"/>
</mappers>
123
这样就会自动加载com.yogurt.mapper
包下的所有mapper,这种方式需要将mapper接口文件和mapper.xml文件都放在com.yogurt.mapper
包下,且接口文件和xml文件的文件名要一致。注意,在IDEA的maven开发环境下,maven中还需配置<resources>
标签,否则maven打包不会将java源码目录下的xml文件打包进去,见下文
三种加载mapper的方式总结
-
<mapper resource="" />
加载普通的xml文件,传入xml的相对路径(相对于类路径)
-
<mapper class="" />
使用mapper接口的全限定名来加载,若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一个目录
-
<package name="" />
扫描指定包下的所有mapper,若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一目录
注意:用后两种方式加载mapper接口和mapper.xml映射文件时,可能会报错
仔细检查了一下,mapper接口文件和xml映射文件确实放在了同一个目录下,而且文件名一致,xml映射文件的namespace也和mapper接口的全限定名对的上。为什么会这样呢?
其实是因为,对于src/main/java
源码目录下的文件,maven打包时只会将该目录下的java文件打包,而其他类型的文件都不会被打包进去,去工程目录的target目录下看看maven构建后生成的文件
我们需要在pom.xml中的<build>
标签下 添加<resources>
标签,指定打包时要将xml文件打包进去
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
12345678910
此时再用maven进行打包,看到对应目录下有了xml映射文件(特别注意,这里配置了pom.xml
下的resource
标签后,可能会引发一些问题,例如原本src/main/resources
资源目录下的文件没有被打包进来,参考我的这篇文章maven打包时的资源文件问题)
此时再运行单元测试,就能正常得到结果了
应用场景
主键返回
通常我们会将数据库表的主键id设为自增。在插入一条记录时,我们不设置其主键id,而让数据库自动生成该条记录的主键id,那么在插入一条记录后,如何得到数据库自动生成的这条记录的主键id呢?有两种方式
-
使用
useGeneratedKeys
和keyProperty
属性<insert id="insert" parameterType="com.yogurt.po.Student" useGeneratedKeys="true" keyProperty="id"> INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender}); </insert> 123
-
使用
<selectKey>
子标签<insert id="insert" parameterType="com.yogurt.po.Student"> INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender}); <selectKey keyProperty="id" order="AFTER" resultType="int" > SELECT LAST_INSERT_ID(); </selectKey> </insert> 123456
如果使用的是mysql这样的支持自增主键的数据库,可以简单的使用第一种方式;对于不支持自增主键的数据库,如oracle,则没有主键返回这一概念,而需要在插入之前先生成一个主键。此时可以用
<selectKey>
标签,设置其order
属性为BEFORE
,并在标签体内写上生成主键的SQL语句,这样在插入之前,会先处理<selectKey>
,生成主键,再执行真正的插入操作。<selectKey>
标签其实就是一条SQL,这条SQL的执行,可以放在主SQL执行之前或之后,并且会将其执行得到的结果封装到入参的Java对象的指定属性上。注意<selectKey>
子标签只能用在<insert>
和<update>
标签中。上面的LAST_INSERT_ID()
实际上是MySQL提供的一个函数,可以用来获取最近插入或更新的记录的主键id。
测试代码如下
public class MapperProxyTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
}
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = new Student(-1, "Podman", 130, 15, 0);
mapper.insert(student);
sqlSession.commit();
System.out.println(student.getId());
}
}
12345678910111213141516171819
结果如下
批量查询
主要是动态SQL标签的使用,注意如果parameterType
是List
的话,则在标签体内引用这个List
,只能用变量名list
,如果parameterType
是数组,则只能用变量名array
<select id="batchFind" resultType="student" parameterType="java.util.List">
SELECT * FROM student
<where>
<if test="list != null and list.size() > 0">
AND id in
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
</select>
1234567891011
@Test
public void testBatchQuery() {
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.batchFind(Arrays.asList(1, 2, 3, 7, 9));
students.forEach(System.out::println);
}
1234567
结果
动态SQL
可以根据具体的参数条件,来对SQL语句进行动态拼接。
比如在以前的开发中,由于不确定查询参数是否存在,许多人会使用类似于where 1 = 1
来作为前缀,然后后面用AND
拼接要查询的参数,这样,就算要查询的参数为空,也能够正确执行查询,如果不加1 = 1
,则如果查询参数为空,SQL语句就会变成SELECT * FROM student where
,SQL不合法。
mybatis里的动态标签主要有
-
if
<!-- 示例 --> <select id="find" resultType="student" parameterType="student"> SELECT * FROM student WHERE age >= 18 <if test="name != null and name != ''"> AND name like '%${name}%' </if> </select> 1234567
当满足test条件时,才会将
<if>
标签内的SQL语句拼接上去 -
choose
<!-- choose 和 when , otherwise 是配套标签 类似于java中的switch,只会选中满足条件的一个 --> <select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose> </select> 123456789101112131415161718
-
trim
-
where
<where>
标签只会在至少有一个子元素返回了SQL语句时,才会向SQL语句中添加WHERE,并且如果WHERE之后是以AND或OR开头,会自动将其删掉<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG <where> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </where> </select> 123456789101112131415
<where>
标签可以用<trim>
标签代替<trim prefix="WHERE" prefixOverrides="AND | OR"> ... </trim> 123
-
set
在至少有一个子元素返回了SQL语句时,才会向SQL语句中添加SET,并且如果SET之后是以
,
开头的话,会自动将其删掉<set>
标签相当于如下的<trim>
标签<trim prefix="SET" prefixOverrides=","> ... </trim> 123
可以通过
<trim>
标签更加灵活地对SQL进行定制实际上在mybatis源码,也能看到trim与set,where标签的父子关系
-
-
foreach
用来做迭代拼接的,通常会与SQL语句中的
IN
查询条件结合使用,注意,到parameterType为List(链表)或者Array(数组),后面在引用时,参数名必须为list或者array。如在foreach标签中,collection属性则为需要迭代的集合,由于入参是个List,所以参数名必须为list<select id="batchFind" resultType="student" parameterType="list"> SELECT * FROM student WHERE id in <foreach collection="list" item="item" open="(" separator="," close=")"> #{item} </foreach> </select> 123456
-
sql
可将重复的SQL片段提取出来,然后在需要的地方,使用
<include>
标签进行引用<select id="findUser" parameterType="user" resultType="user"> SELECT * FROM user <include refid="whereClause"/> </select> <sql id="whereClause"> <where> <if test user != null> AND username like '%${user.name}%' </if> </where> </sql> 123456789101112
-
bind
mybatis的动态SQL都是用OGNL表达式进行解析的,如果需要创建OGNL表达式以外的变量,可以用bind标签
<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern} </select> 12345
缓存
-
一级缓存
默认开启,同一个SqlSesion级别共享的缓存,在一个SqlSession的生命周期内,执行2次相同的SQL查询,则第二次SQL查询会直接取缓存的数据,而不走数据库,当然,若第一次和第二次相同的SQL查询之间,执行了DML(INSERT/UPDATE/DELETE),则一级缓存会被清空,第二次查询相同SQL仍然会走数据库
一级缓存在下面情况会被清除
- 在同一个SqlSession下执行增删改操作时(不必提交),会清除一级缓存
- SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存
- 对mapper.xml中的某个CRUD标签,设置属性
flushCache=true
,这样会导致该MappedStatement的一级缓存,二级缓存都失效(一个CRUD标签在mybatis中会被封装成一个MappedStatement) - 在全局配置文件中设置
<setting name="localCacheScope" value="STATEMENT"/>
,这样会使一级缓存失效,二级缓存不受影响
-
二级缓存
默认关闭,可通过全局配置文件中的
<settings name="cacheEnabled" value="true"/>
开启二级缓存总开关,然后在某个具体的mapper.xml中增加<cache />
,即开启了该mapper.xml的二级缓存。二级缓存是mapper级别的缓存,粒度比一级缓存大,多个SqlSession可以共享同一个mapper的二级缓存。注意开启二级缓存后,SqlSession需要提交,查询的数据才会被刷新到二级缓存当中
缓存的详细分析可以参考我之前的文章 => 极简mybatis缓存
关联查询
使用<resultMap>
标签以及<association>
和<collection>
子标签,进行关联查询,比较简单,不多说
延迟加载
延迟加载是结合关联查询进行应用的。也就是说,只在<association>
和<collection>
标签上起作用
对于关联查询,若不采用延迟加载策略,而是一次性将关联的从信息都查询出来,则在主信息比较多的情况下,会产生N+1问题,导致性能降低。比如用户信息和订单信息是一对多的关系,在查询用户信息时,设置了关联查询订单信息,如不采用延迟加载策略,假设共有100个用户,则我们查这100个用户的基本信息只需要一次SQL查询
select * from user;
1
若开启了关联查询,且不是延迟加载,则对于这100个用户,会发出100条SQL去查用户对应的订单信息,这样会造成不必要的性能开销(其实我认为称之为1+N问题更为合适)
select * from orders where u_id = 1;
select * from orders where u_id = 2;
....
select * from orders where u_id = 100;
1234
当我们可能只关心id=3的用户的订单信息,则很多的关联信息是无用的,于是,采用延迟加载策略,可以按需加载从信息,在需要某个主信息对应的从信息时,再发送SQL去执行查询,而不是一次性全部查出来,这样能很好的提升性能。
另外,针对N+1问题,除了采用延迟加载的策略按需进行关联查询。如果在某些场景下,确实需要查询所有主信息关联的从信息。在上面的例子中,就是如果确实需要把这100个用户关联的订单信息全部查询出来,那怎么办呢?这里提供2个解决思路。
1是采用连接查询,只使用1条SQL即可,如下
select * from user as u left join orders as o on u.id = o.u_id;
1
但使用连接查询查出来的结果是两表的笛卡尔积,还需要自行进行数据的分组处理
2是使用两个步骤来完成,先执行一条SQL,查出全部的用户信息,并把用户的id放在一个集合中,然后第二条SQL采用IN
关键字查询即可。这种方式也可以简化为子查询,如下
select * from orders where u_id in (select id from user);
1
现在说回来,mybatis的延迟加载默认是关闭的,可以通过全局配置文件中的<setting name="lazyLoadingEnabled" value="true"/>
来开启,开启后,所有的SELECT查询,若有关联对象,都会采用延迟加载的策略。当然,也可以对指定的某个CRUD标签单独禁用延迟加载策略,通过设置SELECT标签中的fetchType=eager
,则可以关闭该标签的延迟加载。
(还有一个侵入式延迟加载的概念,在配置文件中通过<setting name="aggressiveLazyLoading" value="true">
来开启,大概是说,访问主对象中的主信息时,就会触发延迟加载,将从信息查询上来,这其实并不是真正意义的延迟加载,真正意义上的延迟加载应该是访问主对象中的从信息时,才触发延迟加载,去加载从信息,侵入式延迟加载默认是关闭的,一般情况下可以不用管他)
注意,延迟加载在关联查询的场景下才有意义。需要配合<resultMap>
标签下的<association>
和<collecction>
标签使用
<!-- StudentMapper.xml -->
<resultMap id="studentExt" type="com.yogurt.po.StudentExt">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="score" column="score"/>
<result property="age" column="age"/>
<result property="gender" column="gender"/>
<!-- 当延迟加载总开关开启时,resultMap下的association和collection标签中,若通过select属性指定嵌套查询的SQL,则其fetchType默认是lazy的,当在延迟加载总开关开启时,需要对个别的关联查询禁用延迟加载时,才有必要配置fetchType = eager -->
<!--
column用于指定用于关联查询的列
property用于指定要封装到StudentExt中的哪个属性
javaType用于指定关联查询得到的对象
select用于指定关联查询时,调用的是哪一个DQL
-->
<association property="clazz" javaType="com.yogurt.po.Clazz" column="class_id"
select="com.yogurt.mapper.ClassMapper.findById" fetchType="lazy"/>
</resultMap>
<select id="findLazy" parameterType="string" resultMap="studentExt">
SELECT * FROM student WHERE name like '%${value}%';
</select>
12345678910111213141516171819202122
<!-- com.yogurt.mapper.ClassMapper -->
<select id="findById" parameterType="int" resultType="com.yogurt.po.Clazz">
SELECT * FROM class WHERE id = #{id}
</select>
1234
/** 用于封装关联查询的对象 **/
public class StudentExt{
private Integer id;
private String name;
private Integer score;
private Integer age;
private Integer gender;
/** 关联对象 **/
private Clazz clazz;
//getter/setter
}
123456789101112131415161718
逆向工程
mybatis官方提供了mapper自动生成工具mybatis-generator-core来针对单表,生成PO类,以及Mapper接口和mapper.xml映射文件。针对单表,可以不需要再手动编写xml配置文件和mapper接口文件了,非常方便。美中不足的是它不支持生成关联查询。一般做关联查询,就自己单独写SQL就好了。
基于IDEA的mybatis逆向工程操作步骤如下
-
配置maven插件
<build> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <!-- 输出日志 --> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> </plugin> </plugins> </build> 1234567891011121314
-
在resources目录下创建名为generatorConfig.xml的配置文件
-
配置文件的模板如下
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!--导入属性配置--> <properties resource="properties/xx.properties"></properties> <!-- 指定数据库驱动的jdbc驱动jar包的位置 --> <classPathEntry location="C:\Users\Vergi\.m2\repository\mysql\mysql-connector-java\8.0.11\mysql-connector-java-8.0.11.jar" /> <!-- context 是逆向工程的主要配置信息 --> <!-- id:起个名字 --> <!-- targetRuntime:设置生成的文件适用于那个 mybatis 版本 --> <context id="default" targetRuntime="MyBatis3"> <!--optional,旨在创建class时,对注释进行控制--> <commentGenerator> <property name="suppressDate" value="true" /> <!-- 是否去除自动生成的注释 true:是 : false:否 --> <property name="suppressAllComments" value="true" /> </commentGenerator> <!--jdbc的数据库连接--> <jdbcConnection driverClass="${db.driver}" connectionURL="${db.url}" userId="${db.user}" password="${db.password}"> </jdbcConnection> <!--非必须,类型处理器,在数据库类型和java类型之间的转换控制--> <javaTypeResolver> <!-- 默认情况下数据库中的 decimal,bigInt 在 Java 对应是 sql 下的 BigDecimal 类 --> <!-- 不是 double 和 long 类型 --> <!-- 使用常用的基本类型代替 sql 包下的引用类型 --> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <!-- targetPackage:生成的实体类所在的包 --> <!-- targetProject:生成的实体类所在的硬盘位置 --> <javaModelGenerator targetPackage="mybatis.generator.model" targetProject=".\src\main\java"> <!-- 是否允许子包 --> <property name="enableSubPackages" value="false" /> <!-- 是否清理从数据库中查询出的字符串左右两边的空白字符 --> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- targetPackage 和 targetProject:生成的 mapper.xml 文件的包和位置 --> <sqlMapGenerator targetPackage="mybatis.generator.mappers" targetProject=".\src\main\resources"> <!-- 针对数据库的一个配置,是否把 schema 作为字包名 --> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <!-- targetPackage 和 targetProject:生成的 mapper接口文件的包和位置 --> <javaClientGenerator type="XMLMAPPER" targetPackage="mybatis.generator.dao" targetProject=".\src\main\java"> <!-- 针对 oracle 数据库的一个配置,是否把 schema 作为子包名 --> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <!-- 这里指定要生成的表 --> <table tableName="student"/> <table tableName="product"/> </context> </generatorConfiguration> 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
-
双击执行mybatis-generator的maven插件
执行日志如下
生成的文件如下
能看到mybatis-generator除了给我们生成了基本的PO类(上图的Student和Product),还额外生成了Example类。Example类是为了方便执行SQL时传递查询条件的。使用的示例如下
public class GeneratorTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("mysql8-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
}
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
StudentExample example = new StudentExample();
StudentExample.Criteria criteria = example.createCriteria();
criteria.andNameLike("%o%");
List<Student> students = mapper.selectByExample(example);
students.forEach(System.out::println);
}
}
123456789101112131415161718192021
结果如下
PageHelper分页插件
使用该插件,快速实现查询结果的分页,使用步骤如下
-
pom.xml中配置依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.6</version> </dependency> 12345
-
mybatis全局配置文件中配置
<plugin>
标签<?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> <properties resource="properties/xx.properties"></properties> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"/> <property name="username" value="${db.user}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <mappers> <package name="mybatis.generator.dao"/> </mappers> </configuration> 123456789101112131415161718192021222324252627282930
-
在执行查询之前,先设置分页信息
// 查询第一页,每页3条信息 PageHelper.startPage(1,3); 12
先看一下查所有数据
@Test public void test() { SqlSession sqlSession = sqlSessionFactory.openSession(); ProductMapper mapper = sqlSession.getMapper(ProductMapper.class); //PageHelper.startPage(1,3); List<Product> products = mapper.selectByExample(new ProductExample()); products.forEach(System.out::println); } 12345678
加上PageHelper分页
@Test public void test() { SqlSession sqlSession = sqlSessionFactory.openSession(); ProductMapper mapper = sqlSession.getMapper(ProductMapper.class); PageHelper.startPage(1,3); List<Product> products = mapper.selectByExample(new ProductExample()); products.forEach(System.out::println); } 12345678
特别注意:在编写mapper.xml的时候,SQL语句的结尾不要带上;
,因为PageHelper插件是在SQL末尾拼接LIMIT
关键字来进行分页的,若SQL语句带上了;
,就会造成SQL语法错误
另外,PageHelper会先查询总数量,然后再发出分页查询,打开mybatis的日志时,可以看到发出了2条SQL
当开启PageHelper时,查询得到的List
实际是PageHelper中自定义的一个类Page
,这个类实现了List
接口,并封装了分页的相关信息(总页数,当前页码等)。
可以通过PageInfo
来获取分页的相关信息,代码如下@Test public void test() { SqlSession sqlSession = factory.openSession(); PageHelper.startPage(1,3); ProductMapper mapper = sqlSession.getMapper(ProductMapper.class); List<Product> list = mapper.findAll(); list.forEach(System.out::println); PageInfo<Product> pageInfo = new PageInfo<>(list); System.out.println(pageInfo.getTotal()); // 获得总数 System.out.println(pageInfo.getPageSize()); // 获得总页数 } 1234567891011
PageHelper插件的源码分析可以查看我之前的文章 =>
极简PageHelper源码分析
Mybatis Plus
mybatis虽然非常方便,但也需要编写大量的SQL语句,于是mybatis plus就应运而生了。它是一个mybatis增强工具,为了简化开发,提高效率。搭配Spring-Boot食用简直不要太爽。
可以参考我的这篇文章 mybatis-plus一发入魂 ,或者mybatis-plus官网,以及慕课网的入门教程和进阶教程
(完)
注:该文是一篇较为全面详细的笔记,内容篇幅很长。当对mybatis的使用较为熟练后,可以查看这篇极为简短的 mybatis精髓总结,从整体架构和源码层面上把握mybatis。