MyBatis 笔记
MyBatis 是一款持久层框架(ORM 编程思想)
MyBatis 免除了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程;
MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的实体类(pojo:Plain Old Java Object 简单的 Java 对象)映射成数据库中的记录;
MyBatis 前身为 iBatis(经常在控制台看见);
Mybatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html
持久化是将程序数据在持久状态和瞬时状态间转换的机制
通俗的讲,就是瞬时数据(比如内存中的数据,是不能永久保存的) 持久化为 持久数据(比如持久化至数据库中,能够长久保存)
即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是 将内存中的对象存储到数据库中 ,或存储在磁盘、XML 等文件中;
JDBC就是一种持久化机制。文件IO也是一种持久化机制。
由于内存的存储特性,将数据从内存中持久化至数据库,从存储成本(容量、保存时长)看,都是必要的。
即数据访问层(DAL 层),其功能主要是负责 数据库 的访问,实现对数据表的 CEUD 等操作。
持久层的作用是将输入库中的数据映射成对象,则我们可以直接通过操作对象来操作数据库 ,而对象如何和数据库发生关系,那就是框架的事情了。
使用环境:
jdk8
MySql 5.7
Maven 3.6.3
IEDA
项目结构:
配置 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> <!--父工程 可以用idea根目录作为工程目录,也可以其为父工程,每个Module作为工程目录 优点:父工程配置一次依赖,所有子工程受用 --> <groupId>org.example</groupId> <artifactId>Mybatis</artifactId> <version>1.0-SNAPSHOT</version> <modules> <module>mybatis_01</module> </modules> <!-- 设置打包格式--> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> </project>
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,可通过SqlSessionFactoryBuilder 对象获得,而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
第一步:新建 MyBatis 的核心配置文件 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="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <!-- 每一个Mapper.xml都要在项目的核心配置文件中注册映射--> <mappers> <mapper resource="yh/dao/UserMapper.xml"/> </mappers> </configuration>
第二步:编写获取 SqlSession 对象的工具类
package yh.utils; 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; /** * 获取SqlSession对象的工具类 * * @author YH * @create 2020-05-13 11:01 */ public class MybatisUtils { //1.获取SqlSessionFactory对象 private static SqlSessionFactory sqlSessionFactory; static { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } /** * 2.获取SqlSession对象 * * @return */ public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }
第三步:创建对应数据表的实体类
package yh.pojo; /** * 实体类 * pojo:简单的Java对象(Plain Old Java Object) * * @author YH * @create 2020-05-13 11:36 */ public class User { private int id; private String name; private String pwd; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public User(int id, String name, String pwd) { this.id = id; this.name = name; this.pwd = pwd; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '/'' + ", pwd='" + pwd + '/'' + '}'; } }
第四步:创建 Mapper 接口
package yh.dao; import yh.pojo.User; import java.util.List; /** * 持久层接口 * 在MyBatis中用Mapper替换原来的Dao * * @author YH * @create 2020-05-13 11:35 */ public interface IUserMapper { /** * 查询所有 * * @return */ List<User> selectUser(); /** * 根据id查用户 * @param id * @return */ User selectUserById(int id); /** * 修改用户信息 * @param user */ int updateUser(User user); /** * 删除用户 * @param id */ int deleteUser(int id); int addUser(User user); }
第五步:创建 Mapper.xml 文件(以前是实现类,显示是实现一个 source)
<?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="yh.dao.IUserMapper"> <!--select标签:表名是要执行查询操作,内部填写SQL语句 id属性:指定接口中定义的方法 resultType属性:指定实体类全类名 这一对标签就像对应接口中的一个方法--> <select id="selectUser" resultType="yh.pojo.User"> select * from mybatis.user </select> <!-- #{} 就像以前的通配符,里面的id就是形参变量 parameterType:设置参数类型--> <select id="selectUserById" parameterType="int" resultType="yh.pojo.User"> select * from mybatis.user where id=#{id} </select> <update id="updateUser" parameterType="yh.pojo.User"> update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id} </update> <delete id="deleteUser" parameterType="int"> delete from mybatis.user where id=#{id} </delete> <insert id="addUser" parameterType="yh.pojo.User"> insert into mybatis.user values(#{id},#{name},#{pwd}); </insert> </mapper>
每一个 Mapper.xml 都需要在 mybatis 核心配置文件中注册
第五步:编写测试类
按照规范,test 目录下测试类的结构要与 java 代码结构对应
package yh.dao; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import yh.pojo.User; import yh.utils.MybatisUtils; import java.util.List; /** * 测试类 * * @author YH * @create 2020-05-13 12:13 */ public class UserMapperTest { @Test public void selectUser() { //1.获取SqlSession对象(用来执行SQL,像以前用的ProperStatement) SqlSession session = MybatisUtils.getSqlSession(); //通过反射从获取对象 IUserMapper mapper = session.getMapper(IUserMapper.class); //调用实例方法 List<User> users = mapper.selectUser(); for (User u : users) { System.out.println(u); } session.close(); } @Test public void selectUserById(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); User user = mapper.selectUserById(2); System.out.println(user); session.close(); } @Test public void updateUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User user = new User(2, "熊大", "123"); int i = mapper.updateUser(user); if(i > 0){ System.out.println("修改成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } @Test public void deleteUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); int i = mapper.deleteUser(3); if(i > 0){ System.out.println("删除成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } @Test public void insterUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User user = new User(5, "熊二", "321"); int i = mapper.addUser(user); if(i > 0){ System.out.println("插入成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } }
查询所有结果:
数据表数据:
MyBatis 应用的增删增删改操作需要提交事务,传统 JDBC 的增删改操操作中,连接对象被创建时,默认自动提交事务,在执行成功关闭数据库连接时,数据就会自动提交。
用 map 集合来保存执行 SQL 所需的参数,多个参数时也可用 Map 或使用注解。
应用场景:假如通过 new 对象作为参数,调用修改方法,new一个对象需要填上构造器的所有参数,而我们可能只需要用到其中一个,就很麻烦,而使用 map 可制造任意参数,key 为参数名,value 为参数值:
直接传值 如 ...method(int id)
,可以直接在 sql 中取 id
;
对象作为参数传递 如 ...method(User user)
,直接在 sql 中去对象的属性即可;
Map 作为参数 如 ...method(Map<String,Object> map)
,直接在 sql 中取出 key 即可
原因:Maven 会对静态资源过滤,即在 java 目录下的非 java 代码都不编译
解决:在 pom.xml 中配置resources:
<!-- build中配置resources标签,针对无法找到java目录下的资源问题--> <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>
Mapper.xml 没有在核心配置文件(mybatis-config.xml 中)注册,解决添加如下配置:
<mappers> <mapper resource="yh/dao/UserMapper.xml"/> </mappers>
其他问题往往是出现在与数据库连接的配置上,如 url 配置等
补充:
模糊查询写法:
在 Java 代码层面,传参数值的时候写通配符 % %
List<User> users = mapper.getUserLike("%李%");
在 sql 拼接中使用通配符
select * from mybatis.user where name like "%"#{value}"%"
MyBatis 核心配置文件顶层结构:
添加对应元素时需要按照顺序来(如配置 properties 元素,要将其放在最上面);
我们在配置数据源(DataSource)的时候设置的 driver、url、username 等都可以使用配置属性的形式获取(这样数据源就可以以一个固定模板呈现,数据源修改是方便些)。设置:
<properties resource="db.properties"> <!-- 同时可以在内部设置属性(隐私性更好)--> <property name="username" value="root"/> <property name="password" value="root"/> </properties>
读取外部可动态改变的 properties文件,不用修改主配置类就可以实现动态配置
Mybatis读取配置的顺序:
先读取properties元素体内属性;再读取resources/url属性中指定属性文件;最后读取作为方法参数传递的属性;**
读取到同名属性时,先读取到的倍后读取到的覆盖;
所以,这几个位置读取属性的 优先级 :
作为方法参数传递的属性 > resources/url属性中指定属性文件 > properties元素体内属性
通过标识被读取的数据,DataSource中可以直接通过name引用,如下:
<dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource>
配置 JDBC 连接所需参数值的都用变量代替了。
别名使用数据库都了解过,字符段太长了可使用别名代替;同理,Mybatis 中尝尝应用类的全限定类名(全类名),往往都很长,可以使用别名的形式让我们引用全类名更加便利:
<typeAliases> <!-- 给指定全类名的类其别名--> <typeAlias type="yh.pojo.User" alias="User"/> <!-- 给指定包下所有的JavaBean起别名,别名为其类名首字母小写-> <package name="yh.dao"/> </typeAliases>
配置后,使用全限定类名的地方都可以用别名替换(如, User
可以使用在任何使用 yh.pojo.User
的地方。
注意:给包起别名,默认别名是包下JavaBean的首字母小写,如果这个JavaBean有注解的话,则别名为其注解值,如下:
@Alias("User1") public class User { ... }
Mybatis 中给常见的 Java 类型内建了别名,整体规律:基本类型别名为在前面加一个下划线 '_';而包装类的别名为其对应的基本类型名(也就是说我们使用 int 作为 resultType 参数值时,实际使用的是 Integer,所以我们设置方法参数时,最好也是用 Integer,没报错应该是自动装箱的功劳)。
有四种方式:
<mappers> <!-- 指定相对于类路径的资源引用(推荐使用)--> <mapper resource="yh/dao/UserMapper.xml"/> <!-- 使用完全限定资源定位符(URL) --> <mapper url="file:///var/mapper/UserMapper.xml"/> <!-- 使用映射器接口实现类的完全限定类名 前提:接口名与xml文件名名相同--> <mapper class="yh.dao.IUserMapper"/> <!-- 将包内的映射器接口实现全部注册为映射器 前提:接口名与xml文件名名相同--> <package name="yh.dao"/> </mappers>
这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了.
作用域和生命周期类别至关重要,因为错误的使用会导致非常严重的 并发问题 。
一次完整的生命周期:
每个线程都有独有的 SqlSession 实例,其线程时不安全的,因此不能被共享(多例)
作用域:请求或方法作用域
如在 web 中,一次请求就打开一个 SqlSession,返回一个响应后,就关闭它,如:
try (SqlSession session = sqlSessionFactory.openSession()) { // 你的应用逻辑代码 }
cache cache-ref resultMap sql insert update delete select
解决列名与 JavaBean 属性名不匹配问题。
ResultMap 设计思想:对简单语句做到 零配置 ,对复杂语句,只需要 描述语句间的关系 就行了。
如前面提到的万能 Map 将列映射到 HashMap
的键上,由 resultType
属性指定以及直接映射到对象(即映射到 ResultSet ,如: resultType="User"
)这些都是简单的映射, MyBatis 底层会自动创建一个 ResultMap
,再根据属性名来映射列到 JavaBean 的属性上 ;
通过 SQL 语句设置别名也可以实现匹配
方式二:描述语句间的关系
Mapper.xml
文件的 mapper
标签内显示配置 resultMap
: <!--id属性:此resultMap的标识,供引用语句指定 type属性:映射JavaBean全类名(可用别名)--> <resultMap id="userResultMap" type="yh.pojo.User"> <result column="id" property="id"/> <result column="name" property="name"/> <!-- 以上两条是匹配的可以省略。就写不匹配的那个属性,如下--> <result column="password" property="pwd"/> </resultMap>
resultMap
属性即可: <select id="selectUsers" resultMap="userResultMap"> select id, name, password from user where id = #{id} </select>
MyBatis 核心配置文件中的 settings
元素的 logImpl
属性用于 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
参数值:
SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
在核心文件 mybatis-config.xml 中进行如下配置:
<settings> <!--标准的日志工程实现--> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>
标准日志输出(STDOUT_LOGGING)测试结果:
Java 日志框架,通过它可以控制日志信息输送的目的地为控制台、文件还是 GUI 组件等;
可以控制每一条日志的输出格式;
通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程;
通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
配置:
添加依赖
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
resource
资源目录下新建 log4j.properties 文件
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码 log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./log/yh.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
配置 log4j 为日志的实现
<settings> <!-- log4j日志实现--> <setting name="logImpl" value="LOG4J"/> </settings>
测试结果:
在要使用 Log4j 的类中,导入包 org.apache.log4j.Logger
然后获取日志对象,参数为当前类的 class,如下:
static Logger logger = Logger.getLogger(UserMapperTest.class);
获取对象后,在要使用它的方法中通过日志对象根据需要的 日志级别 调用对应方法,参数为需要提示的信息(像以前使用 print 输出提示),如下:
logger.info("我是info级别的日志消息"); logger.debug("我是debug级别的日志消息"); logger.error("我是error级别的日志消息");
测试结果:
控制台输出:
输出到 file 文件中:
分页核心由 SQL 完成。
接口中定义:
/** * 查询结果集分页 * @param map * @return */ List<User> selectUserLimit(Map<String,Integer> map);
Mapper.xml
<select id="selectUserLimit" parameterType="map" resultMap="userResultMap"> select * from mybatis.user limit #{index},#{num} </select>
测试
@Test public void testLimit(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); HashMap<String,Integer> map = new HashMap<>(); map.put("index",1); map.put("num",2); List<User> users = mapper.selectUserLimit(map); for(User u : users){ System.out.println(u.toString()); } session.close(); }
结果
不使用 SQL 实现分页。
接口
/** * 查询结果集分页 * @param map * @return */ List<User> selectUserLimit(Map<String,Integer> map);
mybatis.xml
<select id="selectUserLimit" resultMap="userResultMap"> select * from mybatis.user </select>
测试
@Test public void testRowBounds(){ SqlSession session = MybatisUtils.getSqlSession(); //RowBounds实现 RowBounds rowBounds = new RowBounds(1,2); //通过java代码层面实现分页 List<User> userList = session.selectList("yh.dao.IUserMapper.selectUserLimit",null,rowBounds); //遍历输出:略... session.clise(); }
MyBatis 分页插件:PageHelper
官方文档: https://pagehelper.github.io/
public interface IUserMapper { @Select("select * from user") List<User> selectUser(); }
mappers
元素中注册绑定接口: <mappers> <mapper class="yh.dao.IUserMapper"/> </mappers>
@Test public void testAnnSelect(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); List<User> users = mapper.selectUser(); for (User user : users) { System.out.println(user.toString()); } session.close(); }
结果:
MyBatis 中,简单的 sql 语句可使用注解映射,复杂的最好用 xml 配置,否则难上加难;不要拘泥于某种方式,可两者配合着使用。
注意: MybatisUtils
工具类做了以下修改:
/** * 获取SqlSession对象 * @return */ public static SqlSession getSqlSession(){ //造对象并设置其自动提交事务 return sqlSessionFactory.openSession(true); }
编写接口,使用注解
package yh.dao; import org.apache.ibatis.annotations.*; import yh.pojo.User; import java.util.List; /** * @author YH * @create 2020-05-15 10:51 */ public interface IUserMapper { /** * 添加用户 * @param user */ @Insert("insert into user(id,name,pwd) values(#{id},#{name},#{password})") int addUser(User user); /** * 删除用户 * @param id * @return */ @Delete("delete from user where id=#{id}") int seleteUser(@Param("id") int id); /** * 修改用户 * @param user * @return */ @Update("update user set name=#{name},pwd=#{password} where id=#{id}") int updateUser(User user); /** * 查询所有 * @return */ @Select("select * from user") List<User> selectUser(); }
@Param()
注解: @Param()
而在 mybatis 核心配置文件中的 mappers
元素中注册绑定接口:
<mappers> <mapper class="yh.dao.IUserMapper"/> </mappers>
{} 与 ${}:常用前者(sql 解析时会加上" ",当成字符串解析;后者传入数据直接显示在生成的 sql 中,无法防止 SQL 注入。
测试
package yh.dao; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import yh.pojo.User; import yh.utils.MybatisUtils; import java.util.List; /** * @author YH * @create 2020-05-16 10:39 */ public class testAnn { @Test public void addUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.addUser(new User(6, "葫芦娃", "12333")); //由于工具类中设置了自动提交事务,所以这边可以省略 if (i > 0) { System.out.println("增加成功"); } session.close(); } @Test public void seleteUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.seleteUser(1); //由于工具类中设置了自动提交事务,所以这边可以省略 if (i > 0) { System.out.println("删除成功"); } session.close(); } @Test public void updateUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.updateUser(new User(4, "奥特曼", "11111111")); if(i > 0){ System.out.println("修改成功"); } session.close(); } @Test public void testAnnSelect(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); List<User> users = mapper.selectUser(); for (User user : users) { System.out.println(user.toString()); } session.close(); } }
如多个学生被一个老师教,对学生而言是多对一的关系,那么怎么对它们进行查询呢?
如何通过查询学生表,同时获取到他的老师的信息?
单纯用 sql 语句的话,连接查询、子查询很简单就可以实现,但是要怎么在 mybatis 中实现呢?两种方式:
多对一情况下,两个 JavaBean 的属性:
public class Student { private int id; private String name; /** * 多个学生同一个老师,即多对一 */ private Teacher teacher; //略... }
public class Teacher { private int id; private String name; //略... }
先查询出所有学生的信息,根据查询出来的tid(外键),寻找对应的老师。具体实现:
学生接口的 Mapper.xml:
<mapper namespace="yh.dao.IStudentMapper"> <select id="selectStudents" resultMap="StudentResultMap"> select * from mybatis.student </select> <resultMap id="StudentResultMap" type="yh.pojo.Student"> <result property="id" column="id"/> <result property="name" column="name"/> <!--上面的语句也可以省略(JavaBean的属性名和表的字段名可以匹配) 复杂的属性需要单独处理: 处理对象:association 处理集合:collection --> <association property="teacher" column="tid" javaType="yh.pojo.Teacher" select="selectTeachers"/> </resultMap> <select id="selectTeachers" resultType="yh.pojo.Teacher"> select * from mybatis.teacher where id=#{tid} </select> </mapper>
使用子查询实现,自然需要两次查询,关键就是如何将两次查询关联起来,这就用到 mpper
元素的子标签: association
元素, property
为实体类对应的属性, column
为表中对应的字段(外键), javaType
:查询结果对应的 JavaBean 全类名, select
关联查询语句
测试:
@Test public void selectStudent(){ SqlSession session = MybatisUtils.getSqlSession(); IStudentMapper mapperS = session.getMapper(IStudentMapper.class); List<Student> students = mapperS.selectStudents(); for (Student student : students) { System.out.println(student.toString()); } session.close(); }
结果:
方式二:按照结果嵌套处理(对连接查询的结果表进行处理)
先执行sql语句进行连接查询,获取结果(表),再通过对结果表的处理实现mybatis中多对一查询
学生接口的 Mapper.xml 配置:
<mapper namespace="yh.dao.IStudentMapper"> <select id="selectStudents2" resultMap="StudentResultMap2"> select s.id sid,s.name sname,t.id tid,t.name tname from mybatis.student s join mybatis.teacher t on s.tid=t.id </select> <!--通过上面查询的结果表进行处理--> <resultMap id="StudentResultMap2" type="yh.pojo.Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <!--这个属性为对象,所以进行复杂处理,在子标签中对该对象做相对表的映射--> <association property="teacher" javaType="yh.pojo.Teacher"> <!--注意:结果表就相当于对应的数据库表,column元素的值为结果表的字段--> <result property="id" column="tid"/> <result property="name" column="tname"/> </association> </resultMap> </mapper>
需要清楚的一个概念:
java 程序处理数据库查询时,是相对于执行 sql 查询结果所产生结果表,而不是数据库中实际的表。 根据结果表中的字段(如起别名,则结果表对应字段就为该别名),mybatis 才能进行相关的映射。
测试代码:
@Test public void selectStudent2(){ SqlSession session = MybatisUtils.getSqlSession(); IStudentMapper mapperS = session.getMapper(IStudentMapper.class); List<Student> students = mapperS.selectStudents2(); for (Student student : students) { System.out.println(student.toString()); } session.close(); }
结果:
在此说明:程序是相对于查询结果产生的表进行映射的。如果都没有查询某个字段,那么结果表中自然没有,对应的 JavaBean 实例也不能赋值。
如上例,查询语句为: select s.id sid,s.name sname,t.name tname
(查询老师 id 的参数去掉了),那么结果如下:
老师的 id 属性获取不到了(因为查询结果表中没有)。
如一个老师教了多个学生,对于老师来说就是一对多的关系。
那么如何通过一个老师,去查询它对应学生的信息呢?
一对多情况下两个 JavaBean 的属性设置:
public class Teacher { private int id; private String name; /** * 一个老师拥有多个学生,一对多 */ private List<Student> students; //略... }
public class Student { private int id; private String name; private int tid; //略... }
与多对一类似,同样的两种方式:
从 sql 查询角度看,实际采用的子查询方式,通过指定老师的编号去匹配学生信息。
老师接口的 Mapper.xml 中的配置:
<mapper namespace="yh.dao.ITeacherMapper"> <select id="selectTeacherById2" resultMap="TeacherResultMap2"> select * from mybatis.teacher where id=#{id} </select> <resultMap id="TeacherResultMap2" type="Teacher"> <id property="id" column="id"/> <collection property="students" javaType="ArrayList" ofType="Students" column="id" select="selectStudents"/> </resultMap> <select id="selectStudents" resultType="Student"> select * from mybatis.student where tid=#{id} </select> </mapper>
在查询老师信息的结果集 resultMap
元素中映射属性复杂类型(集合)时,再进行查询操作(嵌套),最终实现一对多查询。
使用连接查询获取一个带有对应学生信息结果表,从而实现映射处理。
老师接口中定义的方法:
/** * 查询指定id的老师信息,以及其学生的信息 * @param id * @return */ Teacher selectTeacherById(@Param("id") int id);
老师接口的 Mapper.xml 中的配置
<mapper namespace="yh.dao.ITeacherMapper"> <select id="selectTeacherById" parameterType="int" resultMap="TeacherResultMap"> select t.id tid,t.name tname,s.id sid,s.name sname,s.tid tid from mybatis.teacher t inner join mybatis.student s on t.id=s.tid where t.id=#{id} </select> <resultMap id="TeacherResultMap" type="Teacher"> <result property="id" column="tid"/> <result property="name" column="tname"/> <!--复杂的属性我们需要单独处理 对象:association 集合:collection javaType:用于指定所指类属性的类型 ofType:用于指定类的集合属性中的泛型类型 --> <collection property="students" ofType="Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <result property="tid" column="tid"/> </collection> </resultMap> </mapper>
一对多相对于多对一,用的集合,集合属性的映射处理需要用到 collection
元素,且指定集合泛型类型使用 ofType
属性,其他基本相同。
测试:
@Test public void selectTeacher2(){ SqlSession session = MybatisUtils.getSqlSession(); ITeacherMapper mapper = session.getMapper(ITeacherMapper.class); Teacher teacher = mapper.selectTeacherById(1); System.out.println(teacher.toString()); session.close(); }
结果:
id type
id
和 result
元素的属性: property column javaType
association
:关联 - 【多对一】
collection
:集合 -【一对多】
慢 SQL:执行效率很低的 sql 语句。
相关 MySQL 内容:MySQL 引擎、InnoDB 底层原理、索引及其优化
根据不同的条件,生成不同的 SQL 语句,在编译时无法确定,只有等程序运行起来,执行过程中才能确定的 SQL语句为 动态 SQL (在 SQL 层面执行一个逻辑代码)
数据表如下:
常用于根据判断条件包含 where 子句的一部分,条件成立,被包含的部分得以执行,反之不执行。如:
<!--使用if实现动态查询--> <select id="getBlog" parameterType="Map" resultType="Blog"> select * from mybatis.blog where 1=1 <if test="title != null"> and title=#{title} </if> <if test="author != null"> and author=#{author} </if> </select>
根据是否传入参数控制是否增加筛选条件,如下:
@Test public void test2(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, String> map = new HashMap<>(); //控制参数的有无,实现动态SQL效果 // map.put("title","Java如此简单"); map.put("author","熊大"); List<Blog> blogs = mapper.getBlog(map); for (Blog blog : blogs) { System.out.println(blog.toString()); } //未提交事务是因为在MybatisUtils工具类中开启了自动提交 session.close(); }
choose
when
otherwise
类似于 java 中的 switch
case
, otherwise
,不同的是这里判断条件设置在 when
元素中,符合其条件的,就执行其所包含的 sql 语句。如下:
<!--使用choose、when、otherwise实现动态查询--> <select id="findActiveBlogLike" parameterType="Map" resultType="Blog"> select * from mybatis.blog where 1=1 <choose> <when test="title != null"> and title like #{title} </when> <when test="author != null"> and author like #{author} </when> <otherwise> title=#{title} </otherwise> </choose> </select>
传入的 title
就按照 title
的查找,传入了 author
就按 author
查找,两者都没有就用 otherwise
元素中的,如两个元素都传入了,那个 when 元素先执行就用哪个。如此例就是执行 title
的查找,测试代码如下:
@Test public void test3(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, String> map = new HashMap<>(); //传入两个参数,也只执行先执行的那个 map.put("title","Java%"); map.put("author","熊%"); List<Blog> blogs = mapper.findActiveBlogLike(map); for (Blog blog : blogs) { System.out.println(blog.toString()); } //未提交事务是因为在MybatisUtils工具类中开启了自动提交 session.close(); }
where 元素只会在子元素返回任何内容的情况下(有符合条件的子句时)才插入 “WHERE” 子句。且,若子句的开头为 “AND” 或 “OR”, where 元素也会将它们去除。
<select id="findActiveBlogLike2" parameterType="Map" resultType="Blog"> select * from mybatis.blog # 被包含的子元素成立的情况下,插入where以及子元素包含的条件,且去除and前缀 <where> <if test="title != null"> and title like #{title} </if> <if test="author != null"> and author like #{author} </if> </where> </select>
set 元素用于动态包含需要更新的列(条件成立的列),忽略其它不更新的列(条件不成立的列)。在首行插入 set
关键字,并会删掉额外的逗号(最后一个更新字段不要逗号),如下:
<update id="updateBlog" parameterType="Map"> update mybatis.blog # 在首行插入 `set` 关键字,并会删掉额外的逗号 <set> <if test="id != null">id=#{id},</if> <if test="author != null">author=#{author},</if> <if test="createTime != null">create_time=#{createTime},</if> <if test="views != null">views=#{views}</if> </set> where title=#{title} </update>
测试代码:
@Test public void test5() { SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, Object> map = new HashMap<>(); map.put("title","Java如此简单"); map.put("author","熊大"); map.put("views","10000"); map.put("createTime",new Date()); int i = mapper.updateBlog(map); if (i > 0) { System.out.println("修改成功"); } }
trim 元素用于自定义 where、set 的功能:
prefix
属性:用于覆盖的前缀
prefixOverride
属性:被覆盖的前缀
与上面用到的 where
等价的自定义 trim
元素:
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
与上面用到的 set
等价的自定义 trim
元素:
<trim prefix="SET" suffixOverrides=","> ... </trim>
注意:我们覆盖了后缀值设置,并且自定义了前缀值。
将 SQL 语句中一些功能的部分抽取出出来,方便复用
使用 sql
标签抽取公共部分,如:
<sql id="if-title-author"> <if test="title != null"> title = #{title} </if> <if test="author != null"> and author = #{author} </if> </sql>
在需要使用的地方使用 Include
标签引用即可:
<select id="queryBlogIF" parameterType="Map" resyltType="Blog"> select * from mybatis.blog <where> <include refid="if-title-author"></include> </where> </select>
注意:1.最好基于表单来定义 SQL 片段;
2.片段中不要存在 where 标签;
用于指定一个集合进行遍历或指定开头与结尾的字符串以及集合项迭代之间的分隔符
coolection
属性:指示传递过来用于遍历的集合
item
属性:集合当前遍历出来的元素
index
属性:索引(当前迭代的序号)
open
属性:指定开始符号
close
属性:指定结束符号
separator
属性:指定分隔符
元素内包裹的是通过遍历集合参数,之间用分隔符拼接,两头拼接开始结束符,说白了就是一个 sql 语句拼接的过程,拼接的 sql 长度,取决于所传递的集合长度。示例如下:
Mapper.xml:
<!--foreach--> <select id="findBlogForeach" parameterType="Map" resultType="Blog"> select * from mybatis.blog <where> <foreach collection="ids" item="id" open="(" close=")" separator="or"> id=#{id} </foreach> </where> </select>
测试代码:
@Test public void test6(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, Object> map = new HashMap<>(); List<Integer> ids = new ArrayList<>(); //要查询哪一条主句,就将它的id放进集合中,由foreach遍历,拼接成sql语句,实现动态SQL效果 ids.add(1); ids.add(3); map.put("ids",ids); List<Blog> blogForeach = mapper.findBlogForeach(map); for (Blog foreach : blogForeach) { System.out.println(foreach.toString()); } session.close(); }
测试结果:
注意:
我们表中的 id
字段是 varchar 类型的,而我们向集合中添加的数据是 Integer
类型,但是也能作为判断条件,原因是: MySQL 会进行隐式类型转换(TypeHandler),但是需要注意,有些数据库不支持隐式转换,需要手动转换;
前面说了动态 SQL 实际就是一个拼接 SQL 的过程,我们只需按照 SQL 的格式,去排列组合就可以了,所以必要的一些空格也需要留意(新版本的 mybatis 貌似已经帮我们留意了)。
SqlSession
级别的缓存(也称为本地缓存),即从获取 SqlSession 到 SqlSession 被回收期间的缓存。默认情况下,只有有一级缓存开启。 Cache
,可以通过实现 Cache
接口来自定义二级缓存。 与数据库同一次会话(SqlSession)期间查询到的数据会放在本地缓存中;再需要获取相同数据时,直接从缓存中拿。测试如下:
在 mybatisConfig.xml 中开启日志,方便观察执行过程
<!-- 配置设置--> <settings> <!--标准的日志工程实现--> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- log4j实现--> <!-- <setting name="logImpl" value="LOG4J"/>--> </settings>
接口中定义方法
/** * 根据id查用户 * @param id * @return */ User findUserById(@Param("id") Integer id);
Mapper.xml 配置
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="yh.dao.UserMapper"> <select id="findUserById" parameterType="int" resultType="User"> select * from mybatis.user where id=#{id} </select> </mapper>
测试代码
@Test public void test1(){ SqlSession session = MybatisUtils.getSqlSession(); UserMapper mapper = session.getMapper(UserMapper.class); //同一次会话(SqlSession)中,第一次执行查询 User user1 = mapper.findUserById(2); System.out.println("user1" + user1.toString()); //同一次会话(SqlSession)中,第二次执行查询 User user2 = mapper.findUserById(2); System.out.println("user2" + user2.toString()); //比较查询获取的JavaBean实例地址是否相同(是否是同一个对象) System.out.println(user1 == user2); session.close(); }
对结果日志进行分析
从程序执行看,第二次调用查询时,没有与数据库进行交互,而两次查询所获的 JavaBean 对象实例地址比较结果为 true
,所以可断定第二次查询的数据不是从数据库获取的,而是从本地缓存(内存)获取的,这也就是一级缓存的作用。
SqlSession.clearCache();
一级缓存默认是开启的,只在一次 SQLSession 中有效,也就是获取连接到关闭连接期间;一级缓存就是一个 Map(key 记录 sql,value 记录对应的查询结果)。
二级缓存也称为全局缓存,一级缓存作用域太低了(基于 SqlSession
级别),所以诞生了二级缓存(基于 namespace
级别的缓存),整个 Mapper ,对应一个二级缓存;
mapper
查出的数据会放在自己对应的缓存中(同样是用 Map
保存, key
为 mapper
标识, value
为其二级缓存数据)。 开启全局缓存需要对 Mybatis 核心配置文件的 settings
元素中进行如下配置 :
<!--显示地开启全局缓存--> <setting name="cacheEnabled" value="true"/>
在要使用二级缓存的 SQL 映射(Mapper.xml)中添加一个标签:
<cache/>
也可以在其中自定义一些参数:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
测试代码
结果分析
给 mapper 开启二级缓存前查询结果可以清楚地看出两次从与数据库交互的过程:
而开启了 mapper 缓存后:
只与数据库进行了一次交互,但是通过添加 <cache/>
标签的方式查询时,两个查询对象的比较结果确是 false
,因为使用无参 <cache/>
标签时,未序列化就会报对象序列化异常,而序列化后对通过序列码比价对象肯定是不同的,所以结果为 false
。而使用自定义 <cache/>
标签属性时,结果为 true
:
规范:实体类定义时需要实现序列化接口。
用户进行查询,先先查询二级缓存中是否有对应缓存,有 返回查询结果,无 再去一级缓存查询,有 返回结果,无 去数据库查询;
在查询到数据返回结果时,存在一个缓存过程(将数据存入内存),从数据库查询会根据当前开启的缓存级别,将数据存入级别高的缓存中;在一级缓存中查询到结果时(说明在二级缓存中没有查到),返回数据时,一级缓存会将数据缓存进二级缓存,最后返回结果。
一级缓存提交前提是当前会话关闭,否则不会将缓存送入二级缓存。
流程图:
如果第一次会话的缓存没有提交,则第一次会话中查询的缓存都不能从二级缓存中查询出来,只有第一次缓存提交后,后续的会话查询才能使用二级缓存的结果。
在 mapper 中可通过设置 select
标签的 useCache
属性确定是否需要缓存 ;CUD 标签则通过 flush
属性指定执行后是否刷新缓存。
EhCache 是一种广泛使用的开源 Java 分布式缓存,主要面向通用缓存。
导入依赖使用,在 mapper 中指定我们使用的 ehcache 缓存实现:
<!--在当前Mapper.xml中使用耳机缓存--> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="./tmpdir/Tmp_EhCache"/> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="259200" memoryStoreEvictionPolicy="LRU"/> <cache name="cloud_user" eternal="false" maxElementsInMemory="5000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LRU"/> </ehcache>