Mybatis
JDBC操作数据库的缺点
- 存在大量的冗余代码。
- 手工创建 Connection、Statement 等,效率低下。
- 手工将结果集封装成实体对象。
- 查询效率低,没有对数据访问进行优化。
Mybatis框架
简介
MyBatis 本是 apache 的一个开源项目 iBatis, 2010年这个项目由 apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
iBatis 一词来源于 “internet” 和 “abatis” 的组合,是一个基于Java的持久层框架。iBatis 提供的持久层框架包括 SQL Maps 和 Data Access Objects(DAOs)
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 POJO(Plain Ordinary Java Objects,普通 Java 对象)为数据库中的记录。
Mybatis获取
官网:https://mybatis.org/mybatis-3/
Maven配置:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency>
使用Mybatis
工程搭建
引入依赖库:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.17</version> </dependency>
config配置文件
在resources目录下创建config.xml
1.配置JDBC环境;
2.注册Mapper。
<?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配置--> <!--常用配置标签的先后顺序 properties, settings, typeAliases, typeHandlers, plugins, environments, mappers 如果配置文件中同时存在这些配置标签,它们之间的顺序必须按照上述列表排列 --> <configuration> <!--JDBC环境配置,选中默认环境--> <environments default="dev"> <!--Mysql数据库环境配置--> <environment id="dev"> <!--事务管理,这里的JDBC是一个类的别名:org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory--> <transactionManager type="JDBC"/> <!--连接池,这里的POOLED也是一个类的别名: org.apache.ibatis.datasource.pooled.PooledDataSourceFactory--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/lesson?serverTimezone=Asia/Shanghai&tinyInt1isBit=false"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!--Mapper注册--> <mappers> <!--注册Mapper文件的所在位置--> </mappers> </configuration>
创建userMapper接口以及接口的映射文件
userMapper:
public interface UserMapper { User getUserByUsername(String username); }
userMapper.xml:
<!--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="com.qf.mybatis.mapper.UserMapper"> <!--id表示接口中的方法名,resultType表示查询结果每一行数据对应的转换类型--> <select id="getUserByUsername" resultType="com.qf.mybatis.pojo.User"> <!--#{arg0}表示获取方法参数列表中的第一个参数值--> <!--#{param1}表示获取方法参数列表中的第一个参数值--> SELECT username,password,name,sex FROM user where username=#{arg0} </select> </mapper>
注册Mapper接口
<mappers> <!--注册Mapper文件的所在位置--> <mapper resource="mapper/userMapper.xml"/> </mappers>
测试
构建SqlSessionFactory的构建者,获取配置文件信息,根据配置文件信息构建SqlSessionFactory工厂,工厂开启sqlsession会话。
以上是程序性操作
然后从会话中获得接口的代理对象,底层是动态代理。
@Test public void getUserByUserNameTest() throws IOException { //构建SqlSessionFactory的构建者 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); //获取配置文件信息 InputStream is = Resources.getResourceAsStream("config.xml"); //根据配置信息构建工厂 SqlSessionFactory factory = builder.build(is); //工厂开启sql会话 SqlSession session = factory.openSession(); //从会话中获得userMapper接口的代理对象(原理是动态代理) UserMapper userMapper = session.getMapper(UserMapper.class); //调用方法 User user = userMapper.getUserByUsername("zs"); System.out.println(user); }
properties文件配置
Mybatis支持properties文件的引入,这样做的目的就是为了区分配置:不同的文件中描述不同的配置,这样方便管理。 在 resources 目录下新建 jdbc.properties 文件:
#jdbc.properties jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/lesson?characterEncoding=utf8&tinyInt1isBit=false jdbc.username=root jdbc.password=root
,然后在 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"> <!--MyBatis配置--> <configuration> <!--引入jdbc.properties文件--> <properties resource="jdbc.properties"/> <!--JDBC环境配置,选中默认环境--> <environments default="dev"> <!--Mysql数据库环境配置--> <environment id="dev"> <!--事务管理,这里的JDBC是一个类的别名:org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory--> <transactionManager type="JDBC"/> <!--连接池,这里的POOLED也是一个类的别名: org.apache.ibatis.datasource.pooled.PooledDataSourceFactory--> <dataSource type="POOLED"> <!-- <property name="driver" value="com.mysql.cj.jdbc.Driver"/>--> <!-- <property name="url" value="jdbc:mysql://localhost:3306/lesson?serverTimezone=Asia/Shanghai&tinyInt1isBit=false"/>--> <!-- <property name="username" value="root"/>--> <!-- <property name="password" value="123456"/>--> <property name="diver" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <!--Mapper注册--> <mappers> <!--注册Mapper文件的所在位置--> <mapper resource="mapper/userMapper.xml"/> </mappers> </configuration>
类型别名
在Mapper接口映射文件userMapper.xml文件中
<!--id表示接口中的方法名,resultType表示查询结果每一行数据对应的转换类型--> <select id="getUserByUsername" resultType="com.qf.mybatis.pojo.User">
resultType属性配置很繁琐,当方法很多的时候,开发效率大大降低,因此Mybatis提供了为类型定义别名的功能。该功能需要在config.xml中配置
<!--配置类型的别名:typeAlias方式和package方式只能选择其一--> <typeAliases> <!-- <!–配置单个类的别名–>--> <!-- <typeAlias type="com.qf.mybatis.pojo.User" alias="user" />--> <!--配置需要取别名的类的包,该包中所有类的别名均为类名--> <package name="com.qf.mybatis.pojo"/> </typeAliases>
userMapper.xml:
<select id="getUserByUsername" resultType="User">
日志配置
Mybatis本身有提供日志功能,开启日志需要在
config.xml
进行配置<!-- 打印SQL语句 STDOUT_LOGGING是一个类的别名: org.apache.ibatis.logging.stdout.StdOutImpl--> <setting name="logImpl" value="STDOUT_LOGGING"/>
注:常用配置标签的先后顺序
properties,
settings,
typeAliases,
typeHandlers,
plugins,
environments,
mappers
如果配置文件中同时存在这些配置标签,它们之间的顺序必须按照上述列表排列。
Mybatis增删改查
由于每次实现方法都需要构建SqlSessionFactory的构建者,获取配置文件信息,根据配置文件信息构建SqlSessionFactory工厂,工厂开启sqlsession会话。
因此把这部分封装起来作为工具类:
FactoryUtil:
public class FactoryUtil { private static SqlSessionFactory factory; static { //构建SqlSessionFactory的构建者 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); //获取配置文件信息 InputStream is = null; try { is = Resources.getResourceAsStream("config.xml"); //根据配置信息构建工厂 factory = builder.build(is); } catch (IOException e) { throw new RuntimeException(e); } } public static SqlSession getSqlSession() { return factory.openSession(); } }
标签
<select id="getUser"/> <insert id="addUser"/> <delete id="deleteUser"/> <update id="updateUser"/>
参数取值
在Mybatis中,参数取值有两种方式:一种是#{表达式}, 另一种是 **${表达式} ** ;
#{表达式} 采用的是JDBC中的预编译来实现,因此可以防止SQL注入。
**${表达式} ** 采用的是字符串拼接,因此常用在排序字段变化、分组字段变化、查询表名变化等场景。
常用数据类型作为参数:
使用arg参数下标或者param参数位置获取参数
如:
User getUserByUsername(String username);
<select id="getUserByUsername" resultType="User"> SELECT username,password,name,sex FROM user where username=#{arg0} </select>
public void getUserByUserNameTest() throws IOException { SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = userMapper.getUserByUsername("zs"); System.out.println(user); }
实体对象作为参数
使用#{属性名}获取对象属性参数
单个对象:
接口方法:
int addUser(User user);
xml映射:
<insert id="addUser"> INSERT into user values (#{username},#{password},#{name},#{sex}) </insert>
public void addUser(){ SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user = new User(); user.setName("吉吉"); user.setSex(1); user.setUsername("jj"); user.setPassword("123456"); int i = userMapper.addUser(user); try { session.commit();//不提交事务就不会对数据库中的数据进行修改 } catch (Exception e) { session.rollback();//如果提交失败回滚事务 } System.out.println(i); }
多个对象:
int updateUserPassword(User user1,User user2);
<update id="updateUserPassword"> update user set password=#{arg0.password} where username=#{arg1.username} </update>
public void updateUser(){ SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); User user1 = new User(); User user2 = new User(); user1.setUsername("zs"); user2.setUsername("ls"); user1.setPassword("123456"); int i = userMapper.updateUserPassword(user1, user2); try { session.commit(); } catch (Exception e) { session.rollback(); } System.out.println(i); }
Map作为参数:
由于Map中存放的数据是通过键值对实现的,因此可以将Map当做一个实体类对象来看待。Map中的键就相当于实体类中的属性名,Map中的值就相当于实体类中的属性值。因此,其取值方式与实体类对象作为参数一样。
int deleteUser(Map<String,Object> params);
<delete id="deleteUser"> DELETE from user where username=#{username} and password=#{password} </delete>
public void deleteUser(){ SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); Map<String, Object> params = new HashMap<>(); params.put("username","jj"); params.put("password","123456"); int i = userMapper.deleteUser(params); try { session.commit(); } catch (Exception e) { session.rollback(); } System.out.println(i); }
参数注解
为了方便开发,Mybatis对参数提供了注解,从而可以给参数指定名称,方便在对应的Mapper映射文件中使用
List<User> retrieveUsers(@Param("condition")Map<String,Object> params);
<select id="retrieveUsers" resultType="User"> select * from user where password=#{condition.password} and sex=#{condition.sex} </select>
public void retriveUsers(){ SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); Map<String, Object> params = new HashMap<>(); params.put("password","321321"); params.put("sex",1); List<User> users = userMapper.retrieveUsers(params); try { session.commit(); } catch (Exception e) { session.rollback(); } users.forEach(System.out::println); }
主键回填
当保存一条数据时,我们需要该数据的ID,ID生成有两种方式:一种是数据库自动生成,一种是程序通过编码生成。Mybatis也提供了这两种方式来生成ID,ID生成后可以设置到给定的属性上,这个过程称之为主键回填。
一般采用数据库自动生成的ID,而不是程序编码生成的,因为程序生成的意义不大,无法从数据库中查询。
创建表:
-- 创建表 DROP TABLE IF EXISTS score; CREATE TABLE score ( -- 主键自增 id bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键', name varchar(20) NOT NULL COMMENT '姓名', score double(5,2) DEFAULT NULL COMMENT '成绩' ) ENGINE=InnoDB CHARSET=UTF8;
对应实体类:
@Data public class Score { private long id; private String name; private Double score; }
映射文件:
<?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.qf.mybatis.mapper.ScoreMapper"> <insert id="addScore"> <!-- selectKey表示选择键 通常都是用于主键回填功能 keyProperty表示回填的值设置到哪个属性上 resultType表示回填的值的数据类型 order表示主键回填的时机 AFTER表示数据保存后 BEFORE表 示数据插入之前--> <selectKey keyProperty="score.id" resultType="long" order="AFTER"> SELECT LAST_INSERT_ID() </selectKey> INSERT INTO score(name,score)VALUES(#{score.name},#{score.score}) </insert> </mapper>
注册;
<mapper resource="mapper/scoreMapper.xml"/>
测试:
public void addScore(){ SqlSession session = FactoryUtil.getSqlSession(); ScoreMapper mapper = session.getMapper(ScoreMapper.class); Score score = new Score(); score.setName("zs"); score.setScore(99.0); int i = mapper.addScore(score); try { session.commit(); } catch (Exception e) { session.rollback(); } System.out.println(i); }
关键点:
使用
SELECT LAST_INSERT_ID()
获取自动生成的主键值,并将其回填到score
对象的id
属性中
结果映射
在SQL查询时,我们经常会遇到数据库表中设计的字段名与对应的实体类中的属性名不匹配的情况,针对这种情况,Mybatis 提供了结果集映射,供用户自己实现数据库表中字段与实体类中属性进行匹配。
DROP TABLE IF EXISTS employee;
CREATE TABLE employee(
id int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '员工编号',
name varchar(30) NOT NULL COMMENT '姓名',
entry_time datetime NOT NULL COMMENT '入职时间',
leave_time datetime DEFAULT NULL COMMENT '离职时间'
) ENGINE=InnoDB CHARSET=UTF8;
// 创建实体类 员工
public class Employee {
private long id;
private String name;
private Date entryTime;
private Date leaveTime;
//省略getter和setter
//构造方法:要么无参,要么全参
}
// 创建Mapper接口
public interface EmployeeMapper {
List<Employee> getAllEmployees();
}
<?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.qf.mybatis.mapper.EmployeeMapper">
<resultMap id="empMap" type="com.qf.mybatis.model.Employee">
<id property="id" column="id" />
<result property="name" column="name" />
<!--数据表中列名与实体类中的属性名匹配-->
<result property="entryTime" column="entry_time" />
<!--数据表中列名与实体类中的属性名匹配-->
<result property="leaveTime" column="leave_time" />
</resultMap>
<select id="getAllEmployees" resultMap="empMap">
SELECT id,name,entry_time,leave_time FROM employee
</select>
</mapper>
也可以直接对表中字段重命名。
Mybatis级联查询
1. 一对一级联查询
创建签证表和乘客表,其中一个乘客有多个签证,而一个签证只对应一个乘客:
DROP TABLE IF EXISTS passenger;
CREATE TABLE passenger (
id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '乘客编号',
name varchar(50) NOT NULL COMMENT '姓名',
sex tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别',
birthday date NOT NULL COMMENT '生日'
) ENGINE=InnoDB CHARSET=UTF8;
DROP TABLE IF EXISTS passport;
CREATE TABLE passport (
id bigint NOT NULL AUTO_INCREMENT COMMENT '护照编号',
office varchar(50) NOT NULL COMMENT '签证机关',
valid_time tinyint NOT NULL COMMENT '有效期限',
nationality varchar(50) NOT NULL COMMENT '国籍',
passenger_id bigint NOT NULL COMMENT '乘客编号',
PRIMARY KEY (id),
FOREIGN KEY (passenger_id) REFERENCES passenger (id)
) ENGINE=InnoDB CHARSET=UTF8;
创建对应的实体类,属性名采用驼峰命名法:
@Data
public class Passenger {
private long id;
private String name;
private int sex;
private Date birthday;
}
//----------------------------------------------------
@Data
public class Passport {
private long id;
private String nationality;
private int validTime;
private String office;
private Passenger passenger;
}
现在要通过查询签证表的同时查询出乘客表
因此要写是PassportMapper接口,其中的方法为获取所有签证对象
public interface PassportMapper { List<Passport> getAllPassports(); }
然后写映射文件,因为是通过查询签证表查到乘客表的一对一级联,所以只用写签证表的映射文件。
方式一:
签证表的passenger字段和乘客表的id字段由一个参数passengerId进行连接,先查passport表,然后将参数作为索引查passenger表.
因此要写PassengerMapper接口,其中的方法为获取所有乘客对象。
public interface PassengerMapper { List<Passenger> getPassengers(); }
映射文件:
<?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.qf.mybatis.mapper.PassportMapper"> <resultMap id="passportMap" type="Passport"> <!--查询单张表可以只写实体类属性和数据库字段名不同的部分--> <result property="validTime" column="valid_time"/> <!--一对一的级联查询使用的是association标签--> <!--级联查询也支持传递参数,传递参数需要通过column属性来传递,定义参数的语法:{参数名=列名,...,参数名n=列名n}--> <association property="passenger" column="{passengerId = passenger_id}" select="getPassengers"/> </resultMap> <select id="getAllPassports" resultMap="passportMap"> select * from passport </select> <select id="getPassengers" resultType="Passenger"> select * from passenger where id=#{passengerId} </select> </mapper>
方式二:
将两个表连接起来,查询连接后的表:
注意:在查询复杂关系的表的时候需要在结果映射中将所有属性和数据字段名都写出来
<mapper namespace="com.qf.mybatis.mapper.PassportMapper"> <resultMap id="passportMap" type="Passport"> <id column="id" property="id" /> <result column="nationality" property="nationality" /> <result column="office" property="office" /> <result column="valid_time" property="validTime" /> <association property="passenger" javaType="passenger"> <id column="passengerId" property="id" /> <result column="name" property="name" /> <result column="sex" property="sex" /> <result column="birthday" property="birthday" /> </association> </resultMap> <select id="getAllPassports" resultMap="passportMap"> select a.id, a.nationality, a.office, a.valid_time, b.id passengerId, b.name, b.sex, b.birthday from passport a inner join passenger b on a.passenger_id = b.id </select> </mapper>
2. 一对多级联查询
association改为collection
创建班级表和学生表:
DROP TABLE IF EXISTS class;
CREATE TABLE class (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '班级编号',
name varchar(50) NOT NULL COMMENT '名称'
) ENGINE=InnoDB CHARSET=UTF8;
DROP TABLE IF EXISTS student;
CREATE TABLE student (
id bigint NOT NULL AUTO_INCREMENT COMMENT '学号',
name varchar(50) NOT NULL COMMENT '姓名',
class_id int NOT NULL COMMENT '班级编号',
PRIMARY KEY (id),
FOREIGN KEY (class_id) REFERENCES class (id)
) ENGINE=InnoDB CHARSET=UTF8;
public class Student {
private long id;
private String name;
}
public class Clazz {
private int id;
private String name;
private List<Student> students; //集合作为属性
}
public interface ClazzMapper {
List<Clazz> getClazzList();
}
方式一:查询两次
因此还需要StudentMapper接口:
public interface StudentMapper { List<Student> getStudents(); }
映射文件:
<mapper namespace="com.qf.mybatis.mapper.ClazzMapper"> <resultMap id="clazzMap" type="Clazz"> <id property="id" column="id"/> <result property="name" column="name"/> <!--一对多 级联 方式--> <collection property="students" select="getStudents" column="{sid=id}"/> </resultMap> <select id="getClazzList" resultMap="clazzMap"> select * from class </select> <select id="getStudents" resultType="Student"> select * from student where class_id = #{sid} </select> </mapper>
方式二:
查询两个表的连接表:
<mapper namespace="com.qf.mybatis.mapper.ClazzMapper"> <resultMap id="clazzMap" type="Clazz"> <id column="id" property="id"/> <result column="name" property="name"/> <collection property="students" ofType="Student"> <id column="sid" property="id"/> <result column="sname" property="name"/> </collection> </resultMap> <select id="getClazzList" resultMap="clazzMap"> select a.id, a.name, b.id sid, b.name sname from class a inner join student b on a.id=b.class_id </select> </mapper>
注意:重复的名字需要重命名。
3.RBAC权限模型查询
RBAC权限模型介绍
RBAC(Role Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联,而不是直接将权限赋予用户。
如现在有以下表:
数据表: 用户表 username varchar(50) primary key password varchar(200) name varchar(50) 角色表 id int(11) primary key auto_increment name varchar(50) 用户角色表 username varchar(50) role_id int(11) 菜单表 id int(11) primary key auto_increment name varchar(50) parent_id int(11) 角色菜单表 role_id int(11) menu_id int(11)
关系如下:
RBAC模型中,用户与角色之间、角色与权限之间,一般是多对多的关系。
现在有一个需求,根据用户查询到对应的菜单。
这里采用非级联查询和级联查询(均采用查询一次的方式)
实体类:
@Data public class User { private String username; private String password; private String name; private List<Menu> menus; } //---------------------------------- @Data public class Menu { private int id; private String name; }
非级联查询
接口方法:
List<Menu> getMenus(String username);
映射文件:
<resultMap id="menuMap" type="Menu"> <id column="mid" property="id"/> <result column="mname" property="name"/> </resultMap> <select id="getMenus" resultMap="menuMap"> select m.id mid,m.name mname from menu m join role_menu rm on m.id=rm.menu_id join roles r on rm.role_id=r.id join user_role ur on r.id=ur.role_id join user u on ur.username = u.username where u.username=#{username} </select>
测试:
@Test public void getMenusByUsername() throws IOException { SqlSession session = getSession(); UserMapper mapper = session.getMapper(UserMapper.class); List<Menu> menus = mapper.getMenus("jj"); try { session.commit(); } catch (Exception e) { session.rollback(); } menus.forEach(System.out::println); } private SqlSession getSession() throws IOException { SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); InputStream is = Resources.getResourceAsStream("config.xml"); SqlSessionFactory factory = builder.build(is); SqlSession session = factory.openSession(); return session; }
级联查询
接口方法:
List<User> getUsers();
映射文件:
<resultMap id="userMap" type="User"> <id property="username" column="username"/> <result property="password" column="password"/> <result property="name" column="uname"/> <result property="sex" column="sex"/> <collection property="menus" ofType="Menu"> <id property="id" column="id"/> <result property="name" column="mname"/> <result property="parentId" column="parent_id"/> </collection> </resultMap> <select id="getUsers" resultMap="userMap"> select u.username,u.password,u.name uname,m.id,m.name mname from user u left join user_role ur on u.username = ur.username left join roles r on ur.role_id = r.id left join role_menu rm on r.id = rm.role_id left join menu m on rm.menu_id = m.parent_id </select>
测试:
@Test public void getMenusByUser() throws IOException { SqlSession session = getSession(); UserMapper mapper = session.getMapper(UserMapper.class); List<User> users = mapper.getUsers(); try { session.commit(); } catch (Exception e) { session.rollback(); } users.forEach(System.out::println); } private SqlSession getSession() throws IOException { SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); InputStream is = Resources.getResourceAsStream("config.xml"); SqlSessionFactory factory = builder.build(is); SqlSession session = factory.openSession(); return session; }
级联查询与非级联查询的区别
级联查询查到的结果是包含其他类的集合作为属性的类,这里就是User,User中含有menu的集合属性,因此sql查询中查询的目标含有user表外的其他表的字段内容,且这些表之间有连接关系。而非级联查询只能查询当前表中的内容,返回的是查询对象的类。
动态SQL
sql标签
将特定的SQL代码封装起来,方便进行重用
<!--多条SQL都会使用的字段可以使用sql标签来定义,使用时通过include标签来引入--> <sql id="fields"> username,password,name </sql> <select id="getUser" resultType="User"> select <include refid="fields"/> from user where username=#{username} </select>
if标签
满足标签的验证内容时才将标签内的内容拼接至sql语句中
<!--if标签--> <!--直接在sql语句中插入--> <select id="getUserList" resultType="User"> select * from user where 1=1 <if test="conditions.name!=null and conditions.name!=''"> and name like concat('%',#{conditions.name},'%') </if> </select>
where标签
代替sql语句中的where,可以与if联合使用。当 where 标签内存在查询条件时, where 标签会在SQL代码中添加 WHERE 关键字; 当 where 标签内不不存在查询条件时, where 标签将忽略 WHERE 关键字的添加。除此之外,where 标签还将自动忽略其后的 AND 或者 OR 关键字。
<select id="getUserList" resultType="User"> select * from user <!--where标签,会自动添加where并忽略后面的and或者or关键字--> <where> <if test="conditions.name!=null and conditions.name!=''"> and name like concat('%',#{conditions.name},'%') </if> </where> </select>
set标签
代替sql语句中的update xxx set这里的set,实现动态更新。
set标签会忽略最后一个sql子句的后缀,比如逗号。
<update id="updateUserPassword"> update user <set> <if test="conditions.password!=null and conditions.password!=''"> password = #{conditions.password} </if> <where> <if test="conditions.username!=null and conditions.username!='' "> and username = #{conditions.username} </if> </where> </set> </update>
trim标签
Mybatis 提供了 trim 标签来代替 where 标签和 set 标签。
<!-- 其中 prefixOverrides 属性表示要被重写的前缀,prefix 属性表示用来替换重写的前缀内容。suffix和suffixOvverdides 属性表示对后缀的处理--> <trim prefix="" prefixOverrides="" suffix="" suffixOverrides=""></trim>
<select id="getScores" resultType="score"> SELECT id,name,score FROM score <trim prefix="WHERE" prefixOverrides="AND"> <if test="params.name != null and params.name != ''"> AND name LIKE CONCAT('%', #{params.name}, '%') </if> <if test="params.scoreFrom != null and params.scoreFrom != ''"> AND score >= #{params.scoreFrom} </if> <if test="params.scoreTo != null and params.scoreTo != ''"> <![CDATA[ AND score <= #{params.scoreTo} ]]> </if> </trim> </select> <update id="updateScore"> UPDATE score <trim suffixOverrides="," suffix=""> <if test="s.name != null and s.name != ''"> name = #{s.name}, </if> <if test="s.score != null and s.score != ''"> score = #{s.score}, </if> </trim> <where> <if test="s.id != null and s.id != ''"> AND id = #{s.id} </if> </where> </update>
foreach标签
collection表示遍历的元素类型,如果参数没有使用注解命名,那么该属性值只能是list,array,map其中之一;如果参数使用了注解命名,那么该属性值直接使用注解指定的名称即可。
item表示每次遍历时使用的对象名
open表示前面添加的内容
close表示最后添加的内容
seperator表示每次遍历时内容组装使用的分割符
index表示遍历时的下标<foreach collection="" item="" open="" seperator="" close="" index=""> </foreach>
例:
<delete id="deleteUserByUsername"> delete from user where username in <foreach collection="usernames" item="username" open="(" separator="," close=")"> #{username} </foreach> </delete>
Mybatis缓存
什么是缓存?
缓存是存储在内存中的临时数据,将用户经常查询的数据放在缓存(内存)中,用户再次查询数据的时候就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,能够提高查询效率,解决了高并发系统的性能问题。
为什么使用缓存?
减少和数据库的交互次数,提高效率
缓存的对象
经常查询并且很少改变的数据
一级缓存(没用)
又名Session缓存,简单地说,整个缓存的管理都由Session完成,开发者不需要做任何的事情,这个缓存本身就存在,但是这个一级缓存不能跨越Session,所以没用。
public void getUserByUserNameTest() throws IOException { SqlSession session = FactoryUtil.getSqlSession(); //从会话中获得userMapper接口的代理对象(原理是动态代理) UserMapper userMapper = session.getMapper(UserMapper.class); //调用方法 userMapper.getUserByUsername("zs"); userMapper.getUserByUsername("zs"); //清空session中的缓存 session.clearCache(); userMapper.getUserByUsername("zs"); userMapper.getUserByUsername("zs"); // System.out.println(user); }
在这个测试中,日志中只会打印出两遍sql语句,第一遍是第一次调用方法进行查询的时候,使用sql语句后session会利用一级缓存将查询结果保存,因此再次查询不会再次用sql去查。第二次是由于清空了session中的缓存,所以会重新去查询。
二级缓存
能跨越session,可以使用mybatis默认的简单的二级缓存( 一个简单的、非持久化的内存缓存),也可以引入外部缓存库。
使用外部缓存库:
导入ehcache-core包和mybatis-ehcache包(这是个中间包,承上启下,用于整合ehcache框架和mybatis.cache框架)。
<dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.6.11</version> </dependency> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency>
创建ehcache.xml文件,不用记,只需要根据官方文档改数据就行。这里需要改diskStore中数据在硬盘上的存储位置。
<ehcache> <diskStore path="java.io.tmpdir"/> <cache name="com.example.MyMapper" maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" diskPersistent="false" diskExpiryThreadIntervalSeconds="120"> </cache> </ehcache>
然后再配置文件中的settings中需要开启二级缓存
<sesstings name="cacheEnabled" value="true"/>
哪个Mapper.xml的查询中需要使用二级缓存就在哪里进行配置
<cache type="org.mybatis.caches.EhcacheCache"></cache>
使用默认的二级缓存:
不用导入依赖,全局缓存相同:
<sesstings name="cacheEnabled" value="true"/>
在mapper.xml中:
<!-- cache标签表示使用缓存 flushInterval:表示缓存刷新时间,单位是毫秒 readyOnly:表示是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改 size:表示存放多少条数据 eviction: 缓存回收策略,有这几种回收策略 LRU - 最近最少回收,移除最长时间不被使用的对象 FIFO - 先进先出,按照缓存进入的顺序来移除它们 SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象 WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象--> <cache flushInterval="300000" readOnly="true" size="10000" eviction="LRU"/>
测试:
@Test public void getUserByUserNameTest() throws IOException { SqlSession session = FactoryUtil.getSqlSession(); UserMapper userMapper = session.getMapper(UserMapper.class); userMapper.getUserByUsername("zs"); //需要提交并关闭才能进入二级缓存 session.commit(); session.close(); //------------------------------------------------ SqlSession session1 = FactoryUtil.getSqlSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); userMapper1.getUserByUsername("zs"); session1.commit(); session1.close(); //这时日志中只有一次sql语句 }
注意: 二级缓存失效
二级缓存缓存数据的前提是查询的 SqlSession 关闭,如果 SqlSession 没有关闭,那么数据将不会进入二级缓存,再次进行同构查询时,二级缓存由于没有数据,查询将进入数据库,造成二级缓存失效的现象。
另一种情况是,当前查询的 SqlSession 已经关闭,数据也进入了二级缓存,但在下一次查询之前,如果中间发生了更新操作,该操作更新的数据在的二级缓存中存在,那么二级缓存也将失效。
分页插件 PageHelper
Mybatis中的拦截器:
MyBatis的拦截器可以拦截Executor、ParameterHandler、ResultSetHandler和StatementHandler这四种类型的方法。
1.Executor:负责执行SQL语句,是MyBatis中最核心的组件之一。它负责管理缓存、执行SQL语句、处理缓存中的数据等。
2.ParameterHandler:负责处理SQL语句中的参数,将Java对象转换为JDBC Statement所需的参数。
3.ResultSetHandler:负责处理SQL查询结果集,将JDBC返回的ResultSet对象转换为Java对象。
4.StatementHandler:负责处理SQL语句的生成和执行,包括SQL语句的预编译、参数设置等操作。
这个插件本质上也是一个拦截器,要实现分页,就可以拦截Executor中的Query方法,然后取出这个SQL语句,取出表名,通过表名构建统计的SQL语句 **select count(*) from 表名,**于是向数据库发送一次请求,拿到数据的条目数 total,然后取出要查询的页码和每一页数据的个数,计算应该从哪里查询,查询多少条数据,于是构建第二个SQL语句,**原来的SQL语句 limit 查询的开始位置,查询的条目数,**得到查询数据的结果,然后将total和数据封装到一个类中进行数据的返回…
使用
导入分页插件的包pagehelper
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.0</version> </dependency>
配置文件中进行分页插件配置plugins
<!-- config.xml中进行配置 --> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins>
测试
@Test public void getAllUsers(){ SqlSession session = FactoryUtil.getSqlSession(); UserMapper mapper = session.getMapper(UserMapper.class); PageHelper.startPage(2,3);//查询第二页,每页三条数据,这句必须在查询前! List<User> allUsers = mapper.getAllUsers(); PageInfo<User> pageInfo = new PageInfo<>(allUsers);//将查询结果保存到PageInfo对象中 System.out.println("总条数:"+ pageInfo.getTotal()); System.out.println("总页数" + pageInfo.getPages()); pageInfo.getList().forEach(System.out::println);//展示查询结果 try { session.commit(); } catch (Exception e) { session.rollback(); } finally { session.close(); } }
注意:设置查询页码和每页条数的语句必须在调用查询方法之前,否则查询的结果将不会实现分页效果。
配置数据源 Druid
Druid 是阿里巴巴开源平台上的一个项目,是性能最好的数据库连接池,如何在Mybatis中配置该数据源呢?
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
创建 DruidDataSourceFactory, 并继承 PooledDataSourceFactory,并替换数据源
public class DruidDataSourceFactory extends PooledDataSourceFactory {
public DruidDataSourceFactory() {
this.dataSource = new DruidDataSource();//替换数据源
}
}
<!--config.xml-->
<dataSource type="com.qf.mybatis.datasource.DruidSourceFactory">
<!-- <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>-->
<!-- <property name="url" value="jdbc:mysql://localhost:3306/lesson?serverTimezone=Asia/Shanghai&tinyInt1isBit=false"/>-->
<!-- <property name="username" value="root"/>-->
<!-- <property name="password" value="123456"/>-->
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
注意: 在 Druid 数据源中,属性名称是
driverClass
和jdbcUrl
,而不是driver
和url
。因此,需要使用driverClassName
进行配置。