一、Mybatis
学习版本:3.4.2
Mybatis封装了JDBC的操作,使用户专注于SQL语句的编写
GITEE仓库地址
①、pom.xml
创建普通的 maven 工程,修改 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>
<groupId>com.micozone</groupId>
<artifactId>mybatis3.4.2</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.2</version>
</dependency>
<!--Mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
②、建表、entity、mapper
建库:mybatis,建表:mybatis_table
CREATE TABLE `mybatis_table` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(100) DEFAULT NULL,
`user_age` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
MybatisTable 类
@Data
public class MybatisTable {
private Long id;
private String userName;
private int userAge;
}
MybatisTableMapper 接口
public interface MybatisTableMapper {
@Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
MybatisTable selectByIdAndName(Long id, String userName);
}
MybatisTableMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.MybatisTableMapper">
</mapper>
③、mybatis 配置文件
resources/mybatis.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>
<properties resource="test/jdbc.properties">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
</properties>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<package name="com.micozone.mybatis.entity"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url"
value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="${username}"/>
<property name="password" value="jrbjrb811"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.micozone.mybatis.mapper"/>
</mappers>
</configuration>
resources/test/jdbc.properties
username=root
④、测试
测试类
Reader reader = Resources.getResourceAsReader("mybatis.xml");
// 第一步、创建SqlSessionFactory
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 第二步、创建SqlSession
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
// 第三步、获取mapper接口的动态代理对象
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 第四步、调用方法访问数据库
MybatisTable mybatisTable = mybatisTableMapperProxy.selectByIdAndName(1L, "micozone");
二、JDBC
1、概念
JDBC 是 Java 提供的访问数据库(数据源)的接口(规范),数据库厂商根据 JDBC 规范完成 JDBC 的驱动程序,从而实现 Java 对数据库的访问;
2、原理
1、java.sql.Driver 接口、java.sql.DriverManager 类
所有的 JDBC 驱动程序都必须提供一个实现 java.sql.Driver 接口的实现类,并且必须提供一个静态代码块,代码块中向 DriverManager 注册自己的实例。以 mysql-connector-java-8.0.15为例:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// Register ourselves with the DriverManager
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
......
}
只有成功的向 DriverManager 中注册驱动,才可以通过 DriverManager.getConnection(…) 方法获取 Connection 对象,所以一般情况下,需要显式的加载驱动实现类,如:Class.forName(“com.mysql.cj.jdbc.Driver”); 由于类加载到内存中会执行静态代码块,从而完成向 DriverManager 中注册驱动。但 JDK 提供了一种 SPI(Service Provider Interface) 机制,使我们不用显式的加载驱动实现类,直接通过 DriverManager.getConnection(…); 也能加载驱动实现类。
2、SPI
SPI 机制需要配合 java.util.ServiceLoader 类使用;
第一步:服务提供者,提供 java.sql.Driver 接口的实现类 com.mysql.cj.jdbc.Driver
第二步:在 classpath(resources) 目录下新建 META-INF/services 目录,创建文件名为 java.sql.Driver,文件内容为 com.mysql.cj.jdbc.Driver
第三步:java.sql.DriverManager 的静态代码块中,使用 ServiceLoader.load(java.sql.Driver.class); 方法,加载驱动实现类 com.mysql.cj.jdbc.Driver,从而执行驱动实现类的静态代码块:向 DriverManager 中注册自己的实例。
java.sql.DriverManager
public class DriverManager {
......
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
......
}
3、java.sql.Connection 接口
成功创建 Connection 接口对象,代表与数据库成功建立连接;
创建方式一:使用 DriverManager
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
"root",
"password");
创建方式二:使用 DataSource(推荐)
// 一个 DataSource 代表一个数据源
// UnpooledDataSource 是 mybatis 提供的 DataSource 接口的实现类
DataSource dataSource = new UnpooledDataSource(
"com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
"root",
"password");
Connection connection = dataSource.getConnection();
最后要调用 close(); 方法关闭 Connection 对象。
4、java.sql.PreparedStatement 接口
通过 connection.prepareStatement(String sql); 方法获取 PreparedStatement 对象;
PreparedStatement 是 Statement 接口的实现类,Statement 接口为 SQL 语句的执行器,通过调用方法从而执行 SQL,常用方法有:execute();、executeQuery();、executeUpdate();
PreparedStatement 相比于 Statement 的优点是,PreparedStatement 实例可以表示预编译的 SQL 语句,占位符使用 “?” 代替,不会出现 SQL 注入的风险。
在使用 setXXX(); 方法为占位符设置值时,存在将 Java 数据类型转换为 JDBC 类型的过程,这一过程由 JDBC 驱动程序实现,如:
Java 类型 | JDBC 类型 |
---|---|
int | INTEGER |
Long | BIGINT |
最后要调用 close(); 方法关闭 preparedStatement 对象。
PS:调用存储过程使用 CallableStatement 接口;
5、java.sql.ResultSet 接口
用于解析 SQL 执行结果。
6、JDBC 事务
事务是为了保证多个 SQL 语句同时执行成功或者同时执行失败;
创建 Connection 对象时,默认事务是自动提交的。当调用 Connection 对象的 void setAutoCommit(boolean autoCommit); 方法,参数传递 false 时,禁用事务自动提交,此时必须显式的调用 connection.commit(); 方法提交事务或者 connection.rollback(); 方法回滚事务;
7、事务隔离级别
事务隔离级别代表一个事务对数据的操作对另一个事务的“可见性”。
级别 | 解析 |
---|---|
读未提交 | 脏读,第一个事务未提交的数据,第二个事务可以读取到 |
读已提交 | Oracle 默认级别,不可重复读,第一个事务读到到某行数据,第二个事务修改改行数据并提交,第一个事务再读取该行数据,此时会与第一次读取的结果不同 |
可重复读 | MySQL 默认级别,幻读,某行数据无论另一个事务是否修改过都不会出现不可重复读现象,但是如果查询结果是多行数据,此时会出现幻读现象 |
序列化读 | 事务串行执行,但是并发率低 |
3、使用
package com.micozone.mybatis.test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 使用 JDBC 访问数据库流程
*
* @author Mico Zone
*/
public class JDBC {
public static void main(String[] args) throws Exception {
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
"root",
"password");
String sql = "select * from mybatis_table where id = ? and user_name = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, 1L);
preparedStatement.setString(2, "micozone");
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getLong(1)); // 1
System.out.println(resultSet.getString(2)); // micozone
System.out.println(resultSet.getInt(3)); // 27
}
}
}
三、MyBatis中的工具类
1、SQL 类—拼接 SQL
【MyBatis 3 源码深度解析】P59
Connection connection = getConnection();
String sql = new SQL()
.SELECT("id,user_name,user_age")
.FROM("mybatis_table")
.WHERE("id = ?")
.toString();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, 1L);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getLong(1));
System.out.println(resultSet.getString(2));
System.out.println(resultSet.getInt(3));
}
2、ScriptRunner 类—执行 SQL 脚本
Connection connection = getConnection();
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.runScript(Resources.getResourceAsReader("test/sqlScript.sql"));
resources/test/sqlScript.sql
INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(504, 'zs', 21);
INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(505, 'ls', 34);
INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(600, 'ls', 34),(601, 'ls', 34);
3、SqlRunner 类—执行 SQL 语句
Connection connection = getConnection();
SqlRunner sqlRunner = new SqlRunner(connection);
String selectSql = new SQL()
.SELECT("id,user_name as userName,user_age as userAge")
.FROM("mybatis_table")
.WHERE("id = ?").toString();
Map<String, Object> selectResult = sqlRunner.selectOne(selectSql, 1L);
// selectResult: {USERAGE=27, USERNAME=micozone, ID=1}
System.out.println("selectResult: " + selectResult);
String deleteSql = new SQL()
.DELETE_FROM("mybatis_table")
.WHERE("id = ?")
.toString();
int deleteResult = sqlRunner.delete(deleteSql, 500L);
// deleteResult: 1
System.out.println("deleteResult: " + deleteResult);
String updateSql = new SQL()
.UPDATE("mybatis_table")
.SET("user_name = ?")
.SET("user_age = ?")
.WHERE("id = ?")
.toString();
int updateResult = sqlRunner.update(updateSql, "12", 34, 501L);
// updateResult: 1
System.out.println("updateResult: " + updateResult);
String insertSql = new SQL()
.INSERT_INTO("mybatis_table")
.INTO_COLUMNS("id,user_name,user_age")
.INTO_VALUES("?,?,?")
.toString();
int insertResult = sqlRunner.insert(insertSql, 700L, "qwe", 14);
// insertResult: -2147482647
System.out.println("insertResult: " + insertResult);
4、MetaObject—通过反射获取/设置属性值
List<Book> bookList = new ArrayList<>();
bookList.add(new Book("水浒传"));
bookList.add(new Book("红楼梦"));
BookShop bookShop = new BookShop("新华书店", bookList);
MetaObject metaObject = SystemMetaObject.forObject(bookShop);
System.out.println("第一本书:" + metaObject.getValue("bookList[0].bookName"));
metaObject.setValue("bookList[1].bookName", "三国演义");
System.out.println("第二本书:" + metaObject.getValue("bookList[1].bookName"));
System.out.println("BookShop是否有bookList属性且有getter方法:" + metaObject.hasGetter("bookList"));
System.out.println("BookShop是否有shopName属性且有setter方法:" + metaObject.hasSetter("shopName"));
Book.java
@Data
@AllArgsConstructor
class Book {
private String bookName;
public Book() {
}
}
BookShop.java
@Data
@AllArgsConstructor
class BookShop {
private String shopName;
private List<Book> bookList;
public BookShop() {
}
}
5、MetaClass—通过反射获取类相关信息
MetaClass metaClass = MetaClass.forClass(BookShop.class, new DefaultReflectorFactory());
// 是否有shopName属性:shopName
System.out.println("是否有shopName属性:" + metaClass.findProperty("shopName"));
// 是否有abc属性:null
System.out.println("是否有abc属性:" + metaClass.findProperty("abc"));
// 所有有getter方法的属性名:[shopName, bookList]
System.out.println("所有有getter方法的属性名:" + Arrays.toString(metaClass.getGetterNames()));
// 所有有setter方法的属性名:[shopName, bookList]
System.out.println("所有有setter方法的属性名:" + Arrays.toString(metaClass.getSetterNames()));
6、ObjectFactory—通过反射创建对象
ObjectFactory objectFactory = new DefaultObjectFactory();
BookShop noParamBookShop = objectFactory.create(BookShop.class);
// noParamBookShop: BookShop(shopName=null, bookList=null)
System.out.println("noParamBookShop: " + noParamBookShop);
List<Book> bookList = new ArrayList<>();
bookList.add(new Book("水浒传"));
bookList.add(new Book("红楼梦"));
List<Class<?>> constructorArgTypes = new ArrayList<>();
constructorArgTypes.add(String.class);
constructorArgTypes.add(List.class);
List<Object> constructorArgs = new ArrayList<>();
constructorArgs.add("新华书店");
constructorArgs.add(bookList);
BookShop bookShop = objectFactory.create(BookShop.class, constructorArgTypes, constructorArgs);
// bookShop: BookShop(shopName=新华书店, bookList=[Book(bookName=水浒传), Book(bookName=红楼梦)])
System.out.println("bookShop: " + bookShop);
7、XPathParser—解析 xml 文件
XPathParser 封装了 XPath,从而完成对 xml 文件的解析。
Reader reader = Resources.getResourceAsReader("test/pens.xml");
XPathParser xPathParser = new XPathParser(reader);
List<Pen> penList = new ArrayList<>();
List<XNode> pensXNodeList = xPathParser.evalNodes("/pens/*");
for (XNode penXNode : pensXNodeList) {
Pen pen = new Pen();
Long id = penXNode.getLongAttribute("id");
pen.setId(id);
List<XNode> penChildrenXNodeList = penXNode.getChildren();
for (XNode childrenXNode : penChildrenXNodeList) {
String name = childrenXNode.getName();
if ("name".equals(name)) {
pen.setName(childrenXNode.getStringBody());
} else if ("price".equals(name)) {
pen.setPrice(childrenXNode.getIntBody());
}
}
penList.add(pen);
}
System.out.println(penList);
resources/test/pens.xml
<?xml version="1.0" encoding="UTF-8" ?>
<pens>
<pen id="1">
<name>晨光</name>
<price>3</price>
</pen>
<pen id="2">
<name>三菱</name>
<price>8</price>
</pen>
</pens>
四、创建 SqlSessionFactory 对象
Reader reader = Resources.getResourceAsReader("mybatis.xml");
// 第一步、创建SqlSessionFactory
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
大体分为两步,第一步:解析 mybatis 配置文件,将内容封装到 Configuration 对象中;第二步:创建 DefaultSqlSessionFactory 对象,将第一步创建的 Configuration 对象赋给 DefaultSqlSessionFactory 对象的 Configuration 属性。
1、解析 mybatis 配置文件
sqlSessionFactoryBuilder.build();
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
......
// XMLConfigBuilder 底层通过 XPathParser 对象解析 mybatis 配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// Configuration config = parser.parse();
return build(parser.parse());
......
}
xmlConfigBuilder.parse();
public Configuration parse() {
......
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
xmlConfigBuilder.parseConfiguration();
private void parseConfiguration(XNode root) {
/*
一、解析 <properties> 标签
<properties resource="test/jdbc.properties">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
</properties>
会将jdbc.properties文件中的键值对、以及所有的<property>标签的键值对封装到 Configuration.variables,
其中 Properties variables = new Properties();
*/
propertiesElement(root.evalNode("properties"));
/*
二、解析 <settings> 标签
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="cacheEnabled" value="true"/>
</settings>
会将所有的<setting>标签的键值对封装到 Configuration 对象的对应属性中,如:
<setting name="logImpl" value="STDOUT_LOGGING"/> ===> configuration.logImpl = StdOutImpl.class
<setting name="cacheEnabled" value="true"/> ===> configuration.cacheEnabled = true
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
/*
三、解析 <typeAliases> 标签
<typeAliases>
<package name="com.micozone.mybatis.entity"/>
</typeAliases>
会将<package>标签对应的包名下的所有实体类以及对应别名存放到 configuration.typeAliasRegistry.TYPE_ALIASES 中,
其中 Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
*/
typeAliasesElement(root.evalNode("typeAliases"));
/*
四、解析 <plugins> 标签
<plugins>
<plugin interceptor="com.micozone.mybatis.mybatis.MyPageInterceptor">
<property name="name" value="zs"/>
<property name="age" value="15"/>
</plugin>
</plugins>
MyPageInterceptor 类要实现 Interceptor 接口,并且要有 @Intercepts 注解,插件原理放在第 八 部分。
这里会将 MyPageInterceptor 类的对象放到 configuration.interceptorChain.interceptors 中,
其中 List<Interceptor> interceptors = new ArrayList<Interceptor>();
*/
pluginElement(root.evalNode("plugins"));
/*
五、解析 <objectFactory> 标签
<objectFactory type="com.micozone.mybatis.mybatis.MyObjectFactory">
<property name="zs" value="14"/>
</objectFactory>
MyObjectFactory 类最好继承 DefaultObjectFactory 类,DefaultObjectFactory 对象的作用是创建 Mapper 映射结果对象,
这里会对 configuration 对象的属性赋值:configuration.objectFactory = new MyObjectFactory();
其中 ObjectFactory objectFactory = new DefaultObjectFactory();
@Data
public class MyObjectFactory extends DefaultObjectFactory {
private Properties properties;
@Override
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
if (type.equals(MybatisTable.class)) {
System.out.println("MyObjectFactory.create() execute 正在创建 MybatisTable 对象");
}
return super.create(type, constructorArgTypes, constructorArgs);
}
}
*/
objectFactoryElement(root.evalNode("objectFactory"));
/*
六、objectWrapperFactory TODO
*/
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
/*
七、reflectorFactory TODO
*/
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
/*
八、解析 <environments> 标签
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url"
value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="${username}"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
这里会对 configuration 对象的属性赋值:configuration.environment = new Environment();
根据<transactionManager>、<dataSource>标签创建 Environment 对象:
<transactionManager type="JDBC"/> ===> environment.transactionFactory = new JdbcTransactionFactory();
<dataSource type="POOLED"><property ...>...</dataSource> ===> environment.dataSource = new PooledDataSource();
*/
environmentsElement(root.evalNode("environments"));
/*
九、databaseIdProvider TODO
*/
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
/*
十、typeHandlers TODO
*/
typeHandlerElement(root.evalNode("typeHandlers"));
/*
十一、解析 <mappers> 标签
<mappers>
<!--<package name="com.micozone.mybatis.mapper"/>-->
<mapper class="com.micozone.mybatis.mapper.BuyerMapper"/>
</mappers>
解析 <mappers> 标签步骤较多,放到 七-1 部分
*/
mapperElement(root.evalNode("mappers"));
}
2、创建 DefaultSqlSessionFactory 对象
sqlSessionFactoryBuilder.build();
public SqlSessionFactory build(Configuration config) {
// defaultSqlSessionFactory.configuration = config;
return new DefaultSqlSessionFactory(config);
}
五、创建 SqlSession 对象
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 第二步、创建SqlSession
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
大体分为两步,第一步:创建 Executor 对象;第二步:创建 DefaultSqlSession 对象,将 executor 赋给 defaultSqlSession 的 executor 属性。
1、创建 Executor 对象
defaultSqlSessionFactory.openSession();
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
defaultSqlSessionFactory.openSessionFromDataSource();
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
// 在解析 <environments> 标签时,对 configuration 的 environment 属性赋值
final Environment environment = configuration.getEnvironment();
// <transactionManager type="JDBC"/> ===> JdbcTransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// JdbcTransaction,其中<dataSource type="POOLED">... ===> PooledDataSource
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
/*
如果<setting name="cacheEnabled" value="true"/>,默认为true:executor = new CachingExecutor(); executor.delegate = new SimpleExecutor();
如果<setting name="cacheEnabled" value="false"/>:executor = SimpleExecutor;
*/
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
其中 Executor、Transaction、DataSource的关系:
2、创建 DefaultSqlSession 对象
defaultSqlSessionFactory.openSessionFromDataSource();
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
......
return new DefaultSqlSession(configuration, executor, autoCommit);
}
DefaultSqlSession 的构造函数
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
六、创建 Mapper 接口的代理对象
大体分为两步,第一步:获取 MapperProxyFactory 对象;第二步:通过 mapperProxyFactory 对象创建 Mapper 接口的动态代理对象。
1、获取 MapperProxyFactory 对象
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
// 第三步、获取mapper接口的动态代理对象
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
defaultSqlSession.getMapper();
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
configuration.getMapper();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
mapperRegistry.getMapper();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 解析 <mappers> 标签时,针对每一个 Mapper 接口:configuration.mapperRegistry.knownMappers.put(Mapper.class, new MapperProxyFactory<T>(Mapper.class));
// 此处取到 mapperProxyFactory 对象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
return mapperProxyFactory.newInstance(sqlSession);
}
2、创建 Mapper 接口的动态代理对象
mapperProxyFactory.newInstance(SqlSession sqlSession);
public T newInstance(SqlSession sqlSession) {
// mapperInterface = Mapper.class
// MapperProxy 类实现 InvocationHandler 接口,即 MapperProxy 对象为 Mapper 接口动态代理对象的调用处理程序
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
mapperProxyFactory.newInstance(MapperProxy mapperProxy);
protected T newInstance(MapperProxy<T> mapperProxy) {
// 这里创建 Mapper 接口的动态代理对象,通过该对象调用方法时,实际上会调用 mapperProxy 对象的 invoke(); 方法
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy);
}
七、调用 Mapper 接口的代理对象的方法
此处在介绍 “调用代理对象的方法的执行流程” 前,先介绍一下 四-1 中遗留的 mappers 标签的解析流程。
1、mappers 标签解析
mybatis.xml
<mappers>
<package name="com.micozone.mybatis.mapper"/>
</mappers>
xmlConfigBuilder.parseConfiguration();
private void parseConfiguration(XNode root) {
......
mapperElement(root.evalNode("mappers"));
}
大体分为三步,第一步:将 Mapper 接口添加到 knownMappers 缓存中;第二步:loadXmlResource(); 第三步:加载 Mapper 接口中含有 @Select、@Insert、@Update、@Delete 注解的方法。
①、将 Mapper 接口添加到 knownMappers 缓存中
mapperRegistry.addMappers();
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
// 遍历指定包下的所有 Mapper 接口
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
mapperRegistry.addMapper();
public <T> void addMapper(Class<T> type) {
// 其中 Map<Class, MapperProxyFactory> knownMappers = new HashMap<>();
knownMappers.put(type, new MapperProxyFactory<T>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
}
②、loadXmlResource();
mapperAnnotationBuilder.parse();
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 加载 xml 资源
loadXmlResource();
......
Method[] methods = type.getMethods();
for (Method method : methods) {
parseStatement(method);
}
}
......
}
mapperAnnotationBuilder.loadXmlResource();
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// 默认加载 Mapper 接口所在包下的同名 Mapper.xml 文件
InputStream inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
xmlMapperBuilder.parse();
public void parse() {
...
configurationElement(parser.evalNode("/mapper"));
...
}
xmlMapperBuilder.configurationElement();
private void configurationElement(XNode context) {
cacheRefElement(context.evalNode("cache-ref"));
/*
1、创建 Cache 对象
2、configuration.caches.put(cache.getId(), cache);
其中 Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
*/
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
/*
<resultMap id="baseResultMap" type="mybatisTable">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="userAge" column="user_age" jdbcType="INTEGER"/>
</resultMap>
1、针对每一个<id>、<result>标签,创建 ResultMapping 对象,放到 List<ResultMapping> resultMappings 集合中
2、创建 ResultMap 对象,将 resultMappings 集合赋给 resultMap 对象的 resultMappings 属性
3、configuration.resultMaps.put(resultMap.getId(), resultMap);
其中 Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
*/
resultMapElements(context.evalNodes("/mapper/resultMap"));
/*
<sql id="Base_Column_List">
id,user_name,user_age
</sql>
1、configuration.sqlFragments.put(String id, XNode context);
其中 Map<String, XNode> sqlFragments;
*/
sqlElement(context.evalNodes("/mapper/sql"));
/*
<insert id="insertOne" parameterType="mybatisTable">
insert into mybatis_table (id, user_name, user_age)
values (#{id}, #{userName}, #{userAge})
</insert>
1、针对每一个<select>、<insert>、<update>、<delete>标签,封装 MappedStatement 对象
2、configuration.mappedStatements.put(mappedStatement.getId(), mappedStatement);
其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
*/
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
③、加载 Mapper 接口中的方法
只加载含有 @Select、@Insert、@Update、@Delete 注解的方法。
mapperAnnotationBuilder.parse();
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
......
Method[] methods = type.getMethods();
for (Method method : methods) {
// 处理每一个含有 @Select、@Insert、@Update、@Delete 注解的方法
/*
@Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
MybatisTable selectByIdAndName(Long id, String userName);
1、针对每一个含有 @Select、@Insert、@Update、@Delete 注解的方法,封装 MappedStatement 对象
2、configuration.mappedStatements.put(mappedStatement.getId(), mappedStatement);
其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
*/
parseStatement(method);
}
}
......
}
2、调用代理对象的方法
介绍完 mappers 标签的解析之后,我们知道针对 Mapper.xml 中的每一个标签,都会创建相应的对象放到 configuration 对象的某个缓存中,针对含有 @Select、@Insert、@Update、@Delete 注解的方法,处理逻辑与 Mapper.xml 中的 select、insert、update、delete 标签的处理逻辑大体一致。
再介绍一下 “调用代理对象的方法访问数据库并执行 SQL” 的逻辑。
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 第四步、调用方法访问数据库
MybatisTable mybatisTable = mybatisTableMapperProxy.selectByIdAndName(1L, "micozone");
其中 MybatisTableMapper.java
public interface MybatisTableMapper {
@Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
MybatisTable selectByIdAndName(Long id, String userName);
}
这里调用的主要步骤有:mapperProxy.invoke(); ===> defaultSqlSession.selectOne(); ===> executor.query(); ===> 使用 JDBC 访问数据库
主要针对 executor.query(); 以及 使用 JDBC 访问数据库 这两步进行深度分析。
解析一:executor.query();
mapperProxy.invoke();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
......
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
mapperMethod.execute();
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
...
}
case UPDATE: {
...
}
case DELETE: {
...
}
case SELECT:
...
} else {
// 该方法会对参数进行封装,封装形式见下图
Object param = method.convertArgsToSqlCommandParam(args);
// 由于返回结果是一个实体类对象,此处会调用 sqlSession.selectOne(); 方法
result = sqlSession.selectOne(command.getName(), param);
}
break;
...
}
...
return result;
}
defaultSqlSession.selectOne();
@Override
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.<T>selectList(statement, parameter);
......
}
defaultSqlSession.selectList();
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
// 先根据 语句id,获取 MappedStatement 对象:MappedStatement ms = configuration.mappedStatements.get(id);
// 其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
cachingExecutor.query(…);
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// boundSql 对象封装了 sql 语句信息以及请求参数信息
BoundSql boundSql = ms.getBoundSql(parameterObject);
// cachingExecutor的 query(); 方法主要分为两步,第一步:尝试查询二级缓存,如果二级缓存未开启或者没查询到;第二步:通过装饰者模式,调用 simpleExecutor.query(); 方法
// 通过 mappedStatement、boundSql 等对象封装 cacheKey 对象,cacheKey 对象作为二级缓存的 key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
cachingExecutor.query(…);
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 在解析 mybatis 配置文件的 <mappers> 标签时,除了会将 Mapper 接口的 @CacheNamespace 注解以及 Mapper.xml 中的 <cache> 标签,封装为 cache 对象放到 configuration.caches 缓存中,还会赋给封装的 MappedStatement 对象的 cache 属性。
// 由于二级缓存的数据是存放到 MappedStatement 对象的 cache 属性中,而 MappedStatement 封装的一个 sql 语句的相关信息,所以二级缓存的数据与 SqlSession 的生命周期无关
Cache cache = ms.getCache();
// 如果 cache != null,说明开启了二级缓存
if (cache != null) {
...
// 其中 tcm 是 cachingExecutor的属性,类型为 TransactionalCacheManager,通过该对象去查询、添加二级缓存数据的作用是保证该 sqlSession 在没有提交事务之前,二级缓存中的数据不会更新;
// TransactionalCacheManager 会把查询结果放到 TransactionalCache 对象的某个属性中,sqlSession.commit(); 时,才会把 TransactionalCache 对象的该属性对应的所有查询结果放到 MappedStatement.cache 中;
// 所以既是同一个 sqlSession 同一个请求参数连续查询两次,第二次也不会走二级缓存,但会走一级缓存;
// 只有第一个 sqlSession.commit(); 第二个 sqlSession 同样地请求参数再次查询时会走二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// delegate 的类型为 SimpleExecutor
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将查询结果放到临时的一个缓存中
tcm.putObject(cache, key, list);
}
return list;
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
SimpleExecutor 集成 BaseExecutor,最终会调用 BaseExecutor.query();
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
......
// 先尝试查询一级缓存
// 由于 localCache 是 BaseExecutor 对象的属性,而 BaseExecutor 对象是 CachingExecutor 对象的属性,而 CachingExecutor 对象是 DefaultSqlSession 对象的属性,所以一级缓存针对于同一个 sqlSession 对象,即同一个 sqlSession 对象创建的代理对象,调用同一个方法两次且传递同一个参数时,第二次调用会走一级缓存
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
......
if (list == null) {
// 如果一级缓存没有查到,通过 JDBC 访问数据库并执行 SQL
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
......
return list;
}
BaseExecutor.queryFromDatabase();
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
......
// 访问数据库,获取查询结果
List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
// 将查询结果放到一级缓存
localCache.putObject(key, list);
......
return list;
}
解析二:使用 JDBC 访问数据库
SimpleExecutor.doQuery();
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// configuration.newStatementHandler(); 方法主要有两步
// 第一步:创建 PreparedStatementHandler 对象,
// 第二步:创建 parameterHandler、ResultSetHandler 对象,其中 PreparedStatementHandler 继承 BaseStatementHandler,BaseStatementHandler 有两个属性:parameterHandler、ResultSetHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// SimpleExecutor.prepareStatement(); 方法主要有两步
// 第一步:调用 handler.prepare(); 方法获取 PreparedStatement 对象,底层主要代码为:connection.prepareStatement();
// 第二步:调用 handler.parameterize() 方法为 PreparedStatement 对象的占位符设置值,底层调用 parameterHandler.setParameters(); 方法,底层调用具体的 TypeHandler 处理 Java 类型 ===> JDBC 类型
stmt = prepareStatement(handler, ms.getStatementLog());
// handler.query(); 方法主要有两步
// 第一步:调用 preparedStatement.execute(); 方法执行 SQL
// 第二步:调用 resultSetHandler.handleResultSets(); 方法处理查询结果,底层通过 TypeHandler 完成 JDBC 类型 ===> Java 类型
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
八、MyBatis 插件
MyBatis 插件可以看作是自定义拦截器,对 MyBatis 访问数据库中的某些步骤进行功能增强。
以自定义一个分页插件为例:
准备工作
mybatis_table 表中数据:
MybatisTableMapper 接口
@CacheNamespace // 开启二级缓存
public interface MybatisTableMapper {
@Select("select id,user_name as userName,user_age as userAge from mybatis_table")
List<MybatisTable> selectPage(MyPage myPage);
}
MyPage 类
@Data
public class MyPage {
// 总条数
private int total;
// 页数(从1开始)
private int pageNo;
// 每页个数
private int pageSize;
public MyPage(int pageNo, int pageSize) {
this.pageNo = pageNo;
this.pageSize = pageSize;
}
}
①、创建自定义拦截器(插件)
package com.micozone.mybatis.mybatis;
import lombok.Data;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;
/**
* 自定义分页插件(拦截 {@link StatementHandler#prepare(Connection, Integer)} )
*
* @author Mico Zone
*/
@Data
@Intercepts({
// 此处可以配置多个 @Signature
// 拦截器拦截的维度是 Executor、StatementHandler、ParameterHandler、ResultSetHandler 接口中的某个方法
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class MyPageInterceptor implements Interceptor {
private Properties properties;
@Override
// 分页逻辑,即 statementHandler.prepare(); 时,会走这里的逻辑
// invocation.proceed(); 执行的是原 statementHandler.prepare(); 的逻辑
public Object intercept(Invocation invocation) throws Throwable {
// 目标对象
StatementHandler target = (StatementHandler) invocation.getTarget();
BoundSql boundSql = target.getBoundSql();
// 拿到参数
Object parameterObject = boundSql.getParameterObject();
MyPage myPage = null;
// 判断参数
if (parameterObject instanceof MyPage) {
myPage = (MyPage) parameterObject;
} else if (parameterObject instanceof Map) {
myPage = (MyPage) ((Map<?, ?>) parameterObject).values().stream().filter(parameter -> parameter instanceof MyPage).findFirst().orElse(null);
}
if (myPage == null) {
return invocation.proceed();
}
// 从参数列表中,获取一个Connection
Connection connection = (Connection) invocation.getArgs()[0];
String sql = boundSql.getSql();
int total = 0;
String countSQL = String.format("select count(*) from (%s) as _page", sql);
PreparedStatement preparedStatement = connection.prepareStatement(countSQL);
target.getParameterHandler().setParameters(preparedStatement);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
// 获取总条数
total = resultSet.getInt(1);
}
// 关闭资源,连接
resultSet.close();
preparedStatement.close();
// 设置总条数
myPage.setTotal(total);
// 对原有SQL进行修改,拼接limit
String newSQL = String.format("%s limit %s , %s", sql, (myPage.getPageNo() - 1) * myPage.getPageSize(), myPage.getPageSize());
SystemMetaObject.forObject(boundSql).setValue("sql", newSQL);
return invocation.proceed();
}
@Override
/*
InterceptorChain 对象有一个 pluginAll(); 方法:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// target 为目标对象,Executor、StatementHandler、ParameterHandler、ResultSetHandler 四者之一
// 调用 plugin(); 方法的作用是创建目标对象的代理对象
target = interceptor.plugin(target);
}
return target;
}
*/
public Object plugin(Object target) {
// Plugin.wrap(); 方法的作用就是判断目标对象是否实现 “当前插件拦截的方法所在的接口”,如果实现,则创建目标对象的代理对象(JDK动态代理),调用处理程序为 Plugin 对象
// 所以 statementHandler.prepare(); 时,实际上直接调用 plugin.invoke(); 方法,invoke(); 方法中判断该插件是否拦截的是 statementHandler.prepare(); 方法,如果不是,走目标对象的 prepare(); 逻辑,如果是,则会调用 MyPageInterceptor.interceptor(); 方法
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 将 <property> 标签配置的键值对保存到这里
this.properties = properties;
}
}
②、mybatis 配置文件中配置插件
mybatis.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>
......
<plugins>
<!--在此处配置完插件后,MyPageInterceptor 对象将会放到 configuration.interceptorChain.interceptors 集合中-->
<plugin interceptor="com.micozone.mybatis.mybatis.MyPageInterceptor">
<property name="name" value="zs"/>
<property name="age" value="15"/>
</plugin>
</plugins>
......
</configuration>
③、调用方法执行 MyPageInterceptor.intercept();
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 在创建 Executor 对象时,会调用 configuration.newExecutor(); 方法,内部会遍历所有拦截器创建动态代理对象:executor = (Executor) interceptorChain.pluginAll(executor);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 在二级缓存、一级缓存都没有查到的情况下,会创建 StatementHandler、ParameterHandler、ResultSetHandler 对象通过 JDBC 访问数据库
// 在创建该三个对象时,同样会调用 configuration.newStatementHandler();、configuration.newParameterHandler();、configuration.newResultSetHandler(); 三个方法
// 三个方法内部也会调用 interceptorChain.pluginAll(); 方法
List<MybatisTable> mybatisTableList = mybatisTableMapperProxy.selectPage(new MyPage(2, 3));
九、级联映射
通过 MyBatis 的级联映射,可以实现一对一、一对多、多对多的关联查询。
准备工作
建表:
-- 订单表
CREATE TABLE `orders` (
`id` bigint NOT NULL AUTO_INCREMENT,
`buyer_id` bigint DEFAULT NULL,
`order_name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 买家表(一个买家可以对应一个或者多个订单)
CREATE TABLE `buyer` (
`id` bigint NOT NULL AUTO_INCREMENT,
`buyer_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Orders 类
@Data
public class Orders {
private Long id;
private Long buyerId;
private String orderName;
private Buyer buyer;
}
OrdersMapper 接口
public interface OrdersMapper {
}
OrdersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">
</mapper>
Buyer 类
@Data
public class Buyer {
private Long id;
private String buyerName;
private List<Orders> ordersList;
}
BuyerMapper 接口
public interface BuyerMapper {
}
BuyerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">
</mapper>
一对多
实现方式一:collection 标签添加 select 属性
这种方式的特点是会执行两次 SQL。
BuyerMapper 接口
public interface BuyerMapper {
Buyer selectBuyerByIdOneToMany1(Long id);
}
BuyerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">
<resultMap id="oneToManyByCollection" type="buyer">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
<!-- 通过 select 属性,当某个查询的 resultMap="oneToManyByCollection" 时,会进行二次查询 -->
<collection property="ordersList"
ofType="com.micozone.mybatis.entity.Orders"
select="com.micozone.mybatis.mapper.OrdersMapper.selectOrdersListByBuyerId"
javaType="java.util.ArrayList"
column="id"/><!-- id 对应 buyer 的 id -->
</resultMap>
<select id="selectBuyerByIdOneToMany1" parameterType="long" resultMap="oneToManyByCollection">
select *
from buyer
where id = #{id}
</select>
</mapper>
OrdersMapper 接口
public interface OrdersMapper {
List<Orders> selectOrdersListByBuyerId(Long buyerId);
}
OrdersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">
<resultMap id="baseResultMap" type="orders">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="orderName" column="order_name" jdbcType="VARCHAR"/>
<result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
</resultMap>
<select id="selectOrdersListByBuyerId" resultMap="baseResultMap">
select *
from orders
where buyer_id = #{buyerId}
</select>
</mapper>
测试类
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
BuyerMapper buyerMapper = defaultSqlSession.getMapper(BuyerMapper.class);
buyerMapper.selectBuyerByIdOneToMany1(1L)); // 执行两次 sql
实现方式二:使用 join 进行关联查询
这种方式的特点是只会执行一次 SQL。
BuyerMapper 接口
public interface BuyerMapper {
Buyer selectBuyerByIdOneToMany2(Long id);
}
BuyerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">
<resultMap id="oneToManyByJoin" type="buyer">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
<collection property="ordersList" ofType="com.micozone.mybatis.entity.Orders">
<!-- 这里 column 不能等于 id,否则关联查询时返回列有两个 id 会报错,此处区分开 buyer 和 orders 的 id -->
<id property="id" column="orders_id" jdbcType="BIGINT"/>
<result property="orderName" column="order_name" jdbcType="VARCHAR"/>
<result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
</collection>
</resultMap>
<select id="selectBuyerByIdOneToMany2" parameterType="long" resultMap="oneToManyByJoin">
select buyer.*, orders.id as orders_id, orders.order_name, orders.buyer_id
from buyer
join orders on buyer.id = orders.buyer_id
where buyer.id = #{id}
</select>
</mapper>
测试类
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
BuyerMapper buyerMapper = defaultSqlSession.getMapper(BuyerMapper.class);
buyerMapper.selectBuyerByIdOneToMany2(1L));
一对一
实现方式一:association 标签添加 select 属性
这种方式的特点是会执行两次 SQL。
OrdersMapper 接口
public interface OrdersMapper {
Orders selectOrdersByIdOneToOne1(Long id);
}
OrdersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">
<resultMap id="oneToOneByAssociation" type="orders">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="orderName" column="order_name" jdbcType="VARCHAR"/>
<result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
<association property="buyer"
javaType="com.micozone.mybatis.entity.Buyer"
select="com.micozone.mybatis.mapper.BuyerMapper.selectBuyerById"
column="buyer_id"/><!--对应 orders 表的列-->
</resultMap>
<select id="selectOrdersByIdOneToOne1" parameterType="long" resultMap="oneToOneByAssociation">
select *
from orders
where id = #{id}
</select>
</mapper>
BuyerMapper 接口
public interface BuyerMapper {
@Select("select id ,buyer_name as buyerName from buyer where id = #{arg0}")
Buyer selectBuyerById(Long id);
}
测试类
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
OrdersMapper ordersMapper = defaultSqlSession.getMapper(OrdersMapper.class);
ordersMapper.selectOrdersByIdOneToOne1(1L); // 执行两次 sql
实现方式二:使用 join 进行关联查询
这种方式的特点是只会执行一次 SQL。
OrdersMapper 接口
public interface OrdersMapper {
Orders selectOrdersByIdOneToOne2(Long id);
}
OrdersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">
<resultMap id="oneToOneByJoin" type="orders">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="orderName" column="order_name" jdbcType="VARCHAR"/>
<result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
<association property="buyer" javaType="com.micozone.mybatis.entity.Buyer">
<id property="id" column="b_id" jdbcType="BIGINT"/>
<result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
</association>
</resultMap>
<select id="selectOrdersByIdOneToOne2" parameterType="long" resultMap="oneToOneByJoin">
select orders.*, buyer.id as b_id, buyer.buyer_name
from orders
left join buyer on orders.buyer_id = buyer.id
where orders.id = #{id}
</select>
</mapper>
测试类
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
OrdersMapper ordersMapper = defaultSqlSession.getMapper(OrdersMapper.class);
ordersMapper.selectOrdersByIdOneToOne2(1L);