为什么是非零基础呢?因为本笔记不包括一些概念介绍、历史介绍、还有持久化层技术的对比了,概念那部分对我们直接去开发、或者说想快速做一些项目的童鞋是没有很大必要了吧,我们要的就是实践出真知!!!
官方-用户使用手册
看了下截止到目前MyBatis的最新版本是3.5.16,而尚硅谷发布MyBatis3.5.7零基础入门教程视频是2022年2月,最新版本的压缩包里就没有MyBatis法宝了,这里只能奉上3.5.7的法宝了。
那就是:官方发布的用户使用手册
借花献佛:通过网盘分享的文件:mybatis-3.5.7.pdf
链接: https://pan.baidu.com/s/1uI2gupmRiKVsFWWqbPg42A?pwd=gq52 提取码: gq52
正式开练
教程开发环境
开发中很多问题可能都是环境配置不一致导致的,这里尚硅谷老师给出的教程环境列一下:
Idea2019、maven3.5.4、mysql5.7、jdk1.8
Maven – Maven Releases Historymaven与jdk的版本对照关系可参考:Maven – Maven Releases History
MyBatis初识
这里演示一下通过MyBatis实现增删改操作,这部分官方用户手册写得非常清楚了,一步一步怎么获取sqlSession、获取mapper接口实现类对象的。
准备实体类和数据库实体表
package com.coffeeship.pojo;
/**
* @description: 实体类
* @author: coffeeship
* @date: 2024/10/24 15:01
*/
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
private String email;
public User() {
}
public User(Integer id, String username, String password, Integer age, String sex, String email) {
this.id = id;
this.username = username;
this.password = password;
this.age = age;
this.sex = sex;
this.email = email;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", email='" + email + '\'' +
'}';
}
}
导入依赖
<!-- Mybatis核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
从XML配置文件获取SqlSessionFactory
对应测试类
SqlSession代表和数据库的一次会话,用完必须释放。
SqlSession和Connection一样都是非线程安全的,不能作为成员变量,否则会产生资源竞争。每次使用都应该去获取新的对象。
@Test
public void testMyBatisInsert() throws IOException {
//加载核心配置文件
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
//获取sqlSessionFactoryBuilder
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
//获取sqlSessionFactory
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
//获取SqlSession 默认不自动提交事务
// SqlSession sqlSession = sqlSessionFactory.openSession();
//设置事务自动提交
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//获取Mapper对象 底层技术:代理
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int result = mapper.insertUser();
//手动提交事务
// sqlSession.commit();
System.out.println(result);
}
核心配置文件
包括数据库连接环境(事务管理器、数据连接池)
transactionManager可选项:JDBC | MANAGED
dataSource可选项:POOLED | UNPOOLED | JNDI
关于核心配置文件中的标签介绍,更详细的可以参考尚硅谷在2018年发布的mybatis教程。
数据库连接池中的配置信息可通过properties文件注参;eg.
mapper
引入dtd约束:http://mybatis.org/dtd/mybatis-3-config.dtd
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--配置数据库连接的环境-->
<environments default="development">
<environment id="development">
<!--事务管理器,JDBC:最原始的方式,事务的操作回滚需要手动配置-->
<transactionManager type="JDBC"/>
<!--数据库连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:13306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="qts0922"/>
</dataSource>
</environment>
</environments>
<!--引入映射文件 来自源根的路径-->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
编写Mapper XML文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace等于接口类的全限定名-->
<mapper namespace="com.coffeeship.mapper.UserMapper">
<insert id="insertUser">
insert into t_user values(null, 'admin', '123456', 18, '女', 'zh091699@163.com')
</insert>
<update id="updateUser">
update t_user set username = 'administrator' where id = 2;
</update>
<delete id="deleteUser">
delete from t_user where id = 3
</delete>
</mapper>
执行测试类中的方法
然后查询部分,涉及到返回对象,可以参考spring对JDBC的封装,即JdbcTemplate,就是需要在XML配置文件中指定一下resultType或resultMap,就是一点点不一样哈。比如:
<!--指定SQL返回类型
resultType:设置默认的映射关系
resultMap:设置自定义的映射关系
-->
<select id="getUserById" resultType="com.coffeeship.pojo.User">
select * from t_user where id = 2
</select>
<select id="getAllUser" resultType="com.coffeeship.pojo.User">
select * from t_user
</select>
@Test
public void testMyBatisSelectOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById();
System.out.println(user);
}
@Test
public void testMyBatisSelectAll() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = mapper.getAllUser();
allUser.forEach(user -> System.out.println(user));
}
设置别名
typeAliases标签
MyBatis核心配置文件中的标签顺序
The content of element type "configuration" must match "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)".
mybatis-config.xml :类型别名不区分大小写
type:需要设置别名的类的全路径
alias属性值可选,若设置了以设置的值为准,不区分大小写;
若未设置,则默认是类名,不区分大小写
<typeAliases>
<typeAlias type="com.coffeeship.pojo.User" alias="user"/>
</typeAliases>
UserMapper.xml
<!-- <select id="getUserById" resultType="com.coffeeship.pojo.User">
select * from t_user where id = 2
</select>-->
<!--设置别名后...-->
<select id="getUserById" resultType="user">
select * from t_user where id = 2
</select>
<!-- <select id="getAllUser" resultType="com.coffeeship.pojo.User">
select * from t_user
</select>-->
<!--设置别名后...-->
<select id="getAllUser" resultType="user">
select * from t_user
</select>
以包为单位设置默认别名,即类名、不区分大小写
<typeAliases>
<!-- <typeAlias type="com.coffeeship.pojo.User" alias="user"/>-->
<package name="com.coffeeship.pojo"/>
</typeAliases>
以包为单位引入映射文件
<!--引入映射文件 来自源根的路径-->
<mappers>
<!--<mapper resource="mappers/UserMapper.xml"/>-->
<!--以包为单位引入映射文件
要求: 1.mapper接口所在的包要和映射文件所在的包一致
2.mapper接口要和映射文件的名字一致
-->
<package name="com.coffeeship.mapper"/>
</mappers>
MyBatis获取参数值的两种方式(重点)
MyBatis获取参数值的两种方式:${}和#{} (JDBC原生方式)
${}的本质就是字符串拼接,#{}的本质就是占位符赋值
${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自动添加单引号。
${}、#{}可通过任意名称赋值,获取参数只与参数位置有关,但建议见名知意。
分别对应JDBC Template中的字符串拼接(有SQL注入的风险)和占位符赋值:?
单个字面量类型的参数
若mapper接口中的方法参数为单个的字面量类型,此时可以使用${}和#{}
以任意的名称获取参数的值,注意${}需要手动加单引号。
多个字面量类型的参数
若mapper接口中的方法参数为多个时,此时MyBatis会自动将这些参数放在一个map集合中,
以arg0,arg1...为键,以参数为值;
以param1,param2...为键,以参数为值;
因此只需要通过${}和#{}访问map集合的键就可以获取相对应的值,
注意${}需要手动加单引号。
map集合类型的参数
若mapper接口中的方法需要的参数为多个时,此时可以手动创建map集合,
将这些数据放在map中,只需要通过${}和#{}访问map集合的键就可以获取相对应的值,
注意${}需要手动加单引号。
实体类类型的参数
若mapper接口中的方法参数为实体类对象时,此时可以使用${}和#{},
通过访问实体类对象中的属性名获取属性值,
注意${}需要手动加单引号。
使用@Param标识参数
可以通过@Param注解标识mapper接口中的方法参数,
此时,会将这些参数放在map集合中,
以@Param注解的value属性值为键,以参数为值;
以 param1,param2...为键,以参数为值;
只需要通过${}和#{}访问map集合的键就可以获取相对应的值,注意${}需要手动加单引号。
UserMapper接口
public interface UserMapper {
User getUserById(Integer id);
User getUserByNamePwd(String username, String password);
User getUserByNameAge(Map<String, Object> map);
User getUser(User user);
User getUserByParam(@Param("paramName") String username, @Param("paramPwd") String password);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace等于接口类的全限定名-->
<mapper namespace="com.coffeeship.mapper.UserMapper">
<!--单个字面量的参数,可以以任意名称获取参数的值,但建议”见名知意“-->
<select id="getUserById" resultType="com.coffeeship.pojo.User">
<!--select * from t_user where id = #{id}-->
select * from t_user where id = #{anyName}
</select>
<!--多个字面量的参数,以arg0,arg1...为键或以param1,param2...为键-->
<!--Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]-->
<select id="getUserByNamePwd" resultType="com.coffeeship.pojo.User">
<!--错误写法:select * from t_user where username = #{username} and password = #{password}-->
<!--以下均为正确写法-->
<!--select * from t_user where username = #{arg0} and password = #{arg1}-->
<!--select * from t_user where username = #{param1} and password = #{param2}-->
select * from t_user where username = #{arg0} and password = #{param2}
</select>
<!--map集合类型的参数,访问map集合的键就可以获取相对应的值-->
<select id="getUserByNameAge" resultType="com.coffeeship.pojo.User">
select * from t_user where username = #{name} and age = #{age}
</select>
<!--实体类类型的参数,访问实体类对象中的属性名获取属性值-->
<select id="getUser" resultType="com.coffeeship.pojo.User">
select * from t_user where id = #{id} and username = #{username}
</select>
<!--使用@Param标识参数,以@Param注解的value属性值为键或以 param1,param2...为键-->
<select id="getUserByParam" resultType="com.coffeeship.pojo.User">
<!--select * from t_user where username = #{paramName} and password = #{paramPwd}-->
<!--select * from t_user where username = #{param1} and password = #{param2}-->
select * from t_user where username = #{paramName} and password = #{param2}
</select>
</mapper>
工具类
public class SqlSessionUtil {
public static SqlSession getSqlSession() {
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream("mybatis-config.xml");
} catch (IOException e) {
throw new RuntimeException(e);
}
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
return sqlSession;
}
}
测试类
public class ParamTest {
@Test
public void test01() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(2);
System.out.println(user);
}
@Test
public void test02() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserByNamePwd("admin", "123456");
System.out.println(user);
}
@Test
public void test03() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("name", "admin");
map.put("age", 18);
User user = mapper.getUserByNameAge(map);
System.out.println(user);
}
@Test
public void test04() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User(2, "administrator", "123456", 18, "女", "zh091699@163.com");
User returnUser = mapper.getUser(user);
System.out.println(returnUser);
}
@Test
public void test05() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserByParam("admin", "123456");
System.out.println(user);
}
}
@Param源码解析
User getUserByParam(@Param("paramName") String username, @Param("paramPwd") String password);
<!--使用@Param标识参数,以@Param注解的value属性值为键或以 param1,param2...为键-->
<select id="getUserByParam" resultType="com.coffeeship.pojo.User">
<!--select * from t_user where username = #{paramName} and password = #{paramPwd}-->
<!--select * from t_user where username = #{param1} and password = #{param2}-->
select * from t_user where username = #{paramName} and password = #{param2}
</select>
从这里进入
mybatis创建了接口的实现类代理对象——接口式编程
原生:DAO接口、DAOImpl接口实现类
mybatis:mapper、mapper.xml
进入cachedInvoker的invoke方法
进入execute,匹配到当前是SELECT类型
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
先看参数映射关系是如何放到map中的
再来看sql执行过程
MappedStatement对象中有属性statementType,属性值为PREPARED,表示预编译的;resultMaps,有一个值就是Mapper接口方法中指明的返回值User类;还有就是sqlCommandType,SQL的操作类型,即SELECT。
获取BoundSql对象,包含属性:1.解析后的sql,可以看到原生sql是用的?表示占位符
2.parameterMappings,记录占位符处对应的映射关系
3.parameterObject, 传入的参数及对应的参数值;键与parameterMappings中的property对应
委托代理,返回一个CacheKey
通过CacheKey 来封装缓存的 key 值,CacheKey 可以封装多个影响缓存项的因素。
缓存的作用就是提高查询速度,数据库中的sql也有对应的缓存机制,当你使用绑定变量的时候,判断sql执行的频率,可以考虑将sql缓存起来,也可以类比java中的热代码使用JIT编译一样。
将热点SQL缓存起来,下次只是改变传参而已,sql其实没变,这样查询速率就会很快。
又到了激动人心的环节
这里MappedStatement对象中的cache是null,注意这里的cache与上面的CacheKey不是一回事!那MappedStatement里的这些属性都是啥意思呢?
可以参考这篇博主的介绍,cache是二级缓存策略,默认就是null。
https://juejin.cn/post/6886805132936216583
建立数据库连接,并通过反射执行了autoCommit的设置,再通过反射执行了preparedStatement方法,这里熟悉JDBC原生方式访问数据库的童鞋就知道了。
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
return method.invoke(statement, params);
}
} else if (SET_METHODS.contains(method.getName())) {
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else if ("getUpdateCount".equals(method.getName())) {
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
method.invoke(statement, params)终于要通过反射给sql赋值了
赋完值后再看SQL语句
再赋第2个参数
Executor接口的实现类对象delegate执行。
这里就很熟悉了 ps.execute()也是通过反射去执行
resultSetHandler.handleResultSets(ps)应该是在封装返回结果
好吧,这篇笔记就到这里结束吧,不然都看困了。。。
扒源码的路漫漫,其修远兮,吾将上下而求索~~~