1. mybatis入门
1.mybatis入门
- SqlSessionFactoryBuilder: 读取配置信息,创建SqlSessionFactory.建造者模式 ,方法级别生命周期
- SqlSessionFactoryL 创建SqlSession,工厂单例模式,存在于程序的整个生命周期
- SqlSession: 代表一次数据库连接,一般通过调用Mapper访问数据库,也可以直接发送SQL执行。线程不安全,要保证线程独享(方法级别)
- SQL Mapper:由一个Java接口和XML文件组成,包含了要执行的SQL语句和结果映射规则。方法级别生命周期。
2. resultType 还是resultMap
1.resultType
- 当使用resultType做SQL语句返回结果类型处理时,对SQL语句查询出的字段在相应的POJO中必须有和它相同的字段对应,而resultType中的内容就是pojo在本项目中的位置
- 自动映射注意事项
- 前提:SQL列名和JavaBean的属性是一致的
- 使用resultType,如用简写,需要配置typeAliases(别名)
- 如果列名和JavaBean不一致,但列名符合单词下划线分割,Java是驼峰命名法,则mapUnderscoreToCamelCase可设置为true【注意,windows下面不区分大小写,linux下面区分大小写】
2.resultMap
-
resultMap元素是Mybatis中最重要最强大的元素。它可以让你从90%的JDBC ResultSets数据提取代码中解放出来,在对复杂语句进行联合映射的时候,它可以替代数千行同等功能的代码。
-
ResultMap的设计思想是,简单的语句不需要明确的结果映射,而复杂一点的语句只需要描述它们的关系就行了
-
属性 描述 id 当前命名空间中的一个标识,用来标识一个resultMap type 类的全限定名,或者一个类型别名 autoMapping (自动映射,类似ResultType,如果字段名和表名是一样的,可以不用手动写,会自动映射) 如果设置了这个属性,Mybatis将会为这个resultMap开启或关闭自动映射。这个属性会覆盖全局的属性autoMappingBehavior。默认值为:unset
使用场景:
- 字段有自定义的转换规则
- 复杂的多表查询
-
resultMap子元素
- id: 一个ID结果,标记出作为ID的结果可以帮助提高整体性能,一对多的查询中用于结果集合并
- result:注入到字段或JavaBean属性的普通结果
- association:一对一
- collection:一对多
建议强制使用resultMap
3. 怎么传递多个参数
- 使用map传递参数 :可读性差,导致可维护和可扩展性差,杜绝使用
- 使用注解传递参数:直观明了,当参数较少一般小于等于5个的时候,建议使用
- 使用JavaBean传递参数:当参数大于5个的时候,建议使用
// 1. map传递参数
List<TUser> selectByEmailAndSex1(Map<String, Object> param);
// 2. 注解传递参数
List<TUser> selectByEmailAndSex2(@Param("email") String email, @Param("sex") Byte sex);
// 3. JavaBean传递参数
List<TUser> selectByEmailAndSex3(EmailSexBean esb);
@Data
public class EmailSexBean {
private Byte sex;
private String email;
}
4.怎么获取主键
insert标签相关属性
属性 | 描述 |
---|---|
useGeneratedKeys | (仅对insert和Update有用)。这会令mybatis使用jdbc的getGeneratedKeys方法来取出数据库内部生成的主键(比如:想MySQL和SQLServer这样的关系型数据库的自动递增字段)。默认值:false |
keyProperty | (仅对insert和Update有用),唯一标记一个属性,MyBatis会通过getGeneratedKeys的返回值或者通过insert语句的selectKey子元素设置它的键值。默认:unset/如果希望得到多个生成的列,也可以使用逗号分割的属性名称列表。 |
注意:自增长序列不是简单的行数+1,而是序号最大值+1
selectKey元素
属性 | 描述 |
---|---|
keyProperty | selectKey语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以使用逗号分割属性名称列表 |
resultType | 结果的类型 |
order | 这可以被设置为BEFORE或AFTER、如果设置成BEFORE,那么它会首先选择主键,设置keyProperty然后执行插入语句。如果设置为AFTER,那么先执行插入语句,然后获取主键字段 |
Oracle自增长序号实现:
<selectKey keyProperty="id" order="Before" resultType="int">
select SEQ_ID.nextval from dual
</selectKey>
实战:
1.使用useGeneratedKeys和keyProperty
// 测试插入数据自动生成ID
@Test
public void testInsertGenerateId1(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user1 = new TUser();
user1.setUserName("test1");
user1.setRealName("realname1");
user1.setEmail("myemail1");
mapper.insert1(user1);
System.out.println(user1.getId());
}
<insert id="insert1" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user (id, userName, realName,
sex, mobile,
email,
note, position_id)
values (#{id,jdbcType=INTEGER},
#{userName,jdbcType=VARCHAR},
#{realName,jdbcType=VARCHAR},
#{sex,jdbcType=TINYINT}, #{mobile,jdbcType=VARCHAR},
#{email,jdbcType=VARCHAR},
#{note,jdbcType=VARCHAR},
#{position.id,jdbcType=INTEGER})
</insert>
2.使用
@Test
public void testInsertGenerateId2(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user2 = new TUser();
user2.setUserName("test2");
user2.setRealName("realname2");
user2.setEmail("myemail2");
mapper.insert2(user2);
System.out.println(user2.getId());
}
<insert id="insert2" parameterType="TUser">
<selectKey keyProperty="id" order="AFTER" resultType="int">
select
LAST_INSERT_ID()
</selectKey>
insert into t_user (id, userName, realName,
sex, mobile,
email,
note,
position_id)
values (#{id,jdbcType=INTEGER},
#{userName,jdbcType=VARCHAR},
#{realName,jdbcType=VARCHAR},
#{sex,jdbcType=TINYINT}, #{mobile,jdbcType=VARCHAR},
#{email,jdbcType=VARCHAR},
#{note,jdbcType=VARCHAR},
#{position.id,jdbcType=INTEGER})
</insert>
5. SQL元素和SQL参数
SQL元素:用来定义可重用的SQL代码段,可以包含在其他的语句中
参数:向SQL语句中传递可变参数
- 预编译:#{} 将传入的数据都当成一个字符串,会对自动传入的数据加上一个单引号,能够很大程度的防止sql注入
- 传值:${} 传入的数据直接显示生成在SQL中,无法防止SQL注入
- 表名,选择的列是动态的,order by和in操作,可以考虑使用${}
// 参数#和参数$区别测试
@Test
public void testSymbol() {
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
String inCol = "id, userName, realName, sex, mobile, email, note";
String tableName = "t_user";
// String userName = "lison";
String userName = "'xxx' or 1=1";
String orderStr = "sex,userName";
List<TUser> list = mapper.selectBySymbol(tableName, inCol, orderStr, userName);
list.forEach(System.out::println);
}
List<TUser> selectBySymbol(@Param("tableName") String tableName,
@Param("inCol") String inCol,
@Param("orderStr") String orderStr,
@Param("userName") String userName);
<select id="selectBySymbol" resultMap="BaseResultMap">
select
${inCol}
from ${tableName} a
where a.userName = #{userName}
order by ${orderStr}
</select>
注意:#{}会自动加上单引号,${}不会,使用${}需要手动加上单引号
6. 动态SQL
1. 动态SQL元素
元素 | 作用 | 备注 |
---|---|---|
if | 判断语句 | 单条件分支判断 |
choose,when,otherwise | 相当于java的case when | 多条件分支判断 |
Trim,where,set | 辅助元素 | 用于处理sql瓶装问题 |
foreach | 循环语句 | 在in语句等列举条件常用,常用于实现批量操作 |
2. if用于select,并与where配合
@Test
public void testSelectIfOper(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
String email = "qq.com";
Byte sex = 1;
List<TUser> list = mapper.selectIfOper(null, null);
// List<TUser> list = mapper.selectIfandWhereOper(email, null);
// List<TUser> list = mapper.selectChooseOper(email , sex);
list.forEach(System.out::println);
}
<select id="selectIfOper" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user a
where 1=1
<if test="email != null and email != ''">
and a.email like CONCAT('%', #{email}, '%')
</if>
<if test="sex != null ">
and a.sex = #{sex}
</if>
</select>
3. 使用标签
- 会判断
标签里面有没有条件成立,如果成立,会加上一个where字段。如果不成立就不会加上where - 会把第一个and或者or去掉
mapper.selectIfandWhereOper(email, null);
<select id="selectIfandWhereOper" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user a
<where>
<if test="email != null and email != ''">
and a.email like CONCAT('%', #{email}, '%')
</if>
<if test="sex != null ">
and a.sex = #{sex}
</if>
</where>
</select>
4. set标签
标签有两个作用,如果标签里面有条件成立,那么会加上set字段 - 会把set块里面最后的一个逗号去掉
// if用于update,并与set配合
@Test
public void testUpdateIfOper(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user = new TUser();
user.setId(3);
user.setUserName("cindy");
user.setRealName("王美丽");
user.setEmail("xxoo@163.com");
user.setMobile("18695988747");
// user.setNote("cindy's note");
// user.setSex((byte) 2);
// System.out.println(mapper.updateIfOper(user));
System.out.println(mapper.updateIfAndSetOper(user));
}
<update id="updateIfAndSetOper" parameterType="TUser">
update t_user
<set>
<if test="userName != null">
userName = #{userName,jdbcType=VARCHAR},
</if>
<if test="realName != null">
realName = #{realName,jdbcType=VARCHAR},
</if>
<if test="sex != null">
sex = #{sex,jdbcType=TINYINT},
</if>
<if test="mobile != null">
mobile = #{mobile,jdbcType=VARCHAR},
</if>
<if test="email != null">
email = #{email,jdbcType=VARCHAR},
</if>
<if test="note != null">
note = #{note,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
5. trim标签
if用于insert,并与trim配合
prefix :加上前缀
suffix:加上后缀
suffixOverrides:去掉最后一个
// if用于insert,并与trim配合
@Test
public void testInsertIfOper(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user = new TUser();
user.setUserName("mark");
user.setRealName("毛毛");
user.setEmail("xxoo@163.com");
user.setMobile("18695988747");
// user.setNote("mark's note");
user.setSex((byte) 1);
// System.out.println(mapper.insertIfOper(user));
System.out.println(mapper.insertSelective(user));
}
<insert id="insertSelective" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="id != null">
id,
</if>
<if test="userName != null">
userName,
</if>
<if test="realName != null">
realName,
</if>
<if test="sex != null">
sex,
</if>
<if test="mobile != null">
mobile,
</if>
<if test="email != null">
email,
</if>
<if test="note != null">
note,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=INTEGER},
</if>
<if test="userName != null">
#{userName,jdbcType=VARCHAR},
</if>
<if test="realName != null">
#{realName,jdbcType=VARCHAR},
</if>
<if test="sex != null">
#{sex,jdbcType=TINYINT},
</if>
<if test="mobile != null">
#{mobile,jdbcType=VARCHAR},
</if>
<if test="email != null">
#{email,jdbcType=VARCHAR},
</if>
<if test="note != null">
#{note,jdbcType=VARCHAR},
</if>
</trim>
</insert>
6. foreach
foreach属性:
- collectoin: 两种值:1. array。2.list
- open:前缀
- close:后缀
- item:遍历的变量
- separator:分隔符
@Test
public void testForeach4In(){
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
String[] names = new String[]{"lison","james"};
List<TUser> users = mapper.selectForeach4In(names);
users.forEach(System.out::println);
System.out.println(users.size());
}
<select id="selectForeach4In" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user a
where a.userName in
<foreach collection="array" open="(" close=")" item="userName" separator=",">
#{userName}
</foreach>
</select>
通过Mybatis怎么进行批量操作
1.通过foreach动态拼装SQL
// Foreach用于批量插入
@Test
public void testForeach4Insert() {
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user1 = new TUser();
user1.setUserName("king");
user1.setRealName("李小京");
user1.setEmail("li@qq.com");
user1.setMobile("18754548787");
user1.setNote("king's note");
user1.setSex((byte)1);
TUser user2 = new TUser();
user2.setUserName("deer");
user2.setRealName("陈大林");
user2.setEmail("chen@qq.com");
user2.setMobile("18723138787");
user2.setNote("deer's note");
user2.setSex((byte)1);
int i = mapper.insertForeach4Batch(Arrays.asList(user1,user2));
System.out.println("------批量更新获取主键的方式与单条insert完全相同--------");
System.out.println(user1.getId());
System.out.println(user2.getId());
}
<insert id="insertForeach4Batch" useGeneratedKeys="true" keyProperty="id">
insert into t_user (userName, realName,
sex, mobile,email,note,
position_id)
values
<foreach collection="list" separator="," item="user">
(
#{user.userName,jdbcType=VARCHAR},
#{user.realName,jdbcType=VARCHAR},
#{user.sex,jdbcType=TINYINT},
#{user.mobile,jdbcType=VARCHAR},
#{user.email,jdbcType=VARCHAR},
#{user.note,jdbcType=VARCHAR},
#{user.position.id,jdbcType=INTEGER}
)
</foreach>
</insert>
2.使用BATCH类型的excutor
@Test
// 批量更新
public void testBatchExcutor() {
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 选择执行器的类型
// SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, true);
// 3.获取对应mapper
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
TUser user = new TUser();
user.setUserName("mark");
user.setRealName("毛毛");
user.setEmail("xxoo@163.com");
user.setMobile("18695988747");
user.setNote("mark's note");
user.setSex((byte) 1);
TPosition positon1 = new TPosition();
positon1.setId(1);
user.setPosition(positon1);
System.out.println(mapper.insertSelective(user));
TUser user1 = new TUser();
user1.setId(3);
user1.setUserName("cindy");
user1.setRealName("王美丽");
user1.setEmail("xxoo@163.com");
user1.setMobile("18695988747");
user1.setNote("cindy's note");
user1.setSex((byte) 2);
user1.setPosition(positon1);
System.out.println(mapper.updateIfAndSetOper(user1));
sqlSession.commit();
System.out.println("----------------");
System.out.println(user.getId());
System.out.println(user1.getId());
}
<insert id="insertSelective" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="id != null">
id,
</if>
<if test="userName != null">
userName,
</if>
<if test="realName != null">
realName,
</if>
<if test="sex != null">
sex,
</if>
<if test="mobile != null">
mobile,
</if>
<if test="email != null">
email,
</if>
<if test="note != null">
note,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=INTEGER},
</if>
<if test="userName != null">
#{userName,jdbcType=VARCHAR},
</if>
<if test="realName != null">
#{realName,jdbcType=VARCHAR},
</if>
<if test="sex != null">
#{sex,jdbcType=TINYINT},
</if>
<if test="mobile != null">
#{mobile,jdbcType=VARCHAR},
</if>
<if test="email != null">
#{email,jdbcType=VARCHAR},
</if>
<if test="note != null">
#{note,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateIfAndSetOper" parameterType="TUser">
update t_user
<set>
<if test="userName != null">
userName = #{userName,jdbcType=VARCHAR},
</if>
<if test="realName != null">
realName = #{realName,jdbcType=VARCHAR},
</if>
<if test="sex != null">
sex = #{sex,jdbcType=TINYINT},
</if>
<if test="mobile != null">
mobile = #{mobile,jdbcType=VARCHAR},
</if>
<if test="email != null">
email = #{email,jdbcType=VARCHAR},
</if>
<if test="note != null">
note = #{note,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
7. 关联查询
1. 一对一:association
如果在关联查询的时候配置了ID字段,那么会根据ID字段进行结果集的合并.
1. association标签 嵌套结果方式
<resultMap id="userAndPosition1" extends="BaseResultMap" type="TUser">
<association property="position" javaType="TPosition" columnPrefix="post_">
<id column="id" property="id"/>
<result column="name" property="postName"/>
<result column="note" property="note"/>
</association>
</resultMap>
常用属性:
association内部标签:
- property:对应实体类中的属性名,必填项
- javaType:属性对应的Java类型
- columnPrefix:查询列的前缀,配置前缀后,在子标签配置result的column时可以省略前缀
association内部嵌套标签ID和result
- column:数据库的列名(或别名)
- property:java属性名
Tips:
- resultMap可以通过使用extends实现继承关系,简化很多配置工作量
- 关联的表查询的类添加前缀是编程的好习惯
- 通过添加完整的命名空间,可以引用其他XML文件的resultMap
2. association 嵌套查询
常用属性
- select: 另一个映射查询的ID,MyBatis会额外执行这个查询获取嵌套对象的结果
- column:数据库的列名(或别名)
- fetchType:数据加载方式,可选值为lazy和eager,分别为延迟加载和立即加载,这个配置会覆盖全局的lazyLoadingEnable配置
Tips:
概括的讲,N+1查询问题可以是这样引起的:
- 你执行了一个单独的SQL语句来获取结果列表(就是+1)
- 对返回的每条记录,你执行一个查询语句来为每个加载细节(就是N)
这个问题会导致成百上千的SQL语句被执行,这通常不是被期望的
解决办法:使用fetchType="lazy",并且全局setting进行改善
// 一对一的两种冠梁方式
@Test
public void testOneToOne(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
List<TUser> list1 = mapper.selectUserPosition1();
list1.forEach(System.out::println);
List<TUser> list2 = mapper.selectUserPosition2();
System.out.println("=============主查询结束============");
ObjectMapper om = new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);;
list2.forEach(list ->{
System.out.println(list.getPosition());
try {
System.out.println(om.writeValueAsString(list));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
});
}
嵌套结果查询
<select id="selectUserPosition1" resultMap="userAndPosition1" >
select
a.id,
userName,
realName,
sex,
mobile,
email,
a.note,
b.id post_id,
b.post_name,
b.note post_note
from t_user a,
t_position b
where a.position_id = b.id
</select>
<resultMap id="userAndPosition1" extends="BaseResultMap" type="TUser">
<association property="position" javaType="TPosition" columnPrefix="post_">
<id column="id" property="id"/>
<result column="name" property="postName"/>
<result column="note" property="note"/>
</association>
</resultMap>
<resultMap id="BaseResultMap" type="TUser">
<!-- <constructor> <idArg column="id" javaType="int"/> <arg column="userName"
javaType="String"/> </constructor> -->
<id column="id" property="id" />
<result column="userName" property="userName" />
<result column="realName" property="realName" />
<result column="sex" property="sex" />
<result column="mobile" property="mobile" />
<result column="email" property="email" />
<result column="note" property="note" />
</resultMap>
嵌套查询
<select id="selectUserPosition2" resultMap="userAndPosition2" >
select
a.id,
a.userName,
a.realName,
a.sex,
a.mobile,
a.position_id
from t_user a
</select>
<resultMap id="userAndPosition2" extends="BaseResultMap" type="TUser">
<association property="position" fetchType="lazy" column="position_id" select="com.wxc.mybatis.mapper.TPositionMapper.selectByPrimaryKey" />
</resultMap>
<resultMap id="BaseResultMap" type="TUser">
<!-- <constructor> <idArg column="id" javaType="int"/> <arg column="userName"
javaType="String"/> </constructor> -->
<id column="id" property="id" />
<result column="userName" property="userName" />
<result column="realName" property="realName" />
<result column="sex" property="sex" />
<result column="mobile" property="mobile" />
<result column="email" property="email" />
<result column="note" property="note" />
</resultMap>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
-->
select
<include refid="Base_Column_List" />
from t_position
where id = #{id,jdbcType=INTEGER}
</select>
<resultMap id="BaseResultMap" type="com.wxc.mybatis.bean.TPosition">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="post_name" jdbcType="VARCHAR" property="postName" />
<result column="note" jdbcType="VARCHAR" property="note" />
</resultMap>
2. 一对多:collection
collection支持的属性已经属性的作用和association完全相同
mybatis会根据id标签,进行字段的合并,合理配置好ID标签可以提高处理的效率
Tips:
如果要配置一个相当复杂的映射,一定要从基础映射开始配置,每增加一些配置就进行对应的测试,在循序渐进的过程中更容易发现和解决问题
selectUserJobs1:嵌套结果查询,会进行字段的合并
selectUserJobs2:嵌套查询,先查询主查询sql,然后分别往查询的结果集里塞入第二条sql的结果
// 一对多两种关联方式
@Test
public void testOneToMany(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
List<TUser> selectUserJobs1 = mapper.selectUserJobs1();
List<TUser> selectUserJobs2 = mapper.selectUserJobs2();
selectUserJobs1.forEach(System.out::println);
selectUserJobs2.forEach(System.out::println);
}
- 嵌套结果
<select id="selectUserJobs1" resultMap="userAndJobs1">
select
a.id,
a.userName,
a.realName,
a.sex,
a.mobile,
b.comp_name,
b.years,
b.title
from t_user a,
t_job_history b
where a.id = b.user_id
</select>
<resultMap id="userAndJobs1" extends="BaseResultMap" type="TUser">
<collection property="jobs"
ofType="com.wxc.mybatis.bean.TJobHistory" >
<result column="comp_name" property="compName" jdbcType="VARCHAR" />
<result column="years" property="years" jdbcType="INTEGER" />
<result column="title" property="title" jdbcType="VARCHAR" />
</collection>
</resultMap>
<resultMap id="BaseResultMap" type="TUser">
<!-- <constructor> <idArg column="id" javaType="int"/> <arg column="userName"
javaType="String"/> </constructor> -->
<id column="id" property="id" />
<result column="userName" property="userName" />
<result column="realName" property="realName" />
<result column="sex" property="sex" />
<result column="mobile" property="mobile" />
<result column="email" property="email" />
<result column="note" property="note" />
</resultMap>
嵌套查询
<select id="selectUserJobs2" resultMap="userAndJobs2">
select
a.id,
a.userName,
a.realName,
a.sex,
a.mobile
from t_user a
</select>
<resultMap id="userAndJobs2" extends="BaseResultMap" type="TUser">
<collection property="jobs" fetchType="lazy" column="id"
select="com.wxc.mybatis.mapper.TJobHistoryMapper.selectByUserId" />
</resultMap>
<resultMap id="BaseResultMap" type="TUser">
<!-- <constructor> <idArg column="id" javaType="int"/> <arg column="userName"
javaType="String"/> </constructor> -->
<id column="id" property="id" />
<result column="userName" property="userName" />
<result column="realName" property="realName" />
<result column="sex" property="sex" />
<result column="mobile" property="mobile" />
<result column="email" property="email" />
<result column="note" property="note" />
</resultMap>
<select id="selectByUserId" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List" />
from t_job_history
where user_id = #{userId,jdbcType=INTEGER}
</select>
<resultMap id="BaseResultMap" type="com.wxc.mybatis.bean.TJobHistory">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="user_id" property="userId" jdbcType="INTEGER" />
<result column="comp_name" property="compName" jdbcType="VARCHAR" />
<result column="years" property="years" jdbcType="INTEGER" />
<result column="title" property="title" jdbcType="VARCHAR" />
</resultMap>
3.多对多
先决条件一: 多对多需要一张中间表,建立连接关系
先决条件二:多对多关系是由两个一对多关系组成的,一对多可以用两种方式实现
@Test
public void testManyToMany(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TUserMapper mapper = sqlSession.getMapper(TUserMapper.class);
List<TUser> list = mapper.selectUserRole();
list.forEach(System.out::println);
TRoleMapper mapper1 = sqlSession.getMapper(TRoleMapper.class);
List<TRole> list2 = mapper1.selectRoleandUsers();
list2.forEach(System.out::println);
}
嵌套结果
<select id="selectUserRole" resultMap="userRoleInfo">
select a.id,
a.userName,
a.realName,
a.sex,
a.mobile,
a.note,
b.role_id,
c.role_name,
c.note role_note
from t_user a,
t_user_role b,
t_role c
where a.id = b.user_id AND
b.role_id = c.id
</select>
嵌套查询
<select id="selectRoleandUsers" resultMap="RoleandUsers">
select
<include refid="Base_Column_List" />
from t_role
</select>
<resultMap id="RoleandUsers" type="TRole" extends="BaseResultMap">
<collection property="users" fetchType="lazy" column="id" select="com.wxc.mybatis.mapper.TUserMapper.selectUserByRoleId"></collection>
</resultMap>
<select id="selectUserByRoleId" resultMap="userRoleInfo">
select
<include refid="Base_Column_List" />
from t_user a,
t_user_role b
where a.id = b.user_id and
b.role_id = #{id}
</select>
<sql id="Base_Column_List">
id, userName, realName, sex, mobile, email, note,
position_id
</sql>
8. 缓存
一级缓存:
- 一级缓存会默认启用【flushCache="false"】,想要关闭一级缓存可以在select标签上配置【flushCache="true"】
- 一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存中已经存在该键值时,则会返回缓存中的对象
- 任何的INSERT,UPDATE,DELETE操作都会清空一级缓存
二级缓存
-
二级缓存存在于SqlSessionFactory的生命周期,可以理解为是跨SqlSession的。缓存是以namespace为单位的,不同的namespace下的操作互不影响。
-
setting参数cacheEnable,这个参数是二级缓存的全局开关,默认值是true,如果把这个参数设置为false,即使后面有二级缓存配置,也不会生效
-
想要开启二级缓存,需要你在你的SQL映射文件中添加配置
eviction:缓存情况策略
flushInterval:多长时间清空缓存
size:缓存的最大数量(个数)
readOnly:缓存只读
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
字面上看就是这样。这个简单语句的效果如下:
- 映射语句文件中所有的select语句将会被缓存
- 映射语句文件中所有的insert,update,delete语句会刷新缓存
- 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回
- 根据时间表(比如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新
- 缓存会存储列表集合或对象(无论查询方法返回什么)的512个引用
- 缓存会被视为read/write(可读/可写)的缓存
二级缓存是一个常驻内存的缓存。
二级缓存容易出现脏读,建议避免使用二级缓存,在业务层使用可控的缓存代替更好
二级缓存可以共享,使用
3.缓存使用示意图
调用过程解读
- 每次与数据库的连接都会优先从缓存中获取数据
- 先查二级缓存,再查一级缓存
- 二级缓存以namespace为单位,是SqlSession共享的,容易出现脏读,建议避免使用二级缓存
- 一级缓存是SqlSession独享的,建议开启
二级缓存出现脏读的原因:
当两个二级缓存的键值是一样的(缓存的key是同一个),那么SqlSession1加载的那个二级缓存发生了修改,那么会清空这个二级缓存,但是另外的sqlsession2持有的二级缓存并没有收到影响,所以会出现脏读。
2. mybatis源码
1. mybatis整体架构
2. 外观模式(门面模式)
门面模式:提供了一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。
优点:
使复杂的子系统的接口变得更加简单可用,减少客户端对子系统的依赖,达到了解耦的效果,遵循了OO原则中的迪米特法则,对内封装具体细节,对外只暴露必要的接口。
使用场景:
- 一个复杂的模块或子系统提供一个外界访问的接口
- 子系统相对独立。外界对子系统的访问要暗箱操作即可 。
3. 设计模式的几个原则
- 单一职责原则:一个类或一个接口只负责唯一的职责,尽量设计出功能单一的接口
- 依赖倒转原则:高层模块不应该依赖低层模块的具体实现,解耦高层与低层。即面向接口编程。当实现发生变化的时候,只需要提供新的实现类,不需要修改高层模块代码
- 开放-封闭原则:程序对外扩展开放,对修改关闭。换句话说,我们可以通过添加新模块来满足新需求,而不是通过修改原来的代码来满足新需求
- 迪米特法则:一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合。
- 里氏替换原则:所有引用基类(父类)的地方,必须能透明的使用其子类的对象
- 接口隔离原则:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
AOP+自定义注解
4. 日志模块
Mybatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同,而MyBatis统一提供了trace,debug,waring,error四个级别。
- 自动扫描日志实现,并且第三方日志插件优先级如下:slf4j-->commonsLoging-->Log4J2-->Log4J-->JdkLog
- 日志的使用要优雅的嵌入主体功能中
MyBatis日志接口把日志分为四个等级error,debug,trace,warn
package org.apache.ibatis.logging;
/**
* @author Clinton Begin
*/
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
1. 适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容接口的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由接口不兼容而不能一起工作的那些类可以一起工作。
Target:目标角色,期待得到的接口
Adaptee:适配者角色,被适配的接口
Adapter:适配器角色,将源接口转换成目标接口
比如:Target是mybatis日志接口,Adaptee是Log4J日志接口,Adapter是适配器
适用场景:当调用双方都不太容易修改的时候,为了复用现有的组件,可以使用适配器模式。在系统中接入第三方组件时经常使用。
注意:如果系统中存在过多的适配器,会增加系统的复杂性,设计人员应该考虑对系统进行重构。
2. mybatis使用Log4J
package org.apache.ibatis.logging.log4j;
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/**
* @author Eduardo Macarron
*/
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
3.mybatis使用jdk日志
package org.apache.ibatis.logging.jdk14;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.ibatis.logging.Log;
/**
* @author Clinton Begin
*/
//jdkLog的适配器
public class Jdk14LoggingImpl implements Log {
//真正提供日志能力的jdk的日志类
private final Logger log;
public Jdk14LoggingImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isLoggable(Level.FINE);
}
@Override
public boolean isTraceEnabled() {
return log.isLoggable(Level.FINER);
}
@Override
public void error(String s, Throwable e) {
log.log(Level.SEVERE, s, e);
}
@Override
public void error(String s) {
log.log(Level.SEVERE, s);
}
@Override
public void debug(String s) {
log.log(Level.FINE, s);
}
@Override
public void trace(String s) {
log.log(Level.FINER, s);
}
@Override
public void warn(String s) {
log.log(Level.WARNING, s);
}
}
4.日志模块类图
5. 日志加载顺序
第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
-
setImplementation方法获取日志的实现类的构造函数,放到logConstructor。
-
调用静态代码块方法tryImplementation(...),如果logConstructor为空的话,执行runnable方法,不为空的话,catch空处理。那么如果任意实现类,只会加载第一个。如果都没有的话,就没有日志实现,不会抛出任何异常。
package org.apache.ibatis.logging;
import java.lang.reflect.Constructor;
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers
*/
public static final String MARKER = "MYBATIS";
//被选定的第三方日志组件适配器的构造方法
private static Constructor<? extends Log> logConstructor;
//自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {//当构造方法不为空才执行方法
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
//通过指定的log类来初始化构造方法
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
6. 代理模式
代理模式定义:给目标对象提供一个代理对象,并由代理对象控制目标对象的引用
目的:
- 通过引入代理对象的方式来间接访问目标对象,防止访问目标对象给系统带来的不必要复杂性
- 通过代理对象对原有的业务进行增强
适配器模式和代理模式的区别
适配器模式只有适配器需要实现接口(目标接口)
代理模式的代理对象和被代理对象都需要实现同一个接口
代理模式类图:
7. mybatis在哪些地方需要打日志
- 创建prepareStatement时,打印执行的SQL语句
- 访问数据库时,打印参数的类型和值
- 查询出结果后,打印结果数据条数
1. 日志MookJDBC包类图
-
ConnectionLogger: 负责打印连接信息和SQL语句,并创建PrepareStatementLogger
-
PrepareStatementLogger:负责打印参数信息,并创建ResultSetLogger
-
ResultSetLogger:负责打印数据结果集
2.所有日志增强的抽象基类BaseJdbcLogger
-
EXECUTE_METHODS:保存PrepareStatement中常用的执行SQL语句的方法
-
SET_METHODS:保存PrepareStatement常用的set方法(当调用这些方法的时候,会把方法的参数以占位符的方式保存)
- 保存到以下容器
- columnMap:保存PrepareStatement中set方法的键值对
- columnNames:保存PrepareStatement中set方法的key值
- columnValues:保存PrepareStatement中方法的value值
- 保存到以下容器
package org.apache.ibatis.logging.jdbc;
import java.sql.Array;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.reflection.ArrayUtil;
/**
* Base class for proxies to do logging
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
//所有日志增强的抽象基类
public abstract class BaseJdbcLogger {
//保存preparestatment中常用的set方法(占位符赋值)
protected static final Set<String> SET_METHODS = new HashSet<>();
//保存preparestatment中常用的执行sql语句的方法
protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
//保存preparestatment中set方法的键值对
private final Map<Object, Object> columnMap = new HashMap<>();
//保存preparestatment中set方法的key值
private final List<Object> columnNames = new ArrayList<>();
//保存preparestatment中set方法的value值
private final List<Object> columnValues = new ArrayList<>();
protected Log statementLog;
protected int queryStack;
/*
* Default constructor
*/
public BaseJdbcLogger(Log log, int queryStack) {
this.statementLog = log;
if (queryStack == 0) {
this.queryStack = 1;
} else {
this.queryStack = queryStack;
}
}
static {
SET_METHODS.add("setString");
SET_METHODS.add("setNString");
SET_METHODS.add("setInt");
SET_METHODS.add("setByte");
SET_METHODS.add("setShort");
SET_METHODS.add("setLong");
SET_METHODS.add("setDouble");
SET_METHODS.add("setFloat");
SET_METHODS.add("setTimestamp");
SET_METHODS.add("setDate");
SET_METHODS.add("setTime");
SET_METHODS.add("setArray");
SET_METHODS.add("setBigDecimal");
SET_METHODS.add("setAsciiStream");
SET_METHODS.add("setBinaryStream");
SET_METHODS.add("setBlob");
SET_METHODS.add("setBoolean");
SET_METHODS.add("setBytes");
SET_METHODS.add("setCharacterStream");
SET_METHODS.add("setNCharacterStream");
SET_METHODS.add("setClob");
SET_METHODS.add("setNClob");
SET_METHODS.add("setObject");
SET_METHODS.add("setNull");
EXECUTE_METHODS.add("execute");
EXECUTE_METHODS.add("executeUpdate");
EXECUTE_METHODS.add("executeQuery");
EXECUTE_METHODS.add("addBatch");
}
protected void setColumn(Object key, Object value) {
columnMap.put(key, value);
columnNames.add(key);
columnValues.add(value);
}
protected Object getColumn(Object key) {
return columnMap.get(key);
}
//打印参数的值以及参数的类型
protected String getParameterValueString() {
List<Object> typeList = new ArrayList<>(columnValues.size());
for (Object value : columnValues) {
if (value == null) {
typeList.add("null");
} else {
typeList.add(objectValueString(value) + "(" + value.getClass().getSimpleName() + ")");
}
}
final String parameters = typeList.toString();
return parameters.substring(1, parameters.length() - 1);
}
protected String objectValueString(Object value) {
if (value instanceof Array) {
try {
return ArrayUtil.toString(((Array) value).getArray());
} catch (SQLException e) {
return value.toString();
}
}
return value.toString();
}
protected String getColumnString() {
return columnNames.toString();
}
protected void clearColumnInfo() {
columnMap.clear();
columnNames.clear();
columnValues.clear();
}
protected String removeBreakingWhitespace(String original) {
StringTokenizer whitespaceStripper = new StringTokenizer(original);
StringBuilder builder = new StringBuilder();
while (whitespaceStripper.hasMoreTokens()) {
builder.append(whitespaceStripper.nextToken());
builder.append(" ");
}
return builder.toString();
}
protected boolean isDebugEnabled() {
return statementLog.isDebugEnabled();
}
protected boolean isTraceEnabled() {
return statementLog.isTraceEnabled();
}
protected void debug(String text, boolean input) {
if (statementLog.isDebugEnabled()) {
statementLog.debug(prefix(input) + text);
}
}
protected void trace(String text, boolean input) {
if (statementLog.isTraceEnabled()) {
statementLog.trace(prefix(input) + text);
}
}
private String prefix(boolean isInput) {
char[] buffer = new char[queryStack * 2 + 2];
Arrays.fill(buffer, '=');
buffer[queryStack * 2 + 1] = ' ';
if (isInput) {
buffer[queryStack * 2] = '>';
} else {
buffer[0] = '<';
}
return new String(buffer);
}
}
3.ConnectionLogger
ConnectionLogger实现了InvocationHandler,说明它是一个增强器,他是用来增强Connection的。
- 如果是调用PrepareStatement,PrepareCall,CreateStatement的方法,那么打印要执行的SQL语句,并返回prepareStatement代理对象,让prepareStatement也具备日志能力,打印参数
ConnectionLogger是对真正连接对象的增强和sql语句的打印
package org.apache.ibatis.logging.jdbc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.reflection.ExceptionUtil;
/**
* Connection proxy to add logging
*
* @author Clinton Begin
* @author Eduardo Macarron
*
*/
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
//真正的连接对象
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
//对连接的增强
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
//如果是从Obeject继承的方法直接忽略
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句
//并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
return stmt;
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);//创建代理对象
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
/*
* Creates a logging version of a connection
*
* @param conn - the original connection
* @return - the connection with logging
*/
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
/*
* return the wrapped connection
*
* @return the connection
*/
public Connection getConnection() {
return connection;
}
}
4.PreparedStatementLogger
对PrepareStatement对象的增强,拦截EXECUTE_METHODS方法,并把参数打印出来。
当调用set方法的时候,【set方法的执行一定是早于SQL语句的执行】,会把参进来的值设置到columnMap,columnNames,columnValues,为打印参数做好准备。
功能:
- 增强PreparedStatement的setXXX方法,将参数设置到columnMap,columnNames,columnValues,为参数打印做好准备
- 增强preparedStatement的execute相关方法,当方法执行时,通过动态代理打印参数,返回具有动态代理能力的resultSet
- 如果是查询,增强preparedStatement的getResultSet方法,返回具有动态代理能力的resultSet
- 如果是更新,直接打印影响的行数
package org.apache.ibatis.logging.jdbc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.reflection.ExceptionUtil;
/**
* PreparedStatement proxy to add logging
*
* @author Clinton Begin
* @author Eduardo Macarron
*
*/
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
private final PreparedStatement statement;
private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.statement = stmt;
}
//1,增强PreparedStatement的setxxx方法将参数设置到columnMap、columnNames、columnValues,为打印参数做好准备
//2. 增强PreparedStatement的execute相关方法,当方法执行时,通过动态代理打印参数,返回动态代理能力的resultSet
//3. 如果是查询,增强PreparedStatement的getResultSet方法,返回动态代理能力的resultSet
// 如果是更新,直接打印影响的行数
@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())) {//增强PreparedStatement的execute相关方法
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);//当方法执行时,通过动态代理打印参数
}
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {//返回动态代理能力的resultSet
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())) {//将参数设置到columnMap、columnNames、columnValues,为打印参数做好准备
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
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);
}
}
/*
* Creates a logging version of a PreparedStatement
*
* @param stmt - the statement
* @param sql - the sql statement
* @return - the proxy
*/
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
ClassLoader cl = PreparedStatement.class.getClassLoader();
return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
/*
* Return the wrapped prepared statement
*
* @return the PreparedStatement
*/
public PreparedStatement getPreparedStatement() {
return statement;
}
}
5. ResultSetLogger
ResultSetLogger实现了InvocationHandler,对ResultSet类进行增强。
增强功能:
当调用next方法,判断是否还有数据,如果还有数据,让计数器+1,如果没有数据库了,打印计数器rows的值。打印的rows就是数据条数
package org.apache.ibatis.logging.jdbc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashSet;
import java.util.Set;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.reflection.ExceptionUtil;
/**
* ResultSet proxy to add logging
*
* @author Clinton Begin
* @author Eduardo Macarron
*
*/
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
private static Set<Integer> BLOB_TYPES = new HashSet<>();
private boolean first = true;
private int rows;
private final ResultSet rs;
private final Set<Integer> blobColumns = new HashSet<>();
static {
BLOB_TYPES.add(Types.BINARY);
BLOB_TYPES.add(Types.BLOB);
BLOB_TYPES.add(Types.CLOB);
BLOB_TYPES.add(Types.LONGNVARCHAR);
BLOB_TYPES.add(Types.LONGVARBINARY);
BLOB_TYPES.add(Types.LONGVARCHAR);
BLOB_TYPES.add(Types.NCLOB);
BLOB_TYPES.add(Types.VARBINARY);
}
private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.rs = rs;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
Object o = method.invoke(rs, params);//执行result.next方法,判断是否还有数据
if ("next".equals(method.getName())) {//如果还有数据,计数器rows加一
if (((Boolean) o)) {
rows++;
if (isTraceEnabled()) {
ResultSetMetaData rsmd = rs.getMetaData();
final int columnCount = rsmd.getColumnCount();
if (first) {
first = false;
printColumnHeaders(rsmd, columnCount);
}
printColumnValues(columnCount);
}
} else {
debug(" Total: " + rows, false);//如果没有数据了,打印rows,打印查询出来的数据条数
}
}
clearColumnInfo();
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
StringBuilder row = new StringBuilder();
row.append(" Columns: ");
for (int i = 1; i <= columnCount; i++) {
if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
blobColumns.add(i);
}
String colname = rsmd.getColumnLabel(i);
row.append(colname);
if (i != columnCount) {
row.append(", ");
}
}
trace(row.toString(), false);
}
private void printColumnValues(int columnCount) {
StringBuilder row = new StringBuilder();
row.append(" Row: ");
for (int i = 1; i <= columnCount; i++) {
String colname;
try {
if (blobColumns.contains(i)) {
colname = "<<BLOB>>";
} else {
colname = rs.getString(i);
}
} catch (SQLException e) {
// generally can't call getString() on a BLOB column
colname = "<<Cannot Display>>";
}
row.append(colname);
if (i != columnCount) {
row.append(", ");
}
}
trace(row.toString(), false);
}
/*
* Creates a logging version of a ResultSet
*
* @param rs - the ResultSet to proxy
* @return - the ResultSet with logging
*/
public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
ClassLoader cl = ResultSet.class.getClassLoader();
return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
}
/*
* Get the wrapped result set
*
* @return the resultSet
*/
public ResultSet getRs() {
return rs;
}
}
6.mybatis日志入口
mybatis在哪个地方开启的日志增强功能?
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
//创建Statement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取connection对象的动态代理,添加日志能力;
Connection connection = getConnection(statementLog);
//通过不同的StatementHandler,利用connection创建(prepare)Statement
stmt = handler.prepare(connection, transaction.getTimeout());
//使用parameterHandler处理占位符
handler.parameterize(stmt);
return stmt;
}
org.apache.ibatis.executor.BaseExecutor#getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
5. 数据源模块
常见的数据源组件都实现了java.sql.DataSource接口
mybatis不但要能集成第三方的数据源组件,自身也提供了数据源的实现
一般情况下,数据源的初始化过程参数较多,比较复杂
1.为什么要使用工厂模式
创建对象的方式:
- 使用new关键字直接创建对象
- 通过反射机制创建对象
- 通过工厂类创建对象
1.2两种方式的缺点:
- 对象的创建和使用的职责耦合在一起,违反了单一职责原则
- 当业务扩展的时候,必须修改业务代码,违反了开闭原则
3.方式的优点
- 把对象的创建和使用分开,对象创建和对象使用的职责解耦
- 如果创建对象的过程很复杂,创建过程同一到工厂进行管理,既减少了重复代码,也方便对创建过程的修改和维护
- 当业务扩展的时候,只需要增加工厂子类,符合开闭原则
2.数据源连接池核心类
- PooledDataSource:一个简单,同步的,线程安全的数据库连接池
- PooledConnection:使用动态代理封装了真正的数据库连接对象
- PoolState:用于管理PooledConnection对象状态的组件,通过两个list分别管理空闲的连接资源
3. 请详细描述从数据库连接池中获取一个连接资源的过程
getConnection:
- 如果有空闲连接,直接从空闲连接中获取,
- 如果没有空闲连接,判断活跃连接池中的数量是否大于最大连接数,如果不大于,直接创建新的连接。如果等于,则不能创建新的连接,需要从活跃线程中获取连接。
- 从活跃连接中获取到连接【获取最早创建的活跃连接】,需要判断连接是否超时,如果超时,超时连接数+1,并把该连接从活跃连接中删除,并回滚未提交事务【如果是手动提交的话】,基于该连接创建一个新连接,并让老连接失效【对于数据库来说,并没有创建新连接】
- 如果没有超时,无法创建新的连接,只能阻塞等待
- 获取到连接后需要判断连接是否有效【会ping数据库】,然后把连接添加到活跃连接集合里面并做相应的统计操作。如果连接无效的话,会把当前无效连接数+1,当当前无效连接数>最大空闲连接数+最多允许的无效连接数,那么会报错.
pushConnection()
- 回收连接资源的时候,先把连接从活跃连接中删掉。
- 判断连接是否有效,如果连接无效,什么都不做。
- 如果连接有效,判断空闲连接是否达到上限。如果达到,关闭真实数据库连接,并把连接对象设置为无效【如果是手动提交,会先回滚数据库】
- 如果没有达到空闲连接上限,会基于该连接创建一个新连接,放到空闲连接池里面,并把老连接设置为无效,通知其他阻塞的线程。
注:这里的失效指的是PooConnection对象中的boolean类型的valid变量,真正的连接是与数据库交互建立的连接,是不断在复用的。因为创建连接和销毁连接是非常耗费资源的事情,所以尽量避免。
4. PooledConnection
package org.apache.ibatis.datasource.pooled;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.ibatis.reflection.ExceptionUtil;
/**
* 使用动态代理封装了真正的数据库连接对象
* @author Clinton Begin
*/
//
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
private final int hashCode;
//记录当前连接所在的数据源对象,本次连接是有这个数据源创建的,关闭后也是回到这个数据源;
private final PooledDataSource dataSource;
//真正的连接对象
private final Connection realConnection;
//连接的代理对象
private final Connection proxyConnection;
//从数据源取出来连接的时间戳
private long checkoutTimestamp;
//连接创建的的时间戳
private long createdTimestamp;
//连接最后一次使用的时间戳
private long lastUsedTimestamp;
//根据数据库url、用户名、密码生成一个hash值,唯一标识一个连接池
private int connectionTypeCode;
//连接是否有效
private boolean valid;
/*
* Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
*
* @param connection - the connection that is to be presented as a pooled connection
* @param dataSource - the dataSource that the connection is from
*/
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
/*
* Invalidates the connection
*/
public void invalidate() {
valid = false;
}
/*
* Method to see if the connection is usable
*
* @return True if the connection is usable
*/
public boolean isValid() {
return valid && realConnection != null && dataSource.pingConnection(this);
}
/*
* Getter for the *real* connection that this wraps
*
* @return The connection
*/
public Connection getRealConnection() {
return realConnection;
}
/*
* Getter for the proxy for the connection
*
* @return The proxy
*/
public Connection getProxyConnection() {
return proxyConnection;
}
/*
* Gets the hashcode of the real connection (or 0 if it is null)
*
* @return The hashcode of the real connection (or 0 if it is null)
*/
public int getRealHashCode() {
return realConnection == null ? 0 : realConnection.hashCode();
}
/*
* Getter for the connection type (based on url + user + password)
*
* @return The connection type
*/
public int getConnectionTypeCode() {
return connectionTypeCode;
}
/*
* Setter for the connection type
*
* @param connectionTypeCode - the connection type
*/
public void setConnectionTypeCode(int connectionTypeCode) {
this.connectionTypeCode = connectionTypeCode;
}
/*
* Getter for the time that the connection was created
*
* @return The creation timestamp
*/
public long getCreatedTimestamp() {
return createdTimestamp;
}
/*
* Setter for the time that the connection was created
*
* @param createdTimestamp - the timestamp
*/
public void setCreatedTimestamp(long createdTimestamp) {
this.createdTimestamp = createdTimestamp;
}
/*
* Getter for the time that the connection was last used
*
* @return - the timestamp
*/
public long getLastUsedTimestamp() {
return lastUsedTimestamp;
}
/*
* Setter for the time that the connection was last used
*
* @param lastUsedTimestamp - the timestamp
*/
public void setLastUsedTimestamp(long lastUsedTimestamp) {
this.lastUsedTimestamp = lastUsedTimestamp;
}
/*
* Getter for the time since this connection was last used
*
* @return - the time since the last use
*/
public long getTimeElapsedSinceLastUse() {
return System.currentTimeMillis() - lastUsedTimestamp;
}
/*
* Getter for the age of the connection
*
* @return the age
*/
public long getAge() {
return System.currentTimeMillis() - createdTimestamp;
}
/*
* Getter for the timestamp that this connection was checked out
*
* @return the timestamp
*/
public long getCheckoutTimestamp() {
return checkoutTimestamp;
}
/*
* Setter for the timestamp that this connection was checked out
*
* @param timestamp the timestamp
*/
public void setCheckoutTimestamp(long timestamp) {
this.checkoutTimestamp = timestamp;
}
/*
* Getter for the time that this connection has been checked out
*
* @return the time
*/
public long getCheckoutTime() {
return System.currentTimeMillis() - checkoutTimestamp;
}
@Override
public int hashCode() {
return hashCode;
}
/*
* Allows comparing this connection to another
*
* @param obj - the other connection to test for equality
* @see Object#equals(Object)
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof PooledConnection) {
return realConnection.hashCode() == ((PooledConnection) obj).realConnection.hashCode();
} else if (obj instanceof Connection) {
return hashCode == obj.hashCode();
} else {
return false;
}
}
/*
* Required for InvocationHandler implementation.
* 此方法专门用来增强数据库connect对象,使用前检查连接是否有效,关闭时对连接进行回收
*
* @param proxy - not used
* @param method - the method to be executed
* @param args - the parameters to be passed to the method
* @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {//如果是调用连接的close方法,不是真正的关闭,而是回收到连接池
dataSource.pushConnection(this);//通过pooled数据源来进行回收
return null;
} else {
try {
//使用前要检查当前连接是否有效
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();//
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
}
5. PooledDataSource
package org.apache.ibatis.datasource.pooled;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
/**
* This is a simple, synchronous, thread-safe database connection pool.
* PooledDataSource:一个简单,同步的、线程安全的数据库连接池
*
* @author Clinton Begin
*/
//使用连接池的数据源
public class PooledDataSource implements DataSource {
private static final Log log = LogFactory.getLog(PooledDataSource.class);
private final PoolState state = new PoolState(this);
//真正用于创建连接的数据源
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
//最大活跃连接数
protected int poolMaximumActiveConnections = 10;
//最大闲置连接数
protected int poolMaximumIdleConnections = 5;
//最大checkout时长(最长使用时间)
protected int poolMaximumCheckoutTime = 20000;
//无法取得连接是最大的等待时间
protected int poolTimeToWait = 20000;
//最多允许几次无效连接
protected int poolMaximumLocalBadConnectionTolerance = 3;
//测试连接是否有效的sql语句
protected String poolPingQuery = "NO PING QUERY SET";
//是否允许测试连接
protected boolean poolPingEnabled;
//配置一段时间,当连接在这段时间内没有被使用,才允许测试连接是否有效
protected int poolPingConnectionsNotUsedFor;
//根据数据库url、用户名、密码生成一个hash值,唯一标识一个连接池,由这个连接池生成的连接都会带上这个值
private int expectedConnectionTypeCode;
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
public PooledDataSource(UnpooledDataSource dataSource) {
this.dataSource = dataSource;
}
public PooledDataSource(String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(String driver, String url, Properties driverProperties) {
dataSource = new UnpooledDataSource(driver, url, driverProperties);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
}
@Override
public void setLoginTimeout(int loginTimeout) throws SQLException {
DriverManager.setLoginTimeout(loginTimeout);
}
@Override
public int getLoginTimeout() throws SQLException {
return DriverManager.getLoginTimeout();
}
@Override
public void setLogWriter(PrintWriter logWriter) throws SQLException {
DriverManager.setLogWriter(logWriter);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return DriverManager.getLogWriter();
}
public void setDriver(String driver) {
dataSource.setDriver(driver);
forceCloseAll();
}
public void setUrl(String url) {
dataSource.setUrl(url);
forceCloseAll();
}
public void setUsername(String username) {
dataSource.setUsername(username);
forceCloseAll();
}
public void setPassword(String password) {
dataSource.setPassword(password);
forceCloseAll();
}
public void setDefaultAutoCommit(boolean defaultAutoCommit) {
dataSource.setAutoCommit(defaultAutoCommit);
forceCloseAll();
}
public void setDefaultTransactionIsolationLevel(Integer defaultTransactionIsolationLevel) {
dataSource.setDefaultTransactionIsolationLevel(defaultTransactionIsolationLevel);
forceCloseAll();
}
public void setDriverProperties(Properties driverProps) {
dataSource.setDriverProperties(driverProps);
forceCloseAll();
}
/*
* The maximum number of active connections
*
* @param poolMaximumActiveConnections The maximum number of active connections
*/
public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
this.poolMaximumActiveConnections = poolMaximumActiveConnections;
forceCloseAll();
}
/*
* The maximum number of idle connections
*
* @param poolMaximumIdleConnections The maximum number of idle connections
*/
public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
this.poolMaximumIdleConnections = poolMaximumIdleConnections;
forceCloseAll();
}
/*
* The maximum number of tolerance for bad connection happens in one thread
* which are applying for new {@link PooledConnection}
*
* @param poolMaximumLocalBadConnectionTolerance
* max tolerance for bad connection happens in one thread
*
* @since 3.4.5
*/
public void setPoolMaximumLocalBadConnectionTolerance(
int poolMaximumLocalBadConnectionTolerance) {
this.poolMaximumLocalBadConnectionTolerance = poolMaximumLocalBadConnectionTolerance;
}
/*
* The maximum time a connection can be used before it *may* be
* given away again.
*
* @param poolMaximumCheckoutTime The maximum time
*/
public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
forceCloseAll();
}
/*
* The time to wait before retrying to get a connection
*
* @param poolTimeToWait The time to wait
*/
public void setPoolTimeToWait(int poolTimeToWait) {
this.poolTimeToWait = poolTimeToWait;
forceCloseAll();
}
/*
* The query to be used to check a connection
*
* @param poolPingQuery The query
*/
public void setPoolPingQuery(String poolPingQuery) {
this.poolPingQuery = poolPingQuery;
forceCloseAll();
}
/*
* Determines if the ping query should be used.
*
* @param poolPingEnabled True if we need to check a connection before using it
*/
public void setPoolPingEnabled(boolean poolPingEnabled) {
this.poolPingEnabled = poolPingEnabled;
forceCloseAll();
}
/*
* If a connection has not been used in this many milliseconds, ping the
* database to make sure the connection is still good.
*
* @param milliseconds the number of milliseconds of inactivity that will trigger a ping
*/
public void setPoolPingConnectionsNotUsedFor(int milliseconds) {
this.poolPingConnectionsNotUsedFor = milliseconds;
forceCloseAll();
}
public String getDriver() {
return dataSource.getDriver();
}
public String getUrl() {
return dataSource.getUrl();
}
public String getUsername() {
return dataSource.getUsername();
}
public String getPassword() {
return dataSource.getPassword();
}
public boolean isAutoCommit() {
return dataSource.isAutoCommit();
}
public Integer getDefaultTransactionIsolationLevel() {
return dataSource.getDefaultTransactionIsolationLevel();
}
public Properties getDriverProperties() {
return dataSource.getDriverProperties();
}
public int getPoolMaximumActiveConnections() {
return poolMaximumActiveConnections;
}
public int getPoolMaximumIdleConnections() {
return poolMaximumIdleConnections;
}
public int getPoolMaximumLocalBadConnectionTolerance() {
return poolMaximumLocalBadConnectionTolerance;
}
public int getPoolMaximumCheckoutTime() {
return poolMaximumCheckoutTime;
}
public int getPoolTimeToWait() {
return poolTimeToWait;
}
public String getPoolPingQuery() {
return poolPingQuery;
}
public boolean isPoolPingEnabled() {
return poolPingEnabled;
}
public int getPoolPingConnectionsNotUsedFor() {
return poolPingConnectionsNotUsedFor;
}
/*
* Closes all active and idle connections in the pool
*/
public void forceCloseAll() {
synchronized (state) {
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
for (int i = state.activeConnections.size(); i > 0; i--) {
try {
PooledConnection conn = state.activeConnections.remove(i - 1);
conn.invalidate();
Connection realConn = conn.getRealConnection();
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
realConn.close();
} catch (Exception e) {
// ignore
}
}
for (int i = state.idleConnections.size(); i > 0; i--) {
try {
PooledConnection conn = state.idleConnections.remove(i - 1);
conn.invalidate();
Connection realConn = conn.getRealConnection();
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
realConn.close();
} catch (Exception e) {
// ignore
}
}
}
if (log.isDebugEnabled()) {
log.debug("PooledDataSource forcefully closed/removed all connections.");
}
}
public PoolState getPoolState() {
return state;
}
private int assembleConnectionTypeCode(String url, String username, String password) {
return ("" + url + username + password).hashCode();
}
//回收连接资源
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {//回收连接必须是同步的
state.activeConnections.remove(conn);//从活跃连接池中删除此连接
if (conn.isValid()) {
//判断闲置连接池资源是否已经达到上限
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
//没有达到上限,进行回收
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();//如果还有事务没有提交,进行回滚操作
}
//基于该连接,创建一个新的连接资源,并刷新连接状态
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
//老连接失效
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
//唤醒其他被阻塞的线程
state.notifyAll();
} else {//如果闲置连接池已经达到上限了,将连接真实关闭
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//关闭真的数据库连接
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
//将连接对象设置为无效
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
//从连接池获取资源
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();//记录尝试获取连接的起始时间戳
int localBadConnectionCount = 0;//初始化获取到无效连接的次数
while (conn == null) {
synchronized (state) {//获取连接必须是同步的
if (!state.idleConnections.isEmpty()) {//检测是否有空闲连接
// Pool has available connection
//有空闲连接直接使用
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {// 没有空闲连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {//判断活跃连接池中的数量是否大于最大连接数
// 没有则可创建新的连接
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {// 如果已经等于最大连接数,则不能创建新连接
//获取最早创建的连接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {//检测是否已经以及超过最长使用时间
// 如果超时,对超时连接的信息进行统计
state.claimedOverdueConnectionCount++;//超时连接次数+1
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;//累计超时时间增加
state.accumulatedCheckoutTime += longestCheckoutTime;//累计的使用连接的时间增加
state.activeConnections.remove(oldestActiveConnection);//从活跃队列中删除
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果超时连接未提交,则手动回滚
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {//发生异常仅仅记录日志
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
//在连接池中创建新的连接,注意对于数据库来说,并没有创建新连接;
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
//让老连接失效
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// 无空闲连接,最早创建的连接没有失效,无法创建新连接,只能阻塞
try {
if (!countedWait) {
state.hadToWaitCount++;//连接池累计等待次数加1
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);//阻塞等待指定时间
state.accumulatedWaitTime += System.currentTimeMillis() - wt;//累计等待时间增加
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {//获取连接成功的,要测试连接是否有效,同时更新统计数据
// ping to server and check the connection is valid or not
if (conn.isValid()) {//检测连接是否有效
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();//如果遗留历史的事务,回滚
}
//连接池相关统计信息更新
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {//如果连接无效
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;//累计的获取无效连接次数+1
localBadConnectionCount++;//当前获取无效连接次数+1
conn = null;
//拿到无效连接,但如果没有超过重试的次数,允许再次尝试获取连接,否则抛出异常
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
/*
* Method to check to see if a connection is still usable
*
* @param conn - the connection to check
* @return True if the connection is still usable
*/
protected boolean pingConnection(PooledConnection conn) {
boolean result = true;
try {
result = !conn.getRealConnection().isClosed();
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
result = false;
}
if (result) {
if (poolPingEnabled) {
if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
try {
if (log.isDebugEnabled()) {
log.debug("Testing connection " + conn.getRealHashCode() + " ...");
}
Connection realConn = conn.getRealConnection();
try (Statement statement = realConn.createStatement()) {
statement.executeQuery(poolPingQuery).close();
}
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
result = true;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
}
} catch (Exception e) {
log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
try {
conn.getRealConnection().close();
} catch (Exception e2) {
//ignore
}
result = false;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
}
}
}
}
return result;
}
/*
* Unwraps a pooled connection to get to the 'real' connection
*
* @param conn - the pooled connection to unwrap
* @return The 'real' connection
*/
public static Connection unwrapConnection(Connection conn) {
if (Proxy.isProxyClass(conn.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(conn);
if (handler instanceof PooledConnection) {
return ((PooledConnection) handler).getRealConnection();
}
}
return conn;
}
protected void finalize() throws Throwable {
forceCloseAll();
super.finalize();
}
public <T> T unwrap(Class<T> iface) throws SQLException {
throw new SQLException(getClass().getName() + " is not a wrapper.");
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
public Logger getParentLogger() {
return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); // requires JDK version 1.6
}
}
6.缓存模块
- Mybatis缓存的实现是基于Map的,从缓存里面读写数据是缓存模块的核心基础功能
- 除了核心功能外,还有很多的额外功能,比如:防止缓存击穿,添加缓存清空策略(FIFO,LRU),序列化功能,日志能力,定时清空能力
- 附加功能可以以任意的组合附加的核心基础功能上
1. 装饰器模式
装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代理继承关系,更加灵活,同时避免体系的快速膨胀。
1.装饰器模式uml类图
- 组件(Component):组件接口定义了全部组件类和装饰器实现的行为
- 组件实现类(ConcreteCompont):实现Component接口,组件实现类就是被装饰器修饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的
- 装饰器抽象类(Decorator):实现Component接口的抽象类,在其中封装了一个Component对象,也就是被装饰的对象
- 具体装饰类(ConcreteDecorator):该实现类要向被装饰类的对象添加某些功能
BlockingCache里面的实现根据每个key加了一把锁,实现分段锁的逻辑,在使用redis的缓存的时候可以借鉴里面的思路
package org.apache.ibatis.cache.decorators;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* Simple blocking decorator
*
* Simple and inefficient version of EhCache's BlockingCache decorator.
* It sets a lock over a cache key when the element is not found in cache.
* This way, other threads will wait until this element is filled instead of hitting the database.
*
* 阻塞版本的缓存装饰器,保证只有一个线程到数据库去查找指定的key对应的数据
*
* @author Eduardo Macarron
*
*/
public class BlockingCache implements Cache {
//阻塞的超时时长
private long timeout;
//被装饰的底层对象,一般是PerpetualCache
private final Cache delegate;
//锁对象集,粒度到key值
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
Object value = delegate.getObject(key);
if (value != null) {//获取数据成功的,要释放锁
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
}
@Override
public void clear() {
delegate.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();//创建锁
ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
return previous == null ? lock : previous;
}
//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
private void acquireLock(Object key) {
//获得锁对象
Lock lock = getLockForKey(key);
if (timeout > 0) {//使用带超时时间的锁
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {//如果超时抛出异常
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {//使用不带超时时间的锁
lock.lock();
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
}
2. CacheKey解读
Mybatis中涉及到动态SQL的原因,所以缓存项的key不能仅仅通过一个String来表示,所以通过CacheKey来封装缓存的key值。CacheKey可以封装多个影响缓存项的因素,判断两个CacheKey是否相同是比较两个对象的hash值是否一致。
构成CacheKey的对象
- mappedStatemnt的id
- 指定查询结果集的范围(分页信息)
- 查询所使用的SQL语句
- 用户传递给SQL语句的实际参数值
重点解读方法:
update(Object obj)
equals(Object obj)
二级缓存入口:
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
一级缓存入口:
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
为什么缓存使用HashMap不会有线程安全问题?
二级缓存加上了Synchronize装饰器模式,一级缓存是sqlsession的,是线程安全的
3. 缓存脏读
使用一级缓存和二级缓存都会出现脏读现象。
1. 一级缓存出现脏读
当在同一事务下使用不同的SqlSession操作同一数据对象,一级缓存会出现脏读
因为一级缓存是SqlSession范围的
@Transactional
public void dirtyData(Serializable id) {
// 开启SqlSession1,使用dicMapper第一次查询,会走数据库
Dic dic1 = dicMapper.selectById(id);
System.out.println("第一次查询DIC:\t"+dic1);
// 开启另一个SqlSession2,使用dicMapper第一次查询,会走数据库
SqlSession sqlSession = sqlSessionFactory.openSession(true);
DicMapper mapper = sqlSession.getMapper(DicMapper.class);
Dic dic2 = mapper.selectById(id);
System.out.println("重新开启SqlSession查询DIC:\t"+dic2);
// SqlSession1-dicMapper做更新操作,SqlSession1-dicMapper的缓存会被清空
dic1.setName(dic1.getName()+"-edit");
dicMapper.updateById(dic1);
System.out.println("使用第一次的Mapper进行数据修改");
// SqlSession1-dicMapper查询,会重新走数据库,查询出更新后的值
Dic dic2_1 = dicMapper.selectById(id);
System.out.println("使用第一次的Mapper重新查询数据(此刻会查询到正确数据):\t"+dic2_1);
// SqlSession2-dicMapper查询,会走缓存,查询的是更新前的值
Dic dic2_2 = mapper.selectById(id);
System.out.println("使用开启SqlSession重新查询数据(此刻会出现脏读):\t"+dic2_2);
}
2. 二级缓存出现脏读
同一事务下使用不同的Mapper(namespace)操作同一数据对象,二级缓存会出现脏读
因为二级缓存是SqlSessionFactorhy范围的,可以跨SqlSession,有多个SqlSession
public void dirtyDataTwo(Serializable id) {
// 开启SqlSession1,使用dicMapper第一次查询,会走数据库
Dic dic = dicMapper.selectById(id);
System.out.println("dicMapper第一次查询:\t"+dic);
// 开启SqlSession1,使用dicMapper2【不同的Mapper(namespace)】第一次查询,会走数据库
System.out.println("dicMapper2第一次查询:\t"+dicMapper2.selectById(id));
// 使用SqlSession1-dicMapper做更新操作,会清空缓存
dic.setName("-tow-edit");
dicMapper.updateById(dic);
System.out.println("使用dicMapper进行更新操作");
// 使用SqlSession1-dicMapper查询,会走数据库,查询出更新后的值
Dic dic2 = dicMapper.selectById(id);
System.out.println("dicMapper第二次查询:\t"+dic2);
// 使用SqlSession1-dicMapper2查询,会缓存,查询出更新前的值
System.out.println("dicMapper2第二次查询:\t"+dicMapper2.selectById(id));
}
7. 反射模块
ObjectFactory:专门用来实例化对象的
反射的核心类
- ObjectFactory: MyBatis每次创建结果对象的新实例时,它都会使用对象工厂(ObjectFactory)去构建POJO
- ReflectorFactory:创建Reflector的工厂类,Reflector是MyBatis反射模块的基础,每个Reflector对象都对应一个类,在其中缓存了反射操作所需要的类元信息
- ObjectWrapper:对对象的包装,抽象了对象的属性信息,它定义了一系列查询对象属性信息的方法,以及更新属性的方法
- ObjectWrapperFactory:ObjectWrapper的工厂类,用于创建ObjectWrapper
MetaObject:封装了对象的元信息,包装了MyBatis中五个核心的反射类。也是提供给外部使用的反射工具类,可以利用它读取或修改对象的属性信息
8. Mybatis核心流程三大阶段
-
初始化阶段
读取XML配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作
-
代理阶段
封装IBatis的编程模型,使用mapper接口开发的初始化工作
-
数据读写阶段
通过SqlSession完成SQL的解析,参数映射,SQL的执行,结果的解析过程