前置知识
说说什么是mybatis吧
答: 是一个半ORM(对象关系映射)
框架,它封装了JDBC
的,使得我们在开发数据查询的业务代码时可以更加专注的去编写SQL
本身,无需手动去创建驱动、建立连接、创建statement
等操作,这就使得我们可以非常非常高效完成SQL
操作功能,以及调优也非常的方便。
当然它也是优缺点的,对于复杂的SQL
还是需要自己手动编写,这就对码农SQL功底有着较高的要求了,而且它的SQL
语句非常依赖数据库,这就使得可能这个数据库可以用的SQL
换一个数据库就不行了(PS
:前段时间处理兼容国产数据库问题搞得头大)。
你刚刚说了ORM框架,能不能告诉我什么是ORM?为什么说Mybatis的半自动ORM框架呢?
答: ORM
说白了就是建立数据库字段和Java对象(POJO)
的一种映射关系技术,而Mybatis
由于建立这种映射需要我们手动编写SQL
,所以说它是半自动的。
我们已经有JDBC了,为什么需要Mybatis呢?
**答:**因为JDBC
有下面几个缺点:
- 建立连接麻烦
SQL
写在代码里面不好维护。- 传参也很麻烦。
- 处理结果也很麻烦。
那它和Hibernate 有哪些区别知道嘛?
答: 首先一点是Mybatis
是半自动的ORM
框架,而Hibernate
是全自动的ORM
框架,而且前者Java
对象是和SQL
语句形成映射,所以进行多表联查的配置非常简单,而后者则是一表和Java
对象的方式构成映射关系,所以多表配置的关系比较复杂。
Mybatis
的SQL
都是需要手写的,且支持动态操作、编写存储过程、动态生成表明等操作,但不支持数据无关系(即编写的SQL仅仅支持某些数据库使用)
。而Hibernate
支持缓存、日志、级联以及HQL(Hibernate Query Language)
这种方式确保功能和数据库无关性,虽然开销略大,但是兼容性就比Mybatis要好很多了,当然这种数据无关性的框架自然不好优化了。
所以Mybatis
适合于迭代快的项目,而Hibernate
使用于业务比较稳定的项目。
核心知识
迁至步骤,搭建一个demo环境
在讨论下面这些面试题之前,我认为我们有必要搭建一套实验环境,首先我们的pom
文件必须引入下面这些依赖
<!--Mybatis核心-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!--junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency>
然后数据库建立两三张实验表,就以笔者为例,笔者建立了user1
、user2
、phone
表
create table user1(id varchar(10),name varchar(10));
create table user2(id varchar(10),name varchar(10));
create table phone(id varchar(10),phone_number varchar(10));
然后分别为这些表建立对应的POJO
对象。完成后我们进行Mybatis
配置文件配置,首先在resources目录下创建一个mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
<!--开启Mybatis支持延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"></setting>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:xxxxxx"/>
<property name="username" value="xxxx"/>
<property name="password" value="xxxxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/User1Mapper.xml"/>
</mappers>
</configuration>
然后再新建一个User1Mapper.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">
<mapper namespace="com.zsy.mapper.User1Mapper">
<select id="select" resultType="com.zsy.po.User1">
select * from user1 where id = #{id}
</select>
<select id="selectBySeq" resultType="com.zsy.po.User1">
select * from user1 where id = #{arg0} and name= #{arg1}
</select>
<select id="selectByAnnotation" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
<select id="selectByMap" parameterType="java.util.Map" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
<select id="selectByJavaBean" parameterType="com.zsy.po.User1" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
<select id="selectConvert" resultType="com.zsy.po.UserDto">
select id as uId, name as uName from user1 where id = #{id} and name= #{name}
</select>
<select id="selectConvert2" resultMap="userResultMap">
select * from user1 where id = #{id} and name= #{name}
</select>
<resultMap id="userResultMap" type="com.zsy.po.UserDto">
<id property="uId" column="id"/>
<result property="uName" column="name"/>
</resultMap>
<select id="selectLike" resultType="com.zsy.po.User1">
select * from user1 where name like CONCAT('%',#{name},'%')
</select>
<select id="selectAssociation" resultMap="peopleResultMap">
select u1.*,u2.id as u2_id,u2.name as u2_name from user1 u1
left join user2 u2 on u1.id=u2.id
</select>
<resultMap id="peopleResultMap" type="com.zsy.po.User1">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!--一对一结果映射-->
<association property="user2" javaType="com.zsy.po.User2">
<id column="u2_id" property="id"/>
<result column="u2_name" property="name"/>
</association>
</resultMap>
<select id="selectAssociation2" resultMap="peopleResultMap2">
select u1.*,p1.id as p_id,p1.phone_number as phone_number from user1 u1
left join phone p1 on u1.id=p1.id
</select>
<resultMap id="peopleResultMap2" type="com.zsy.po.User1Dto2">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!--一对多结果映射-->
<collection property="phoneList" ofType="com.zsy.po.Phone">
<!--注意 id标签标记的字段会使得这个值的数据只有一个-->
<!--<id column="p_id" property="id"/>-->
<result column="p_id" property="id"/>
<result column="phone_number" property="phoneNumber"/>
</collection>
</resultMap>
<select id="selectLazyQuery" resultMap="peopleResultMap3">
select * from user1 where id=#{id}
</select>
<select id="selectById" resultType="com.zsy.po.User2">
select * from user2 where id=#{id}
</select>
<resultMap id="peopleResultMap3" type="com.zsy.po.User1">
<id property="id" column="id"/>
<result property="id" column="id"/>
<result property="name" column="name"/>
<!--一对一结果映射-->
<association property="user2" column="id" javaType="com.zsy.po.User2"
select="com.zsy.mapper.User1Mapper.selectById"></association>
</resultMap>
</mapper>
最后创建User1Mapper.java
即可
public interface User1Mapper {
User1 select(String id);
User1 selectBySeq(String id,String name);
User1 selectByAnnotation(@Param("id") String id, @Param("name") String name);
User1 selectByMap(Map<String,String> params);
User1 selectByJavaBean(User1 params);
UserDto selectConvert(@Param("id") String id, @Param("name") String name);
UserDto selectConvert2(@Param("id") String id, @Param("name") String name);
User1 selectLike(@Param("name") String name);
List<User1> selectAssociation ();
List<User1Dto2> selectAssociation2 ();
User1 selectLazyQuery(String id);
User2 selectById(String id);
}
Mybatis使用过程知道吗?可以给我演示一下嘛?
**答:**整体分为这么4个步骤吧:
- 创建
SqlSessionFactory
- 创建
sqlSession
- 执行
SQL
(在此之间可能还有一步获取Mapper
) - 提交事务
- 关闭
session
代码基础示例如下
// 可以从配置或者直接编码来创建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2)通过SqlSessionFactory创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//3)通过sqlsession执行数据库操作
User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
User1 user = user1Mapper.select("1");
logger.info("查询结果:[{}]", user.toString());
if (sqlSession != null) {
sqlSession.close();
}
那它每个组件的生命周期了解嘛?
答: emmm
,这个嘛,我们完全可以基于上面说的几个类进行分析:
- 首先是
SqlSessionFactoryBuilder
,这个类纯是用于创建SqlSessionFactory
的,所以就是一个方法的东西属于方法级。 - 而
SqlSessionFactory
用于建立sqlSession
的,相当于一个数据库连接池。经常会被用到,这是个重量级对象,一般是单例且应用级。 SqlSession
就是用于执行SQL
的,且存在线程安全问题,一般是一次请求就销毁或者方法级。User1Mapper
不用说了,方法级。
Mybatis几种传参方式了解嘛?
**答:**我们不妨一个个举例吧
基于参数索引传参,这种方式可读性很差,基本不用
Java
代码
User1 selectBySeq(String id,String name);
xml配置
<select id="selectBySeq" resultType="com.zsy.po.User1">
select * from user1 where id = #{arg0} and name= #{arg1}
</select>
使用示例
//根据参数顺序查询,可读性不强 不建议使用
User1 user1 = user1Mapper.selectBySeq("1", "小明");
logger.info("根据参数顺序查询:[{}]", user1);//根据参数顺序查询,可读性不强 不建议使用
基于注解传参
Java
代码如下所示,通过注解告知框架多参数情况下,每一个形参对应xml
文件的哪个参数。
User1 selectByAnnotation(@Param("id") String id, @Param("name") String name);
xml
配置如下,可以看到参数为id
和name
和上面对应
<select id="selectByAnnotation" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
测试代码如下所示,输出结果也正常
//参数注解法查询,参数两少的情况建议使用
User1 user11 = user1Mapper.selectByAnnotation("1", "小明");
logger.info("参数注解法查询:[{}]", user11);
Map传参法,对于多变的情况很合适
java
代码
User1 selectByMap(Map<String,String> params);
xml
配置如下所示,将parameterType
生命为Java
的Map
对象即可
<select id="selectByMap" parameterType="java.util.Map" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
测试代码,可以看到params
中按照xml
配置的参数大小写进行传参
//Map参数法,多参数 可变情况多情况下建议使用
Map<String, String> params = new HashMap<>();
params.put("id", "1");
params.put("name", "小明");
User1 user12 = user1Mapper.selectByMap(params);
logger.info("Map参数法:[{}]", user12);
最常用的就算Java bean传参法,比较易于排查问题
Java
代码
User1 selectByJavaBean(User1 params);
xml
配置
<select id="selectByJavaBean" parameterType="com.zsy.po.User1" resultType="com.zsy.po.User1">
select * from user1 where id = #{id} and name= #{name}
</select>
测试代码
// Java bean查询法
User1 param = new User1();
param.setId("1");
param.setName("小明");
User1 user13 = user1Mapper.selectByJavaBean(param);
logger.info("Java bean 查询法:[{}]", user13);
你刚刚说的都是字段和表字段一致的情况,那如果我们的类字段和表字段不一致怎么办呢?
**答:**有两种方式,一种的SQL语句处理,如下所示我们的UserDto类,用uId存数据库中的id,那么我们SQL语句就给id起个别名就好了
<select id="selectConvert" resultType="com.zsy.po.UserDto">
select id as uId, name as uName from user1 where id = #{id} and name= #{name}
</select>
还有一种就是用resultMap标签建立映射关系了,如下便是sql配置
<select id="selectConvert2" resultMap="userResultMap">
select * from user1 where id = #{id} and name= #{name}
</select>
然后userResultMap映射的配置
<resultMap id="userResultMap" type="com.zsy.po.UserDto">
<id property="uId" column="id"/>
<result property="uName" column="name"/>
</resultMap>
Mybatis是否可以映射Enum枚举类?可不可以给我说说如何做到?
**答:**可以的,Mybatis
会自动完成数据库字段转Java
枚举的映射操作,我们不妨看看下面这个示例
首先我们可以创建一张数据表,插入一条boy
、MALE
create table user3(name varchar(255) primary key,gender varchar(255));
然后创建一个描述gender
的枚举
public enum GenderEnum {
MALE,FEMALE;
}
User
映射类代码
public class User3 {
private String name;
private GenderEnum gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public GenderEnum getGender() {
return gender;
}
public void setGender(GenderEnum gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User3{" +
"name='" + name + '\'' +
", gender=" + gender +
'}';
}
}
编写查询的xml
<select id="selectUser3" resultType="com.zsy.po.User3">
select * from user3 where name = #{name}
</select>
测试代码,可以看到输出结果有枚举的值,说明自动映射成功了。
User3 boy = user1Mapper.selectUser3("boy");
logger.info("枚举映射:[{}]",boy);// 输出结果 [main] INFO com.zsy.mapper.MyBatisTest - 枚举映射:[User3{name='boy', gender=MALE}]
实现原理也很简单,既然是Mybatis
自己完成映射转换,那我们不妨就从结果处理的源码中查看详情,入口就从DefaultResultSetHandler
这个类看起,如下所示,可以看到查询结束后处理结果的逻辑会走到handleRowValues
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
......
} else if (this.resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);
//处理查询出来的结果
this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
.......
}
} finally {
this.closeResultSet(rsw.getResultSet());
}
}
因为我们的结果集没有嵌套所以走到handleRowValuesForSimpleResultMap
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
........
} else {
//结果没有嵌套结构,就走到这里
this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
在往前走,进入getRowValue
准备将数据库值转为Java
值
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
........
// 如果还有数据要处理
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 获取数据库查询结果并转成Java对象
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
.........
}
}
再步入getRowValue
,开始进行类型转换applyAutomaticMappings
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
// 创建结果对象
Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 如果需要自动转换,则执行applyAutomaticMappings
if (this.shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
........
}
return rowValue;
}
核心代码来了,applyAutomaticMappings
会通过createAutomaticMappings
创建每个字段对应的映射处理类,例如String
就会找到String
对应的handler
,而枚举就会找到枚举对应的EnumTypeHandler
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
//创建每个字段对应映射的处理Handler
List<DefaultResultSetHandler.UnMappedColumnAutoMapping> autoMapping = this.createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
Iterator var7 = autoMapping.iterator();
while(true) {
DefaultResultSetHandler.UnMappedColumnAutoMapping mapping;
Object value;
do {
if (!var7.hasNext()) {
return foundValues;
}
mapping = (DefaultResultSetHandler.UnMappedColumnAutoMapping)var7.next();
// 通过对应类型Handler完成数据库字段转为Java结果,而我们的枚举类则会调用EnumTypeHandler
value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
} while(value == null && (!this.configuration.isCallSettersOnNulls() || mapping.primitive));
metaObject.setValue(mapping.property, value);
}
} else {
return foundValues;
}
}
那如果我自定义了一个枚举,实现这种映射关系呢?
答: 由上面的源码分析我们可知,每一个SQL
转换都是通过一个TypeHandler
类进行转换,所以如果我们希望实现自定义转换的话,完全继承TypeHandler
的共用类实现一个转换类,完成自定义枚举转换逻辑。
为了演示自定义类型和数据库字段的转换,我们不妨创建一张用户表
create table USER4 (id varchar(10),name varchar(10),sex int);
然后插入一条数据,如下所示,需要补充一下我们这张表的性别字段只有两种情况,一个1,另一个是2。
insert into user4 values ('1','jack',1);
所以我们希望性别这个字段要转为Java的自定义枚举类,以提高代码可读性。
对此我们首先定义一个枚举的接口,代码如下所示,其中int
为数据库的值,value
为显示的文本语义。
public interface IEnum {
int getKey();
void setKey(int key);
String getValue();
void setValue(String value);
}
然后我们定义一个性别的枚举类,代码如下
public enum SexEnum implements IEnum {
MAN(1, "男"), WOMAN(2, "女");
private int key;
private String value;
private SexEnum(int key, String value) {
this.key = key;
this.value = value;
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
然后我们继承Mybatis
的TypeHandler
编写自定义转换逻辑:
public class EnumKeyTypeHandler extends BaseTypeHandler<IEnum> {
private Class<IEnum> type;
private final IEnum[] enums;
/**
* 设置配置文件设置的转换类以及枚举类内容,供其他方法更便捷高效的实现
*
* @param type 配置文件中设置的转换类
*/
public EnumKeyTypeHandler(Class<IEnum> type) {
if (type == null)
throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
this.enums = type.getEnumConstants();
if (this.enums == null)
throw new IllegalArgumentException(type.getSimpleName()
+ " does not represent an enum type.");
}
@Override
public IEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
int dbValue = rs.getInt(columnName);
if (rs.wasNull()) {
return null;
} else {
// 根据数据库中的code值,定位IEnum子类
return locateIEnum(dbValue);
}
}
@Override
public IEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
int dbValaue = rs.getInt(columnIndex);
if (rs.wasNull()) {
return null;
} else {
// 根据数据库中的code值,定位IEnum子类
return locateIEnum(dbValaue);
}
}
public IEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
// 根据数据库存储类型决定获取类型,本例子中数据库中存放INT类型
int dbValue = cs.getInt(columnIndex);
if (cs.wasNull()) {
return null;
} else {
// 根据数据库中的code值,定位IEnum子类
return locateIEnum(dbValue);
}
}
public void setNonNullParameter(PreparedStatement ps, int i, IEnum parameter, JdbcType jdbcType)
throws SQLException {
// baseTypeHandler已经帮我们做了parameter的null判断
ps.setInt(i, parameter.getKey());
}
/**
* 枚举类型转换,由于构造函数获取了枚举的子类enums,让遍历更加高效快捷
*
* @param key 数据库中存储的自定义code属性
* @return code对应的枚举类
*/
private IEnum locateIEnum(int key) {
for (IEnum status : enums) {
if (status.getKey() == key) {
return status;
}
}
throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName());
}
}
完成之后我们就可以编写用户类了
public class User4 {
private String id;
private String name;
private SexEnum sex;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SexEnum getSex() {
return sex;
}
public void setSex(SexEnum sex) {
this.sex = sex;
}
@Override
public String toString() {
return "User4{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", sex=" + sex.getValue() +
'}';
}
}
重点来了,因为我们希望将数据库的int类型转为Java的枚举类型,所以我们xml配置时就需要使用自定义类型转换器处理
<select id="selectByConvert2Enum" resultMap="enumMap">
select * from user4 where id=#{id}
</select>
<resultMap id="enumMap" type="com.zsy.po.User4">
<result property = "sex" column = "sex" javaType = "com.zsy.po.SexEnum" typeHandler = "com.zsy.po.EnumKeyTypeHandler" />
</resultMap>
最后我们编写一下测试代码
User4 user14 = user1Mapper.selectByConvert2Enum("1");
logger.info("自定义枚举转换:[{}]", user14);
输出结果如下,可以看到我们数据库的数字转换为枚举中的value
了。
[main] INFO com.zsy.mapper.MyBatisTest - 自定义枚举转换:[User4{id='1', name='jack', sex=男}]
#{}和${}的有什么区别?
答: 总结出大概以下几点吧:
- 前者会在预编译阶段被处理,后者不会,只是单纯的字符串替换。
- 处理
#{}
时候,会将值变成字符串,然后将#{}
替换为?
,用PreparedStatement
的set
操作进行赋值。 - 前者可以放置
SQL
注入,提高系统安全性。后者有可能被SQL
注入。 - 前者的替换是在
DBMS
中,后者是在DBMS
外。
模糊查询like语句该怎么写呢?
**答:**可以使用'%{变量}%'
,有可能导致SQL
注入问题
或者下面这种CONCAT('%',#{变量},'%')
,我比较常用
<select id="selectLike" resultType="com.zsy.po.User1">
select * from user1 where name like CONCAT('%',#{name},'%')
</select>
好,那Mybatis一对一、一对多的查询怎么做可以给我说说嘛?
答:
先来说说一对一吧,有两种方式,举个例子,例如我们的希望User1
类中有个User2
对象,我们的POJO
代码如下
public class User1 {
private String id;
private String name;
private User2 user2;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User2 getUser2() {
return user2;
}
public void setUser2(User2 user2) {
this.user2 = user2;
}
@Override
public String toString() {
return "User1{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", user2=" + user2 +
'}';
}
}
查询语句手动起个别名,使用resultMap
配好每个数据库查询的结果值和Java
对象映射即可,一对一关联的用association
标签处理即可
<select id="selectAssociation" resultMap="peopleResultMap">
select u1.*,u2.id as u2_id,u2.name as u2_name from user1 u1
left join user2 u2 on u1.id=u2.id
</select>
<resultMap id="peopleResultMap" type="com.zsy.po.User1">
<id property="id" column="id" />
<result property="name" column="name"/>
<!--一对一结果映射-->
<association property="user2" javaType="com.zsy.po.User2">
<id column="u2_id" property="id"/>
<result column="u2_name" property="name"/>
</association>
</resultMap>
一对多同理,例如我们一个用户有多个手机号,我们的Java
对象如下,可以看到一个用户可能有多个手机号,我们的user
内部有个成员属性phoneList
public class User1Dto2 {
private String id;
private String name;
private List<Phone> phoneList;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Phone> getPhoneList() {
return phoneList;
}
public void setPhoneList(List<Phone> phoneList) {
this.phoneList = phoneList;
}
@Override
public String toString() {
return "User1Dto2{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", phoneList=" + phoneList +
'}';
}
}
那我们的xml
配置如下,写好SQL
外层做好字段关联,一对多的映射结果用collection
标签处理即可。
<select id="selectAssociation2" resultMap="peopleResultMap2">
select u1.*,p1.id as p_id,p1.phone_number as phone_number from user1 u1
left join phone p1 on u1.id=p1.id
</select>
<resultMap id="peopleResultMap2" type="com.zsy.po.User1Dto2">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!--一对多结果映射-->
<collection property="phoneList" ofType="com.zsy.po.Phone">
<!--注意 id标签标记的字段会使得这个值的数据只有一个-->
<!--<id column="p_id" property="id"/>-->
<result column="p_id" property="id"/>
<result column="phone_number" property="phoneNumber"/>
</collection>
</resultMap>
Mybatis动态语句了解嘛?
答: 常用的无非就算if、set、where、foreach这些。我们不妨通过几个示例了解一下:
例如我们某些查询必传一个参数,非必要一个参数,就可以使用动态if
查询
动态查询
<select id="selectIf" resultMap="enumMap">
select * from user4 where id=#{id}
<if test="name!=null">
and name=#{name}
</if>
</select>
再或者我们希望某个参数有传就取传的值,反之取默认值,就可以用choose
<select id="selectChoose" resultMap="enumMap">
select * from user4 where
<choose>
<when test="id!=null">
id=#{id}
</when>
<otherwise>
id=2
</otherwise>
</choose>
</select>
如果所有参数都可变,那么就需要使用 <where>
标签了
<select id="selectWhere" resultMap="enumMap">
select * from user4
<where>
<if test="id!=null">
and id=#{id}
</if>
<if test="name!=null">
and name=#{name}
</if>
</where>
</select>
我们希望动态范围查询的话,就可以使用foreach
标签
<select id="selectForEach" resultType="com.zsy.po.User1">
select * from user1
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" >
#{item}
</foreach>
</where>
</select>
动态更新
如果每个更新的字段都看你不更新,我们就可以使用动态更新<set>
<update id="updatebySet">
update user1
<set>
<if test="id!=null">id=#{id},</if>
<if test="name!=null">name=#{name},</if>
</set>
where id=#{id}
</update>
动态插入
如果我们希望批量插入的话,建议使用下面这种,如果参数是集合类型,则用collection
标签,然后遍历的每一项作为item
,用separator
以,
分割,批量生成value
<insert id="batchInsert">
INSERT INTO user1(id,name)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.id},#{user.name})
</foreach>
</insert>
通过输出结果我们也可以看到这种方式是将value
组装完成一次性插入的。
INSERT INTO user1(id,name) VALUES (?,?) , (?,?)
进阶知识
Mybatis懒加载了解嘛?能不能给我说说它是如何做到的?
**答:**介绍这个示例,我们首先要修改mybatis-config.xml
的配置,开启懒加载
<settings>
<setting name="logImpl" value="LOG4J"/>
<!--开启Mybatis支持延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"></setting>
</settings>
然后我们编写带有嵌套的resultMap
的结果集
<select id="selectLazyQuery" resultMap="peopleResultMap3">
select * from user1 where id=#{id}
</select>
<!--resultMap 内部嵌套一个查询user2的association -->
<resultMap id="peopleResultMap3" type="com.zsy.po.User1">
<id property="id" column="id"/>
<result property="id" column="id"/>
<result property="name" column="name"/>
<!--一对一结果映射-->
<association property="user2" column="id" javaType="com.zsy.po.User2"
select="com.zsy.mapper.User1Mapper.selectById"></association>
</resultMap>
<!--嵌套的查询sql-->
<select id="selectById" resultType="com.zsy.po.User2">
select * from user2 where id=#{id}
</select>
测试代码如下,从日志中可以看到,如果我们没有getUser2
,就不会触发查询user2
的SQL
User1 user17 = user1Mapper.selectLazyQuery("1");
logger.info("懒加载:[{}]", user17.getName());
输出结果,可以看到并没有查询User2
2022-11-26 00:38:01,954 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Preparing: select * from user1 where id=?
2022-11-26 00:38:01,985 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Parameters: 1(String)
2022-11-26 00:38:02,214 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - <== Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[小明]
修改测试代码
User1 user17 = user1Mapper.selectLazyQuery("1");
logger.info("懒加载:[{}]", user17.getName());
logger.info("懒加载:[{}]", user17.getUser2());
可以看到,查询user2
的SQL
在日志打印出来了
2022-11-26 00:39:05,567 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Preparing: select * from user1 where id=?
2022-11-26 00:39:05,600 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - ==> Parameters: 1(String)
2022-11-26 00:39:05,807 [main] DEBUG [com.zsy.mapper.User1Mapper.selectLazyQuery] - <== Total: 1
2022-11-26 00:39:05,814 [main] DEBUG [com.zsy.mapper.User1Mapper.selectById] - ==> Preparing: select * from user2 where id=?
2022-11-26 00:39:05,814 [main] DEBUG [com.zsy.mapper.User1Mapper.selectById] - ==> Parameters: 1(Long)
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[小明]
[main] INFO com.zsy.mapper.MyBatisTest - 懒加载:[User2{id='1', name='小明2'}]
它的工作原理也很简单,如果我们有配置懒加载,在在查询外层SQL
并组装对象时,看到xml
有嵌套属性且我们配置了懒加载,就会通过代理生成一个结果对象,若没有get
懒加载字段,就不会进行sql
查询,反之。
为了解释这条工作原理,我们不妨修改一下IDEA
的配置。我们可以通过debug
来了解,注意懒加载原理是基于动态代理,为了避免IDEA
调用toString
触发懒加载,我们需要进行如下设置
在 IDEA
中点击 settings->Build,Execution,Deploynent->Debugger->Data view->Java
去掉Enable 'toString()' object view
的勾,禁用该功能。
首先我们先来debug
一下非懒加载的查询,看看对象是否生成就是原生对象而代理生成的,我们的示例代码如下下
User1 user = user1Mapper.select("1");
logger.info("查询结果:[{}]", user.toString());
对应xml
配置,可以看到结果就是User1
,并没有什么嵌套的属性
<select id="select" resultType="com.zsy.po.User1">
select * from user1 where id = #{id}
</select>
debug
时,我们DefaultResultSetHandler
的handleResultSets
作为入口,代码会走到handleResultSet
就可以进入结果对象生成以及SQL
结果转换
public List<Object> handleResultSets(Statement stmt) throws SQLException {
.....忽略
String[] resultSets = this.mappedStatement.getResultSets();
if (resultSets != null) {
while(rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);
//处理查询结果
this.handleResultSet(rsw, resultMap, (List)null, parentMapping);
}
rsw = this.getNextResultSet(stmt);
this.cleanUpAfterHandlingResultSet();
++resultSetCount;
}
}
return this.collapseSingleResultList(multipleResults);
}
步入handleResultSet
,由于resultHandler
为空,进入handleRowValues
,处理每个SQL
列值
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
this.handleRowValues(rsw, resultMap, (ResultHandler)null, RowBounds.DEFAULT, parentMapping);
} else if (this.resultHandler == null) {
//resultHandler 为空,进入handleRowValues
this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
......
}
} finally {
this.closeResultSet(rsw.getResultSet());
}
}
进入handleRowValues
,因为我们的xml
的SQL
没有嵌套结构,所以查询走到handleRowValuesForSimpleResultMap
进入简单结果处理
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
.....
} else {
this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
然后走到handleRowValuesForSimpleResultMap
的getRowValue
进行结果对象生成
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 获取sql结果转为Java对象rowValue
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
进入getRowValue
,调用createResultObject
创建结果对象,但不赋值
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
ResultLoaderMap lazyLoader = new ResultLoaderMap();
//生成Java对象返回
Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
.........省略
}
return rowValue;
}
核心步骤来了,执行createResultObject
因为我们这个结果对象不是嵌套的resultMap
,所以对象就是通过createResultObject
反射直接生成的User
对象
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false;
List<Class<?>> constructorArgTypes = new ArrayList();
List<Object> constructorArgs = new ArrayList();
//创建结果对象user
Object resultObject = this.createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
//因为我们没有配置resultMap,所以不走下面的迭代器
Iterator var9 = propertyMappings.iterator();
while(var9.hasNext()) {
ResultMapping propertyMapping = (ResultMapping)var9.next();
//如果属性包含嵌套结果以及配置为懒加载,则需要通过代理方式生成对象
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = this.configuration.getProxyFactory().createProxy(resultObject, lazyLoader, this.configuration, this.objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
//返回一个没赋值的user
return resultObject;
}
如下图User1
,是一个都没赋值的空对象,而且对象类型就是User1
,并不是代理对象
对象创建后就开始赋值了,我们回到getRowValue
,执行applyAutomaticMappings
就会将SQL
结果通过对应的类型handler
赋值到对象上。
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
ResultLoaderMap lazyLoader = new ResultLoaderMap();
//生成没赋值的Java对象
Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
MetaObject metaObject = this.configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (this.shouldApplyAutomaticMappings(resultMap, false)) {
//通过mybatis自带的handler处理每一个属性对照rowValue 的成员属性进行赋值操作
foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
.....
}
//返回结果
return rowValue;
}
以上就是非懒加载的对象查询,然后我们再来看看懒加载的对象生成,通过debug
我们可以发现在createResultObject
时,因为我们的xml
配置了一个嵌套的User2
,如下所示
<resultMap id="peopleResultMap3" type="com.zsy.po.User1">
<id property="id" column="id"/>
<result property="id" column="id"/>
<result property="name" column="name"/>
<!--一对一结果映射-->
<association property="user2" column="id" javaType="com.zsy.po.User2"
select="com.zsy.mapper.User1Mapper.selectById"></association>
</resultMap>
所以下面这段代码,在遍历成员属性时发现有个嵌套的User2
对象,且我们配置开启了懒加载,所以代码会走到createProxy
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false;
List<Class<?>> constructorArgTypes = new ArrayList();
List<Object> constructorArgs = new ArrayList();
//先创建一个非代理的结果对象
Object resultObject = this.createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !this.hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
//获取结果对象映射
List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
//遍历成员变量的映射对象ResultMapping
Iterator var9 = propertyMappings.iterator();
while(var9.hasNext()) {
ResultMapping propertyMapping = (ResultMapping)var9.next();
//如果有嵌套的成员属性以及开启懒加载配置
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
// 基于代理生成对象
resultObject = this.configuration.getProxyFactory().createProxy(resultObject, lazyLoader, this.configuration, this.objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
return resultObject;
}
如下图,因为嵌套的User2
,使得对象需要通过代理创建,对结果的get
方法进行增强,只有get
嵌套属性时才触发懒加载。
可以看到这个对象就是代理生成的,而不是User
对象
回到我们的业务代码,当我们调用 user17.getUser2()
时,因为get
的值是嵌套属性,触发JavassistProxyFactory
的invoke
方法
logger.info("懒加载:[{}]", user17.getUser2());
源代码如下所示,源码根据我们的get方法的属性得知,当前get的属性是懒加载的属性,于是触发this.lazyLoader.load(property);
进行SQL
查询组装Java
结果。
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
String methodName = method.getName();
try {
ResultLoaderMap var6 = this.lazyLoader;
synchronized(this.lazyLoader) {
//有懒加载器以及当前方法不是finalize
if (this.lazyLoader.size() > 0 && !"finalize".equals(methodName)) {
if (!this.aggressive && !this.lazyLoadTriggerMethods.contains(methodName)) {
......
//我们代码getUser2是get方法
} else if (PropertyNamer.isGetter(methodName)) {
property = PropertyNamer.methodToProperty(methodName);
//lazyLoader包含user2这个属性
if (this.lazyLoader.hasLoader(property)) {
//触发懒加载调用sql查询并处理结果返回
this.lazyLoader.load(property);
}
}
} else {
this.lazyLoader.loadAll();
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable var10) {
throw ExceptionUtil.unwrapThrowable(var10);
}
}
}
mybatis的mapper配置文件里,如何实现同一段sql的重复引用?
使用sql
标签配合include
标签
<!-- <sql>抽离重复代码 <include>进行引用拼接-->
<sql id="findAllStudentsql">
select * from student_tb
</sql>
<select id="findAllStudent" resultType="Student">
<include refid="findAllStudentsql"></include> where id >40
</select>