一个项目,一个工程,导出为一个war包,在一个Tomcat上运行。也叫all in one。
一个项目,拆分成很多个模块,每个模块是一个工程。每一个工程都是运行在自己的Tomcat上。模块之间可以互相调用。每一个模块内部可以看成是一个单一架构的应用。
框架=jar包+配置文件
MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下, iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。
iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。 iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAO)。
https://github.com/mybatis/mybatis-3
MyBatis支持定制化SQL、存储过程以及高级映射
MyBatis避免了几乎所有的JDBC代码和手动设置参数以及结果集解析操作
MyBatis可以使用简单的XML或注解实现配置和原始映射;将接口和Java的POJO(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录
Mybatis是一个半自动的ORM(Object Relation Mapping)框架
JDBC
SQL 夹杂在Java代码中耦合度高,导致硬编码内伤
维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见
代码冗长,开发效率低
Hibernate 和 JPA
操作简便,开发效率高
程序中的长难复杂 SQL 需要绕过框架
内部自动生产的 SQL,不容易做特殊优化
基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
反射操作太多,导致数据库性能下降
MyBatis
轻量级,性能出色
SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据
开发效率稍逊于HIbernate,但是完全能够接收
CREATE DATABASE `mybatis-example`;
USE `mybatis-example`;
CREATE TABLE `t_emp`(
emp_id INT AUTO_INCREMENT,
emp_name CHAR(100),
emp_salary DOUBLE(10,5),
PRIMARY KEY(emp_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("tom",200.33);
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.7version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.3version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.6version>
dependency>
dependencies>
实体类是和现实世界中某一个具体或抽象的概念对应,是软件开发过程中,为了管理现实世界中的数据而设计的模型。
实体类的多个不同的叫法:
domain:领域模型
entity:实体
POJO:Plain Old Java Object
Java bean:一个Java类
/**
* 和数据库表t_emp对应的实体类
* emp_id INT AUTO_INCREMENT
* emp_name CHAR(100)
* emp_salary DOUBLE(10,5)
*
* Java的实体类中,属性的类型不要使用基本数据类型,要使用包装类型。因为包装类型可以赋值为null,表示空,而基本数据类型不可以。
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Emp {
private Long empId;
private String empName;
private Double empSalary;
}
主要操作就是创建全局配置文件和Mybatis映射文件
习惯上命名为mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。将来整合Spring之后,这个配置文件可以省略,所以大家操作时可以直接复制、粘贴。
mybatis-config.xml
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-example?useUnicode=true&characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="mappers/EmployeeMapper.xml"/>
mappers>
configuration>
environments表示配置Mybatis的开发环境,可以配置多个环境,在众多具体环境中,使用default属性指定实际运行时使用的环境。default属性的取值是environment标签的id属性的值。environment表示配置Mybatis的一个具体的环境。
在mappers标签中进行Mapper注册:指定Mybatis映射文件的具体位置,在mapper文件写sql语句。
对Maven工程的目录结构来说,resources目录下的内容会直接放入类路径,所以这里我们可以以resources目录为基准
配置url时在xml配置文件中,需要将’&‘符号转义,所以这里要写成’&’。如果关联外部配置文件jdbc.proerties中在properties类型问可以写&。
注意:配置文件存放的位置是src/main/resources目录下。
相关概念:ORM(Object Relationship Mapping)对象关系映射。
Java概念 | 数据库概念 |
---|---|
类 | 表 |
属性 | 字段/列 |
对象 | 记录/行 |
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.myBatis.dao.EmployeeMapper">
<select id="selectEmpById" resultType="com.stonebridge.myBatis.domain.Emp">
select emp_id empId,emp_name empName,emp_salary empSalary from t_emp where emp_id=#{empId}
select>
mapper>
namespace
在Java程序中,必须找到当前的Mapper配置文件,才能进一步找到这里配置的SQL语句。Java程序就是通过mapper标签的namespace属性找到当前Mapper配置文件。将来为了让一个Java接口,直接对应这个Mapper配置文件,通常使用Java接口的全类名作为这个namespace属性的值。以前我们叫dao,现在叫mapper,本质上都是持久化层的类型,只是命名习惯的区别。
sql类型的定义
在select标签中编写一条select语句实现查询效果
id属性:这条SQL语句的唯一标识
resultType属性:Mybatis负责解析结果集,将解析得到的数据封装到Java类型中。resultType属性就是指定这个Java类型
如果查出表的字段和Java类型的字段不一致,例如数据库字段含有下划线,大小写和java不一致。可以通过取别名的方式实现
注意:EmployeeMapper.xml所在的目录要和mybatis-config.xml中使用mapper标签配置的一致。
public class Mybatis {
@Test
public void TestSelect() throws IOException {
// 1.使用Mybatis的Resources类读取Mybatis全局配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2.创建SqlSessionFactoryBuilder对象。主要用于创建SqlSessionFactory
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 3.调用builder对象的build方法创建SqlSessionFactory对象
SqlSessionFactory sessionFactory = builder.build(inputStream);
// 4.通过SqlSessionFactory对象开启一个从Java程序到数据库的会话
SqlSession session = sessionFactory.openSession();
// 5.通过SqlSession对象找到Mapper配置文件中可以执行的SQL语句
// statement参数的格式:Mapper配置文件的namespace属性.SQL标签的id属性
// parameter参数:给SQL语句传入的参数
Object object = session.selectOne("com.stonebridge.myBatis.dao.EmployeeMapper.selectEmpById", "1");
// 6.直接打印查询结果
System.out.println("object = " + object);
// 7.提交事务
session.commit();
// 8.关闭SqlSession
session.close();
}
}
执行测试代码:
@Test
public void testHelloWorldReview() throws IOException {
//1.借助Mybatis的Resource类将Mybatis全局配置文件读取到内存中
//这里使用的路径仍然是一个以类路径目录为基准的相对路径
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建SqlSessionFactoryBuilder对象。SqlSessionFactoryBuilder对象主要用于创建SqlSessionFactory,SqlSessionFactory又是生产SqlSession的
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.调用builder对象的build()方法读取《全局配置文件的输入流》来创建SqlSessionFactory
SqlSessionFactory factory = builder.build(inputStream);
//4.调用工厂对象的方法开启一个会话,从java程序的持久层到数据库的会话
SqlSession session = factory.openSession();
//5.执行Mapper配置文件中准备好sql语句。找到sql语句的过程:通过namespace找到mapper映射文件的sql语句标签
//备注:此时执行的查找操作的sql已经不是到xml配置文件中找了,因为xml配置文件中的信息已经读取到内存中封装成了对象,
//所以此时其实是到已经封装的对象中查找,查找的依据是:mapper配置文件namespace值,sql语句标签的id
String statement = "com.stonebridge.myBatis.dao.EmployeeMapper.selectEmpById";
Long empId = 1L;
Object object = session.selectOne(statement, empId);
// 6.直接打印查询结果
System.out.println("object = " + object);
// 7.提交事务
session.commit();
// 8.关闭SqlSession
session.close();
}
流程说明:
如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。
刚开始接触框架,我们会认为Java程序会转入XML配置文件中执行,但其实框架会在初始化时将XML文件读取进来,封装到对象中,再然后就都是Java代码的执行了,XML中的配置是没法执行的。
封装Configuration对象
准备去获取已映射的指令
正式获取已映射的指令
mappedStatements对象结构
mappedStatements对象的类型:Configuration类中的一个静态内部类:StrictMap
在Mybatis工作过程中,通过打印日志的方式,将要执行的SQL语句打印出来。
加入依赖
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
加入log4j的配置文件
支持XML和properties属性文件两种形式。无论使用哪种形式,文件名是固定的:
DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n"/>
layout>
appender>
<logger name="java.sql">
<level value="debug"/>
logger>
<logger name="org.apache.ibatis">
<level value="info"/>
logger>
<root>
<level value="debug"/>
<appender-ref ref="STDOUT"/>
root>
log4j:configuration>
日志的级别
FATAL(致命)>ERROR(错误)>WARN(警告)>INFO(信息)>DEBUG(调试)
从左到右打印的内容越来越详细
STDOUT
是standard output的缩写,意思是标准输出。对于Java程序来说,打印到标准输出就是打印到控制台。
打印效果
DEBUG 10-26 22:02:51,877 ==> Preparing: select emp_id empId,emp_name empName,emp_salary empSalary from t_emp where emp_id=? (BaseJdbcLogger.java:137)
DEBUG 10-26 22:02:51,910 ==> Parameters: 1(String) (BaseJdbcLogger.java:137)
DEBUG 10-26 22:02:51,936 <== Total: 1 (BaseJdbcLogger.java:137)
object = Emp(empId=1, empName=tom, empSalary=200.33)
在实际开发时,同一套代码往往会对应多个不同的具体服务器环境。使用的数据库连接参数也不同。为了更好的维护这些信息,我们建议把数据库连接信息提取到Mybatis全局配置文件外边。
创建jdbc.properties配置文件
wechat.dev.driver=com.mysql.jdbc.Driver
wechat.dev.url=jdbc:mysql://localhost:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
wechat.dev.username=root
wechat.dev.password=123456
wechat.test.driver=com.mysql.jdbc.Driver
wechat.test.url=jdbc:mysql://192.168.198.150:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
wechat.test.username=root
wechat.test.password=123456
wechat.product.driver=com.mysql.jdbc.Driver
wechat.product.url=jdbc:mysql://192.168.198.200:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
wechat.product.username=root
wechat.product.password=123456
配置url时在xml配置文件中,需要将’&‘符号转义,所以这里要写成’&’。如果关联外部配置文件jdbc.proerties中在properties类型问可以写&。
在Mybatis全局配置文件中指定外部jdbc.properties文件的位置
<properties resource="jdbc.properties"/>
在需要具体属性值的时候使用${key}格式引用属性文件中的键
<dataSource type="POOLED">
<property name="driver" value="${wechat.dev.driver}"/>
<property name="url" value="${wechat.dev.url}"/>
<property name="username" value="${wechat.dev.username}"/>
<property name="password" value="${wechat.dev.password}"/>
dataSource>
Mybatis中的Mapper接口相当于以前的Dao。但是区别在于,Mapper仅仅是接口,我们不需要提供实现类。
声明这个接口是为了上层代码调用Mybatis的具体功能。就整个接口及其中的方法而言
接口的全类名要和Mapper配置文件的namespace一致,这样才能通过接口找到Mapper配置信息。
接口中的方法名要和mapper配置中sql语句所在标签的id一致,这样才能通过方法名找到具体的sql语句。
mapper配置文件的id属性值是唯一,所以对应mapper接口中的方法名也是唯一,当前接口的方法不允许重载。
方法对应Mapper配置文件配置的标签,就方法而言。
public interface EmployeeMapper {
/**
* 通过这个方法对应Mapper配置文件的sql语句
*
* @param empId 当前方法的参数对应sql中#{empId}声明的参数
* @return 当前方法的返回值类型和resultType属性指定的类型一致
*/
Emp selectEmpById(Long empId);
Integer insertEmp(Emp emp);
Integer deleteEmp(Long empId);
Integer updateEmp(Emp emp);
}
在select标签中编写一条select语句实现查询效果。
SQL语句参数接收
#{empId}这里是为了方便接收Java程序传过来的参数数据
如果参数是基本类型,Mybatis负责将#{empId}部分转换为"?"占位符。用empId去填充占位符。
如果参数类型需要从Bean中解析
以insertEmp(Emp emp);为例
现在在这条SQL语句中,#{}中的表达式需要被用来从Emp emp实体类中获取emp_name的值、emp_salary的值。
而我们从实体类中获取值通常都是调用getter()方法。而getter()方法、setter()方法定义了实体类的属性。
定义属性的规则是:把get、set去掉,剩下部分首字母小写。
所以我们在#{}中使用getXxx()方法、setXxx()方法定义的属性名即可。
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.myBatis.dao.EmployeeMapper">
<select id="selectEmpById" resultType="com.stonebridge.myBatis.domain.Emp">
select emp_id empId,emp_name empName,emp_salary empSalary from t_emp where emp_id=#{empId}
select>
<insert id="insertEmp">
INSERT INTO t_emp(emp_name,emp_salary) VALUES (#{empName},#{empSalary})
insert>
<delete id="deleteEmp">
DELETE
FROM t_emp
WHERE emp_id = #{empId}
delete>
<update id="updateEmp">
UPDATE t_emp
SET emp_name=#{empName},
emp_salary=#{empSalary}
where emp_id = #{empId}
update>
mapper>
@Before标注的方法会在会在每个@Test方法执行前执行。init()方法读取配置文件,准备环境。
@After标注的方法会在会在每个@Test方法执行前执行。clear()方法关闭资源。
public class MyBastisTestImprove {
private SqlSession session;
//junit会在每个@Test方法执行前执行
@Before
public void init() throws IOException {
session = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")).openSession();
}
@Test
public void testUserMapperInterface() {
//TODD
}
@After
public void clear() {
session.commit();
session.close();
}
}
public class MyBastisTestImprove {
private SqlSession session;
//junit会在每个@Test方法执行前执行
@Before
public void init() throws IOException {
session = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")).openSession();
}
//执行查询
@Test
public void testUserMapperInterface() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
System.out.println("employeeMapper.getClass().getName():" + employeeMapper.getClass().getName());
//调用employeeMapper的方法去完成对数据库的操作
Emp emp = employeeMapper.selectEmpById(1L);
System.out.println(emp.toString());
}
//执行插入方法
@Test
public void TestInsert() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Integer row = employeeMapper.insertEmp(new Emp(null, "网", 112.333));
System.out.println(row);
}
//执行删除方法
@Test
public void TestDelete() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Integer row = employeeMapper.deleteEmp(2l);
System.out.println(row);
}
//执行更新方法
@Test
public void TestUpdate() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Integer row = employeeMapper.updateEmp(new Emp(3l, "张三", 222.33));
System.out.println(row);
}
@After
public void clear() {
session.commit();
session.close();
}
}
Mybatis会在运行过程中,把配置文件中的SQL语句里面的#{}转换为“?”占位符,发送给数据库执行。
配置文件中的SQL:
<delete id="deleteEmployeeById">
delete from t_emp where emp_id=#{empId}
delete>
实际执行的SQL:
delete from t_emp where emp_id=?
根据${}拼字符串
示例:
Mapper接口
public interface EmployeeMapper {
Emp selectEmpByName(String name);
}
在mapper.xml配置文件配置sql
<select id="selectEmpByName" resultType="com.stonebridge.myBatis.domain.Emp">
select emp_id empId, emp_name empName, emp_salary empSalary
from t_emp
where emp_name like "%${name}%"
select>
调用
@Test
public void testDollar() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Emp emp = employeeMapper.selectEmpByName("r");
System.out.println(emp.toString());
}
拼接sql效果
select emp_id empId, emp_name empName, emp_salary empSalary from t_emp where emp_name like "%r%"
这里数据输入具体是指上层方法(例如Service方法)调用Mapper接口时,数据传入的形式。
Integer deleteEmp(Long empId);
<delete id="deleteEmp">
DELETE
FROM t_emp
WHERE emp_id = #{empId}
delete>
Integer insertEmp(Emp emp);
<insert id="insertEmp">
INSERT INTO t_emp(emp_name,emp_salary) VALUES (#{empName},#{empSalary})
insert>
Mybatis会根据#{}中传入的数据,加工成getXxx()方法,通过反射在实体类对象中调用这个方法,从而获取到对应的数据。填充到#{}这个位置。
Integer createEmp(@Param("empId") Long empId, @Param("name") String name, @Param("salary") Double salary);
如果我们有在接口声明抽象方法时使用@Param注解给参数命名,那么就可以在#{}中使用我们指定的名称
void updateSalaryById(@Param(“empId”) Long empId, @Param(“empSalary”) Double salary);
如果我们没有在接口声明抽象方法时给参数命名,那么Mybatis会要求使用默认参数名
BindingException: Parameter ‘salary’ not found. Available parameters are [arg1, arg0, param1, param2]
<insert id="createEmp">
INSERT INTO t_emp(emp_id, emp_name, emp_salary)
VALUES (#{empId}, #{name}, #{salary})
insert>
@Test
public void TestMultiParama() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Long empId = 24L;
Double salary = Double.parseDouble("12345.67");
String name = "stonebridge";
Integer row = employeeMapper.createEmp(empId, name, salary);
System.out.println(row); //1
}
INSERT INTO t_emp(emp_id, emp_name, emp_salary) VALUES (?, ?, ?) (BaseJdbcLogger.java:137)
Parameters: 24(Long), stonebridge(String), 12345.67(Double) (BaseJdbcLogger.java:137)
DEBUG 10-28 21:13:38,791 <== Updates: 1 (BaseJdbcLogger.java:137)
int updateEmployeeByMap(Map<String, Object> paramMap);
<update id="updateEmployeeByMap">
update t_emp
set emp_salary=#{empSalaryKey}
where emp_id = #{empIdKey}
update>
@Test
public void testUpdateEmpNameByMap() {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("empSalaryKey", 999.99);
paramMap.put("empIdKey", 3);
int result = mapper.updateEmployeeByMap(paramMap);
System.out.println("result = " + result);
}
#{}中写Map中的key
有很多零散的参数需要传递,但是没有对应的实体类类型可以使用。使用@Param注解一个一个传入又太麻烦了,可以都封装到Map中。
int selectEmpCount();
<select id="selectEmpCount" resultType="int">
select count(*) from t_emp
select>
@Test
public void testEmpCount() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
int count = employeeMapper.selectEmpCount();
System.out.println("count = " + count);
}
Employee selectEmployee(Integer empId);
<select id="selectEmployee" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id empId,emp_name empName,emp_salary empSalary from t_emp where emp_id=#{maomi}
select>
通过给数据库表字段加别名,让查询结果的每一列都和Java实体类中属性对应起来。如果所有查询的字段和实体类中的字段都对不上,就会返回null,而不是空的实体类对象。
@Test
public void testSelectEmployee() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Emp emp = employeeMapper.selectEmployee(23);
System.out.println(emp);
}
在mybatis的主配置文件mybatis-config.xml开启增加全局配置自动识别对应关系。
将mapUnderscoreToCamelCase属性配置为true,表示开启自动映射驼峰式命名规则
规则要求如下:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
开启该配置后,Mapper配置文件。
<select id="selectEmployee" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_salary from t_emp where emp_id=#{maomi}
select>
适用于SQL查询返回的各个字段综合起来并不和任何一个现有的实体类对应,没法封装到实体类对象中。能够封装成实体类类型的,就不使用Map类型。
Map<String,Object> selectEmpNameAndMaxSalary();
<select id="selectEmpNameAndMaxSalary" resultType="map">
SELECT emp_name 员工姓名,
emp_salary 员工工资,
(SELECT AVG(emp_salary) FROM t_emp) 部门平均工资
FROM t_emp
WHERE emp_salary = (
SELECT MAX(emp_salary)
FROM t_emp
)
select>
@Test
public void testQueryEmpNameAndSalary() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> resultMap = employeeMapper.selectEmpNameAndMaxSalary();
Set<Map.Entry<String, Object>> entrySet = resultMap.entrySet();
for (Map.Entry<String, Object> entry : entrySet) {
String key = entry.getKey();
Object value = entry.getValue();
System.out.println(key + "=" + value);
}
}
查询结果返回多个实体类对象,希望把多个实体类对象放在List集合中返回。此时不需要任何特殊处理,在resultType属性中还是设置实体类类型即可。
List<Emp> selectAll();
<select id="selectAll" resultType="com.stonebridge.myBatis.domain.Emp">
select emp_id empId, emp_name empName, emp_salary empSalary
from t_emp
select>
@Test
public void testSelectAll() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
List<Emp> employeeList = employeeMapper.selectAll();
for (Emp employee : employeeList) {
System.out.println("employee = " + employee);
}
}
例如:保存订单信息。需要保存Order对象和List
在保存List
insert into t_order_item(item_name,item_price,item_count,order_id) values(...)
这里需要用到的order_id,是在保存Order对象时,数据库表以自增方式产生的,需要特殊办法拿到这个自增的主键值。至于,为什么不能通过查询最大主键的方式解决这个问题,参考下图:
Mapper接口中的抽象方法
int insertEmployee(Employee employee);
SQL语句
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="empId">
insert into t_emp(emp_name,emp_salary)
values(#{empName},#{empSalary})
insert>
Junit测试
@Test
public void testSaveEmp() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Emp employee = new Emp();
employee.setEmpName("john");
employee.setEmpSalary(666.66);
employeeMapper.insertEmployee(employee);
System.out.println("employee.getEmpId() = " + employee.getEmpId());
}
注意
Mybatis是将自增主键的值设置到实体类对象中,而不是以Mapper接口方法返回值的形式返回。
而对于不支持自增型主键的数据库(例如 Oracle),则可以使用 selectKey 子元素:selectKey 元素将会首先运行,id 会被设置,然后插入语句会被调用。
<insert id="insertEmployee" parameterType="com.atguigu.mybatis.beans.Employee" databaseId="oracle">
<selectKey order="BEFORE" keyProperty="id" resultType="integer">
select employee_seq.nextval from dual
selectKey>
insert into orcl_employee(id,last_name,email,gender) values(#{id},#{lastName},#{email},#{gender})
insert>
将字段的别名设置成和实体类属性一致。
<select id="selectEmployee" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id empId,emp_name empName,emp_salary empSalary from t_emp where emp_id=#{maomi}
select>
关于实体类属性的约定:getXxx()方法、setXxx()方法把方法名中的get或set去掉,首字母小写。
在Mybatis全局配置文件加入如下配置:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
SQL语句中可以不使用别名
<select id="selectEmployee" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_salary from t_emp where emp_id=#{empId}
select>
使用resultMap标签定义对应关系,再在后面的SQL语句中引用这个对应关系
Mapper接口中的抽象方法
List<Emp> selectWithResultMap();
映射文件
<resultMap id="selectWithResultMapResultMap" type="com.stonebridge.myBatis.domain.Emp">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_salary" property="empSalary"/>
resultMap>
<select id="selectWithResultMap" resultMap="selectWithResultMapResultMap">
select emp_id, emp_name, emp_salary
from t_emp
select>
测试
@Test
public void testSelectWithResultMap() {
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
List<Emp> empList = employeeMapper.selectWithResultMap();
for (Emp emp : empList) {
System.out.println("emp = " + emp);
}
}
主要体现在数据库表中
一对一
夫妻关系,人和身份证号
一对多
用户和用户的订单,锁和钥匙
多对多
老师和学生,部门和员工
主要体现在Java实体类中
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Customer {
private long customerId;
private String customerName;
// 体现对多关系
private List<Order> orderList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Order {
private long orderId;
private String orderName;
private long customerId;
// 体现对一关系
private Customer customer;
}
CREATE TABLE `t_customer` (
`customer_id` INT NOT NULL AUTO_INCREMENT,
`customer_name` CHAR(100),
PRIMARY KEY (`customer_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order` (
`order_id` INT NOT NULL AUTO_INCREMENT,
`order_name` CHAR(100),
`customer_id` INT,
PRIMARY KEY (`order_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t_customer` (`customer_name`) VALUES ('c01');
INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o1', '1');
INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o2', '1');
INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o3', '1');
实际开发时,一般在开发过程中,不给数据库表设置外键约束。
原因是避免调试不方便。
一般是功能开发完成,再加外键约束检查是否有bug。
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Customer {
private Integer customerId;
private String customerName;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Order {
private long orderId;
private String orderName;
private long customerId;
// 体现对一关系
private Customer customer;
}
public interface OrderMapper {
Order selectOrderWithCustomer(Integer orderId);
}
配置关联关系和SQL语句
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.OrderMapper">
<resultMap id="selectOrderWithCustomerResultMap" type="com.stonebridge.mybatis.entity.Order">
<id column="order_id" property="orderId">id>
<result column="order_name" property="orderName">result>
<result column="customer_id" property="customerId">result>
<association property="customer" javaType="com.stonebridge.mybatis.entity.Customer">
<id column="customerId" property="customer_id">id>
<result column="customerName" property="customer_name">result>
association>
resultMap>
<select id="selectOrderWithCustomer" resultMap="selectOrderWithCustomerResultMap">
select order_id, order_name, a.customer_id
from t_order a
left join t_customer b on a.customer_id = b.customer_id
where order_id = #{orderId};
select>
mapper>
mybatis-config.xml
<mappers>
<mapper resource="mappers/OrderMapper.xml"/>
mappers>
@Test
public void testQueryOrderWithCustomer() {
OrderMapper orderMapper = session.getMapper(OrderMapper.class);
Order order = orderMapper.selectOrderWithCustomer(1);
System.out.println(order);
}
DEBUG 10-30 22:52:25,155 ==> Preparing: select order_id, order_name, a.customer_id from t_order a left join t_customer b on a.customer_id = b.customer_id where order_id = ?; (BaseJdbcLogger.java:137)
DEBUG 10-30 22:52:25,244 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 10-30 22:52:25,272 <== Total: 1 (BaseJdbcLogger.java:137)
Order(orderId=1, orderName=o1, customerId=1, customer=null)
在“对一”关联关系中,我们的配置比较多,但是关键词就只有:association和javaType
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Customer {
private Integer customerId;
private String customerName;
// 声明order的List集合类型的属性,建立『对多(对方是多的一端)』关联关系
private List<Order> orderList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Order {
private Integer orderId;
private String orderName;
private Integer customerId;
}
public interface CustomerMapper {
Customer selectCustomerWithOrderList(int i);
}
配置关联关系和SQL语句
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.CustomerMapper">
<resultMap id="selectCustomerWithOrderLisResultMap" type="com.stonebridge.mybatis.entity.Customer">
<id column="customer_id" property="customerId">id>
<result column="customer_name" property="customerName">result>
<collection property="orderList" ofType="com.stonebridge.mybatis.entity.Order">
<id column="order_id" property="orderId">id>
<result column="order_name" property="orderName">result>
<result column="customer_id" property="customerId">result>
collection>
resultMap>
<select id="selectCustomerWithOrderList" resultMap="selectCustomerWithOrderLisResultMap">
select c.customer_id, customer_name, order_id, order_name
from t_customer c
left join t_order o on c.customer_id = o.customer_id
where c.customer_id = #{customerId}
select>
mapper>
mybatis-config.xml
<mappers>
<mapper resource="mappers/OrderMapper.xml"/>
mappers>
@Test
public void testQueryCustomerWithOrderList() {
CustomerMapper customerMapper = session.getMapper(CustomerMapper.class);
Integer customerId = 1;
// 执行查询
Customer customer = customerMapper.selectCustomerWithOrderList(customerId);
// 打印Customer本身
System.out.println("customer = " + customer);
// 从Customer中获取Order集合数据
List<Order> orderList = customer.getOrderList();
for (Order order : orderList) {
System.out.println("order = " + order);
}
}
在“对多”关联关系中,同样有很多配置,但是提炼出来最关键的就是:collection和ofType
在执行一对一查询关联查询时,查询Order表作为主表,其中的Customer数据不管用不用都会查出来,如果查询的数据不用就浪费资源,完全不查需要的时候又要单独查询,此时也没有必要考虑关联关系了。
希望即能维持关联关系,用的时候又可以查询出来。
为了实现延迟加载,对Customer和Order的查询必须分开,分成两步来做,才能够实现(分步查询是懒加载的基础)。为此,我们需要单独查询Order,也就是需要在Mapper配置文件中,单独编写查询Order集合数据的SQL语句。
public interface OrderMapper {
Order selectOrderWithOrderTwoStep(Integer orderId);
}
OrderMapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.OrderMapper">
<resultMap id="selectOrderWithCustomerTwoStepResultMap" type="com.stonebridge.mybatis.entity.Order">
<id column="order_id" property="orderId"/>
<result column="order_name" property="orderName"/>
<result column="customer_id" property="customerId"/>
<association
property="customer"
column="customer_id"
select="com.stonebridge.mybatis.mapper.CustomerMapper.selectCustomerById"/>
resultMap>
<select id="selectOrderWithOrderTwoStep" resultMap="selectOrderWithCustomerTwoStepResultMap">
select order_id, order_name, customer_id
from t_order
where order_id = #{orderId}
select>
mapper>
CustomerMapper.xml
配置分步查询中对com.stonebridge.mybatis.mapper.CustomerMapper.selectCustomerById进行定义
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.CustomerMapper">
<select id="selectCustomerById" resultType="com.stonebridge.mybatis.entity.Customer">
select customer_id, customer_name
from t_customer
where customer_id = #{customerId}
select>
mapper>
@Test
public void testQueryOrderWithOrderList() {
OrderMapper orderMapper = session.getMapper(OrderMapper.class);
Integer orderId = 1;
// 查询Order对象
Order order = orderMapper.selectOrderWithOrderTwoStep(orderId);
// 打印Order对象本身信息
System.out.println("order = " + order);
// 通过Order对象获取关联的Customer对象
Customer customer = order.getCustomer();
System.out.println("customer = " + customer);
}
public interface CustomerMapper {
Customer selectCustomerWithOrderList(Integer customerId);
}
CustomerMapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.CustomerMapper">
<resultMap id="selectCustomerWithOrderListResultMap" type="com.stonebridge.mybatis.entity.Customer">
<id column="customer_id" property="customerId"/>
<result column="customer_name" property="customerName"/>
<collection property="orderList"
column="customer_id"
select="com.stonebridge.mybatis.mapper.OrderMapper.selectOrderListByCustomerId"/>
resultMap>
<select id="selectCustomerWithOrderList" resultMap="selectCustomerWithOrderListResultMap">
select customer_id, customer_name
from t_customer
where customer_id = #{customerId}
select>
mapper>
OrderMapper.xml
配置分步查询中对com.stonebridge.mybatis.mapper.CustomerMapper.selectCustomerById进行定义
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stonebridge.mybatis.mapper.OrderMapper">der_id = #{orderId}
select>
<select id="selectOrderListByCustomerId" resultType="com.stonebridge.mybatis.entity.Order">
select order_id, order_name, customer_id
from t_order
where customer_id = #{customerId}
select>
mapper>
@Test
public void testQueryCustomerWithOrderList() {
CustomerMapper customerMapper = session.getMapper(CustomerMapper.class);
Integer customerId = 1;
// 查询Order对象
Customer customer = customerMapper.selectCustomerWithOrderList(customerId);
// 打印Order对象本身信息
System.out.println("customer = " + customer);
// 通过Order对象获取关联的Customer对象
List<Order> list = customer.getOrderList();
for (Order order : list) {
System.out.println("order=" + order);
}
}
查询到Customer的时候,不一定会使用Order的List集合数据。如果Order的集合数据始终没有使用,那么这部分数据占用的内存就浪费了。对此,我们希望不一定会被用到的数据,能够在需要使用的时候再去查询。
例如:对Customer进行1000次查询中,其中只有15次会用到Order的集合数据,那么就在需要使用时才去查询能够大幅度节约内存空间。
延迟加载的概念:对于实体类关联的属性到需要使用时才查询。也叫懒加载。
在Mybatis全局配置文件中配置settings
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
settings>
官方文档中对aggressiveLazyLoading属性的解释:
When enabled, an object with lazy loaded properties will be loaded entirely upon a call to any of the lazy properties.Otherwise, each property is loaded on demand.
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
settings>
不要先打印customer整体
@Test
public void testQueryCustomerWithOrderList() throws InterruptedException {
CustomerMapper customerMapper = session.getMapper(CustomerMapper.class);
Integer customerId = 1;
// 查询Order对象
Customer customer = customerMapper.selectCustomerWithOrderList(customerId);
// 打印Order对象本身信息
// System.out.println("customer = " + customer);
System.out.println("customerId:" + customer.getCustomerId());
System.out.println("customerName:" + customer.getCustomerName());
TimeUnit.SECONDS.sleep(3);
// 通过Order对象获取关联的Customer对象
List<Order> list = customer.getOrderList();
for (Order order : list) {
System.out.println("order=" + order);
}
}
效果:刚开始先查询Customer本身,需要用到OrderList的时候才发送SQL语句去查询
我们是在“对多”关系中举例说明延迟加载的,在“对一”中配置方式基本一样。
关联关系 | 配置项关键词 | 所在配置文件 |
---|---|---|
对一 | association标签/javaType属性 | Mapper配置文件中的resultMap |
对多 | collection标签/ofType属性 | Mapper配置文件中的resultMap |
对一分步 | association标签/select属性 | Mapper配置文件中的resultMap |
对多分步 | collection标签/select属性 | Mapper配置文件中的resultMap |
延迟加载[低] | lazyLoadingEnabled设置为true aggressiveLazyLoading设置为false | Mybatis全局配置文件中的settings |
延迟加载[高] | lazyLoadingEnabled设置为true | Mybatis全局配置文件中的settings |
在某一个表中,使用一个字段保存多个“外键”值,这将导致无法使用SQL语句进行关联查询。
这样就可以使用SQL进行关联查询了。只是有可能需要三张表进行关联。
使用联合主键时,只要多个字段的组合不重复即可,单个字段内部是可以重复的。
Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能,它存在的意义是为了解决拼接SQL语句字符串时的痛点问题。
One of the most powerful features of MyBatis has always been its Dynamic SQL capabilities. If you have any experience with JDBC or any similar framework, you understand how painful it is to conditionally concatenate strings of SQL together, making sure not to forget spaces or to omit a comma at the end of a list of columns. Dynamic SQL can be downright painful to deal with.
MyBatis的一个强大的特性之一通常是它的动态SQL能力。如果你有使用JDBC或其他相似框架的经验,你就明白条件地串联SQL字符串在一起是多么的痛苦,确保不能忘了空格或在列表的最后省略逗号。动态SQL可以彻底处理这种痛苦。
使用where标签动态生成SQL语句中的where子句
使用if标签可以让SQL语句的片段,在满足条件时才加入最终的SQL语句
test属性:if标签中的判断条件。在test属性中能直接访问接口抽象方法传入的数据。
test属性:在条件判断语句中,难免会用到大于号、小于号。此时需要将大于号(>)、小于号(<)转义
XML转义字符
< | < | 小于号 |
---|---|---|
> | > | 大于号 |
<= | <= | 小于等于号 |
>= | >= | 大于等于号 |
& | & | 和 |
' | ’ | 单引号 |
" | " | 双引号 |
<select id="selectEmpByCondition" resultType="com.stonebridge.MyBatis.domain.Emp">
select emp_id, emp_name, emp_salary
from t_emp
<where>
<if test="empName!=null">
or emp_name=#{empName}
if>
<if test="empSalary > 100">
or emp_salary=#{empSalary}
if>
where>
select>
测试:
@Test
public void testQueryEmp() {
EmployeeMapper customerMapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("empName",null);
map.put("empSalary",999.99);
List<Emp> list = customerMapper.selectEmpByCondition(map);
for (Emp emp : list) {
System.out.println("emp:" + emp.toString());
}
}
@Test
public void testQueryEmp() {
EmployeeMapper customerMapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("empName","stonebridge");
map.put("empSalary",200000);
List<Emp> list = customerMapper.selectEmpByCondition(map);
for (Emp emp : list) {
System.out.println("emp:" + emp.toString());
}
}
实际开发时,对一个实体类对象进行更新。往往不是更新所有字段,而是更新一部分字段。此时页面上的表单往往不会给不修改的字段提供表单项。
<form action="" method="">
<input type="hidden" name="userId" value="5232" />
年 龄:<input type="text" name="userAge" /><br/>
性 别:<input type="text" name="userGender" /><br/>
坐 标:<input type="text" name="userPosition" /><br/>
<button type="submit">修改button>
form>
例如上面的表单,如果服务器端接收表单时,使用的是User这个实体类,那么userName、userBalance、userGrade接收到的数据就是null。
如果不加判断,直接用User对象去更新数据库,在Mapper配置文件中又是每一个字段都更新,那就会把userName、userBalance、userGrade设置为null值,从而造成数据库表中对应数据被破坏。
此时需要我们在Mapper配置文件中,对update语句的set子句进行定制,此时就可以使用动态SQL的set标签,没有set子句的update语句会导致SQL语法错误。
<update id="updateEmployeeDynamic">
update t_emp
<set>
<if test="empName != null">
emp_name=#{empName},
if>
<if test="empSalary < 3000">
emp_salary=#{empSalary},
if>
set>
where emp_id=#{empId}
update>
@Test
public void testUpadteEmpCondition() {
EmployeeMapper customerMapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("empName", "stonebridge");
map.put("empSalary", 20000);
Integer row = customerMapper.updateEmpConditional(map);
}
使用trim标签控制条件部分两端是否包含某些字符
<select id="selectEmployeeByConditionByTrim" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_age,emp_salary,emp_gender
from t_emp
<trim prefix="where" suffixOverrides="and|or">
<if test="empName != null">
emp_name=#{empName} and
if>
<if test="empSalary > 3000">
emp_salary>#{empSalary} and
if>
<if test="empAge <= 20">
emp_age=#{empAge} or
if>
<if test="empGender=='male'">
emp_gender=#{empGender}
if>
trim>
select>
<select id="selectEmpByConditionByTrim" resultType="com.stonebridge.MyBatis.domain.Emp">
select emp_id, emp_name, emp_salary from t_emp
<trim prefix="where" prefixOverrides="and|or">
<if test="empName != null">
or emp_name=#{empName}
if>
<if test="empSalary > 2000">
and emp_salary>#{empSalary}
if>
trim>
select>
@Test
public void testSelectEmpByConditionByTrim() {
EmployeeMapper customerMapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("empName", "stonebridge");
// map.put("empSalary", 2000);
List<Emp> list = customerMapper.selectEmpByConditionByTrim(map);
for (Emp emp : list) {
System.out.println(emp);
}
}
多个分支条件中,仅执行一个
<select id="selectEmployeeByConditionByChoose" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_salary from t_emp
where
<choose>
<when test="empName != null">emp_name=#{empName}when>
<when test="empSalary < 3000">emp_salary < 3000when>
<otherwise>1=1otherwise>
choose>
select>
用批量插入举例
<foreach collection="empList" item="emp" separator="," open="values" index="myIndex">
(#{emp.empName},#{myIndex},#{emp.empSalary},#{emp.empGender})
foreach>
上面批量插入的例子本质上是一条SQL语句,而实现批量更新则需要多条SQL语句拼起来,用分号分开。也就是一次性发送多条SQL语句让数据库执行。此时需要在数据库连接信息的URL地址中设置:allowMultiQueries=true
atguigu.dev.url=jdbc:mysql://192.168.198.100:3306/mybatis0922?allowMultiQueries=true
对应的foreach标签如下:
<update id="updateEmployeeBatch">
<foreach collection="empList" item="emp" separator=";">
update t_emp set emp_name=#{emp.empName} where emp_id=#{emp.empId}
foreach>
update>
如果没有给接口中List类型的参数使用@Param注解指定一个具体的名字,那么在collection属性中默认可以使用collection或list来引用这个list集合。这一点可以通过异常信息看出来:
Parameter 'empList' not found. Available parameters are [collection, list]
在实际开发中,为了避免隐晦的表达造成一定的误会,建议使用@Param注解明确声明变量的名称,然后在foreach标签的collection属性中按照@Param注解指定的名称来引用传入的参数。
<insert id="batchInsert">
insert into t_emp(emp_name, emp_salary)
<foreach collection="empList" item="emp" separator="," open="values">
(#{emp.empName},#{emp.empSalary})
foreach>
insert>
@Test
public void testBatchInsert() {
EmployeeMapper customerMapper = session.getMapper(EmployeeMapper.class);
List<Emp> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Emp emp = new Emp(null, "tiger_" + i, i * 1000.00);
list.add(emp);
}
customerMapper.batchInsert(list);
}
<sql id="mySelectSql">
select emp_id,emp_name,emp_age,emp_salary,emp_gender from t_emp
sql>
<include refid="mySelectSql"/>
理解缓存的工作机制和缓存的用途。
为了节约时间,节约性能,一个数据集查出来后把他放在缓存里面,下次需要的时候直接获取。
缓存一定是用在查询过程中的,增删改后数据库数据和缓存不一致,很多情况执行增删改后会清空缓存。
查询的顺序是:
它们之间范围的大小参考下面图:
@Test
public void testFirstLevelCache() {
SqlSession session = factory.openSession();
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Integer empId = 1;
Emp emp01 = mapper.selectEmpById(empId);
System.out.println("第一次查询emp01=" + emp01.toString());
Emp emp02 = mapper.selectEmpById(empId);
System.out.println("第二次查询emp02=" + emp02.toString());
session.commit();
session.close();
}
打印结果:
一级缓存的应用范围就是在同一个事务中,连续两次相同的查询操作才会用到,故一级缓存应用范围不大。
这里使用的是Mybatis自带的二级缓存。
在想要使用二级缓存的Mapper配置文件中加入cache标签
<mapper namespace="com.stonebridge.MyBatis.dao.EmployeeMapper">
<cache/>
mapper>
public class Emp implements Serializable {
……
}
未启用二级缓存
查询后SqlSession关闭时,一级缓存中的数据会写入二级缓存。此时后续相同查询才会使用二级缓存。如果未关闭SqlSession则不会将一级缓存写入二级缓存。
示例:
@Test
public void testSecondLevelCache() {
// 测试二级缓存存在:使用两个不同SqlSession执行查询
// 说明:SqlSession提交事务时才会将查询到的数据存入二级缓存
// 所以本例并没有能够成功从二级缓存获取到数据
SqlSession session01 = factory.openSession();
SqlSession session02 = factory.openSession();
EmployeeMapper mapper01 = session01.getMapper(EmployeeMapper.class);
EmployeeMapper mapper02 = session02.getMapper(EmployeeMapper.class);
Integer empId = 1;
// DEBUG 11-01 16:59:15,933 Cache Hit Ratio [com.stonebridge.MyBatis.dao.EmployeeMapper]: 0.0
Emp emp01 = mapper01.selectEmpById(empId);
// DEBUG 11-01 16:59:16,630 Cache Hit Ratio [com.stonebridge.MyBatis.dao.EmployeeMapper]: 0.0
Emp emp02 = mapper02.selectEmpById(empId);
System.out.println("emp01 = " + emp01);
System.out.println("emp02 = " + emp02);
session01.commit();
session01.close();
session02.commit();
session02.close();
}
启用二级缓存
@Test
public void testSecondLevelCacheWork() {
SqlSession session = factory.openSession();
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Integer empId = 1;
// 第一次查询
Emp emp01 = employeeMapper.selectEmpById(empId);
System.out.println("emp01 = " + emp01);
// 提交事务
session.commit();
// 关闭旧SqlSession
session.close();
// 开启新SqlSession
session = factory.openSession();
// 第二次查询
employeeMapper = session.getMapper(EmployeeMapper.class);
Emp emp02 = employeeMapper.selectEmpById(empId);
System.out.println("emp02 = " + emp02);
session.commit();
session.close();
session = factory.openSession();
employeeMapper = session.getMapper(EmployeeMapper.class);
employeeMapper.selectEmpById(empId);
session.commit();
session.close();
}
结论:SqlSession关闭的时候,一级缓存中的内容会被存入二级缓存
// 1.开启两个SqlSession
SqlSession session01 = factory.openSession();
SqlSession session02 = factory.openSession();
// 2.获取两个EmployeeMapper
EmployeeMapper employeeMapper01 = session01.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper02 = session02.getMapper(EmployeeMapper.class);
// 3.使用两个EmployeeMapper做两次查询,返回两个Employee对象
Employee employee01 = employeeMapper01.selectEmployeeById(2);
Employee employee02 = employeeMapper02.selectEmployeeById(2);
// 4.比较两个Employee对象
System.out.println("employee02.equals(employee01) = " + employee02.equals(employee01));
上面代码打印的结果是:
DEBUG 12-01 10:10:32,209 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0 (LoggingCache.java:62)
DEBUG 12-01 10:10:32,570 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,624 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,643 <== Total: 1 (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,644 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0 (LoggingCache.java:62)
DEBUG 12-01 10:10:32,661 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,662 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,665 <== Total: 1 (BaseJdbcLogger.java:145)
employee02.equals(employee01) = false
修改代码:
// 1.开启两个SqlSession
SqlSession session01 = factory.openSession();
SqlSession session02 = factory.openSession();
// 2.获取两个EmployeeMapper
EmployeeMapper employeeMapper01 = session01.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper02 = session02.getMapper(EmployeeMapper.class);
// 3.使用两个EmployeeMapper做两次查询,返回两个Employee对象
Employee employee01 = employeeMapper01.selectEmployeeById(2);
// ※第一次查询完成后,把所在的SqlSession关闭,使一级缓存中的数据存入二级缓存
session01.close();
Employee employee02 = employeeMapper02.selectEmployeeById(2);
// 4.比较两个Employee对象
System.out.println("employee02.equals(employee01) = " + employee02.equals(employee01));
// 5.另外一个SqlSession用完正常关闭
session02.close();
打印结果:
DEBUG 12-01 10:14:06,804 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0 (LoggingCache.java:62)
DEBUG 12-01 10:14:07,135 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,202 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,224 <== Total: 1 (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,308 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.5 (LoggingCache.java:62)
employee02.equals(employee01) = false
在Mapper配置文件中添加的cache标签可以设置一些属性:
eviction属性:缓存回收策略
LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认的是 LRU。
flushInterval属性:刷新间隔,单位毫秒
默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
size属性:引用数目,正整数
代表缓存最多可以存储多少个对象,太大容易导致内存溢出
readOnly属性:只读,true/false
true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。
官网地址:https://www.ehcache.org/
Ehcache is an open source, standards-based cache that boosts performance, offloads your database, and simplifies scalability. It’s the most widely-used Java-based cache because it’s robust, proven, full-featured, and integrates with other popular libraries and frameworks. Ehcache scales from in-process caching, all the way to mixed in-process/out-of-process deployments with terabyte-sized caches.
在Mybatis环境下整合EHCache,前提当然是要先准备好Mybatis的环境。
依赖信息
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-ehcacheartifactId>
<version>1.2.1version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
依赖传递情况
各主要jar包作用
jar包名称 | 作用 |
---|---|
mybatis-ehcache | Mybatis和EHCache的整合包 |
ehcache | EHCache核心包 |
slf4j-api | SLF4J日志门面包 |
logback-classic | 支持SLF4J门面接口的一个具体实现 |
创建EHCache配置文件
ehcache.xml
文件内容
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<diskStore path="D:\atguigu\ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
defaultCache>
ehcache>
引入第三方框架或工具时,配置文件的文件名可以自定义吗?
- 可以自定义:文件名是由我告诉其他环境
- 不能自定义:文件名是框架内置的、约定好的,就不能自定义,以避免框架无法加载这个文件
指定缓存管理器的具体类型
还是到查询操作所的Mapper配置文件中,找到之前设置的cache标签:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
存在SLF4J时,作为简易日志的log4j将失效,此时我们需要借助SLF4J的具体实现logback来打印日志。
各种Java日志框架简介
门面:
名称 | 说明 |
---|---|
JCL(Jakarta Commons Logging) | 陈旧 |
SLF4J(Simple Logging Facade for Java)★ | 适合 |
jboss-logging | 特殊专业领域使用 |
实现:
名称 | 说明 |
---|---|
log4j★ | 最初版 |
JUL(java.util.logging) | JDK自带 |
log4j2 | Apache收购log4j后全面重构,内部实现和log4j完全不同 |
logback★ | 优雅、强大 |
注:标记★的技术是同一作者。
Logback配置文件
<configuration debug="true">
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%npattern>
encoder>
appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
root>
<logger name="com.atguigu.crowd.mapper" level="DEBUG"/>
configuration>
正常按照二级缓存的方式测试即可。因为整合EHCache后,其实就是使用EHCache代替了Mybatis自带的二级缓存。
@Test
public void testSecondLevelCacheWork() {
SqlSession session = factory.openSession();
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
Integer empId = 1;
// 第一次查询
Emp emp01 = employeeMapper.selectEmpById(empId);
System.out.println("emp01 = " + emp01);
// 提交事务
session.commit();
// 关闭旧SqlSession
session.close();
// 开启新SqlSession
session = factory.openSession();
// 第二次查询
employeeMapper = session.getMapper(EmployeeMapper.class);
Emp emp02 = employeeMapper.selectEmpById(empId);
System.out.println("emp02 = " + emp02);
session.commit();
session.close();
session = factory.openSession();
employeeMapper = session.getMapper(EmployeeMapper.class);
employeeMapper.selectEmpById(empId);
session.commit();
session.close();
}
当借助CacheManager.add(“缓存名称”)创建Cache时,EhCache便会采用
defaultCache标签各属性说明:
属性名 | 是否必须 | 作用 |
---|---|---|
maxElementsInMemory | 是 | 在内存中缓存的element的最大数目 |
maxElementsOnDisk | 是 | 在磁盘上缓存的element的最大数目,若是0表示无穷大 |
eternal | 是 | 设定缓存的elements是否永远不过期。 如果为true,则缓存的数据始终有效, 如果为false那么还要根据timeToIdleSeconds、timeToLiveSeconds判断 |
overflowToDisk | 是 | 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 |
timeToIdleSeconds | 否 | 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时, 这些数据便会删除,默认值是0,也就是可闲置时间无穷大 |
timeToLiveSeconds | 否 | 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大 |
diskSpoolBufferSizeMB | 否 | DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区 |
diskPersistent | 否 | 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。 |
diskExpiryThreadIntervalSeconds | 否 | 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s, 相应的线程会进行一次EhCache中数据的清理工作 |
memoryStoreEvictionPolicy | 否 | 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。 默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出) |
org.apache.ibatis.cache.Cache接口:所有缓存都必须实现的顶级接口
org.apache.ibatis.cache.impl.PerpetualCache:mybatis一级缓存和二级缓存都需要的用到的类
缓存基本都是键值对结构,查询条件不同,去的值不同。根据Cache接口中方法的声明我们能够看到,缓存的本质是一个Map。
方法名 | 作用 |
---|---|
putObject() | 将对象存入缓存 |
getObject() | 从缓存中取出对象 |
removeObject() | 从缓存中删除对象 |
org.apache.ibatis.cache.impl.PerpetualCache是Mybatis的默认缓存,也是Cache接口的默认实现。Mybatis一级缓存和自带的二级缓存都是通过PerpetualCache来操作缓存数据的。但是这就奇怪了,同样是PerpetualCache这个类,怎么能区分出来两种不同级别的缓存呢?
其实很简单,调用者不同。
org.apache.ibatis.executor.BaseExecutor类中的关键方法:
BaseExecutor实现org.apache.ibatis.executor.Executor接口
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 尝试从本地缓存中获取数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果本地缓存中没有查询到数据,则查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (org.apache.ibatis.executor.BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 从数据库中查询数据
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将数据存入本地缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
正向工程:先创建Java实体类,由框架负责根据实体类生成数据库表。Hibernate是支持正向工程的。
逆向工程:先创建数据库表,由框架负责根据数据库表,反向生成如下资源:
<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">
<parent>
<artifactId>MyBatis-ProartifactId>
<groupId>org.stonebridgegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>mybatis-MBGartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.7version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.0version>
<dependencies>
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.8version>
dependency>
dependencies>
plugin>
plugins>
build>
project>
文件名必须是:generatorConfig.xml
注意数据库的链接信息和目录名称
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis-example"
userId="root"
password="123456">
jdbcConnection>
<javaModelGenerator targetPackage="com.stonebridge.mybatis.entity" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
javaModelGenerator>
<sqlMapGenerator targetPackage="com.stonebridge.mybatis.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.stonebridge.mybatis.mapper"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true"/>
javaClientGenerator>
<table tableName="t_emp" domainObjectName="Employee"/>
<table tableName="t_customer" domainObjectName="Customer"/>
<table tableName="t_order" domainObjectName="Order"/>
context>
generatorConfiguration>
QBC:Query By Criteria
QBC查询最大的特点就是将SQL语句中的WHERE子句进行了组件化的封装,让我们可以通过调用Criteria对象的方法自由的拼装查询条件。
@Test
public void testQBC() {
// 目标:组装查询条件WHERE (xxx and xxx) or (xxx and xxx)
// 1.创建Example对象
EmployeeExample example = new EmployeeExample();
// 2.根据Example对象创建Criteria对象
EmployeeExample.Criteria criteria01 = example.createCriteria();
EmployeeExample.Criteria criteria02 = example.or();
// 3.在Criteria对象中添加查询条件
// ①emp_name like %o% and emp_salary > 6000.00
criteria01.andEmpNameLike("%o%").andEmpSalaryGreaterThan(6000.00);
// ②emp_name like %t% and emp_salary < 3000.00
criteria02.andEmpNameLike("%t%").andEmpSalaryLessThan(3000.00);
// 4.根据Example对象执行查询
EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
List<Employee> employeeList = employeeMapper.selectByExample(example);
for (Employee employee : employeeList) {
System.out.println("employee = " + employee);
}
// 最终运行的结果:WHERE ( emp_name like %o% and emp_salary > 6000.00) or( emp_name like %t% and emp_salary < 3000.00 )
}
// 1.创建EmployeeExample对象
EmployeeExample example = new EmployeeExample();
// 2.通过example对象创建Criteria对象
EmployeeExample.Criteria criteria01 = example.createCriteria();
EmployeeExample.Criteria criteria02 = example.or();
// 3.在Criteria对象中封装查询条件
criteria01.andEmpAgeBetween(9, 99).andEmpNameLike("%o%").andEmpGenderEqualTo("male").andEmpSalaryGreaterThan(500.55);
criteria02.andEmpAgeBetween(9, 99).andEmpNameLike("%o%").andEmpGenderEqualTo("male").andEmpSalaryGreaterThan(500.55);
SqlSession session = factory.openSession();
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
// 4.基于Criteria对象进行查询
List<Employee> employeeList = mapper.selectByExample(example);
for (Employee employee : employeeList) {
System.out.println("employee = " + employee);
}
session.close();
// 最终SQL的效果:
// WHERE ( emp_age between ? and ? and emp_name like ? and emp_gender = ? and emp_salary > ? ) or( emp_age between ? and ? and emp_name like ? and emp_gender = ? and emp_salary > ? )
让Mapper配置文件中使用的实体类类型名称更简洁。让Mapper配置文件中使用的实体类类型名称更简洁。
<typeAliases>
<package name="com.atguigu.mybatis.entity"/>
typeAliases>
在Mapper文件中配置对应的resultType时就不需要,配置全限定名。只需要使用简单类名即可。
<select id="selectEmployeeById" resultType="Employee">
select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp
where emp_id=#{empId}
select>
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
类型处理器 | Java类型 | JDBC类型 |
---|---|---|
BooleanTypeHandler | Boolean,boolean | 任何兼容的布尔值 |
ByteTypeHandler | Byte,byte | 任何兼容的数字或字节类型 |
ShortTypeHandler | Short,short | 任何兼容的数字或短整型 |
IntegerTypeHandler | Integer,int | 任何兼容的数字和整型 |
LongTypeHandler | Long,long | 任何兼容的数字或长整型 |
FloatTypeHandler | Float,float | 任何兼容的数字或单精度浮点型 |
DoubleTypeHandler | Double,double | 任何兼容的数字或双精度浮点型 |
BigDecimalTypeHandler | BigDecimal | 任何兼容的数字或十进制小数类型 |
StringTypeHandler | String | CHAR和VARCHAR类型 |
ClobTypeHandler | String | CLOB和LONGVARCHAR类型 |
NStringTypeHandler | String | NVARCHAR和NCHAR类型 |
NClobTypeHandler | String | NCLOB类型 |
ByteArrayTypeHandler | byte[] | 任何兼容的字节流类型 |
BlobTypeHandler | byte[] | BLOB和LONGVARBINARY类型 |
DateTypeHandler | Date(java.util) | TIMESTAMP类型 |
DateOnlyTypeHandler | Date(java.util) | DATE类型 |
TimeOnlyTypeHandler | Date(java.util) | TIME类型 |
SqlTimestampTypeHandler | Timestamp(java.sql) | TIMESTAMP类型 |
SqlDateTypeHandler | Date(java.sql) | DATE类型 |
SqlTimeTypeHandler | Time(java.sql) | TIME类型 |
ObjectTypeHandler | 任意 | 其他或未指定类型 |
EnumTypeHandler | Enumeration类型 | VARCHAR。任何兼容的字符串类型,作为代码存储(而不是索引) |
Mybatis允许在指定Mapper映射文件时,只指定其所在的包:
<mappers>
<package name="com.atguigu.mybatis.dao"/>
mappers>
此时这个包下的所有Mapper配置文件将被自动加载、注册,比较方便。
但是,要求是:
如果工程是Maven工程,那么Mapper配置文件还是要放在resources目录下: