【MyBatis】银行转账系统:MyBatis接口绑定方案及多参数传递、动态SQL、ThreadLocal线程容器、缓存

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/sinat_42483341/article/details/100522575

完整代码见github:GitHub地址-银行转账系统

Mybatis官方文档:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html

一个MyBatis练习的Demo,用于学习MyBatis接口绑定方案及多参数传递、动态SQL、ThreadLocal线程容器配合Filter使用获取SqlSession

运行效果(search.jsp页面):
在这里插入图片描述
项目结构:
在这里插入图片描述


准备:数据库数据

create table account(
	id int(10) primary key auto_increment,
	accno varchar(18) unique,
	pwd varchar(20),
	balance double,
	name varchar(20)
);

insert into account values(default,'10001','zhangsan',1000,'张三');
insert into account values(default,'10002','lisi',2000,'李四');
insert into account values(default,'10003','wangwu',3000,'王五');
insert into account values(default,'10004','laoda',4000,'老大');

create table log(
	id int(10) primary key auto_increment,
	accout varchar(18),
	accin varchar(18),
	money double,
	reason varchar(50)
);

一.MyBatis 接口绑定方案及多参数传递

  1. 作用:实现创建一个接口后,把 mapper.xml 由 mybatis 生成接口的实现类,通过调用接口对象,就可以获取 XXXmapper.xml 中编写的sql.
  2. 后面mybatis 和spring 整合时使用的是这个方案.
  3. 实现步骤:
    3.1 创建一个接口
    3.1.1 接口包名和接口名与 mapper.xml 中<mapper>namespace相同
    3.1.2 接口中方法名和 mapper.xml 标签的id 属性相同
    3.2 在 mybatis.xml 中使用<package>进行扫描接口和mapper.xml
  4. 代码实现步骤:
    4.1 在mybatis.xml 中<mappers>下使用<package>name=""中写XXXmapper.xml所在的包名+类名
    在这里插入图片描述
    mybatis.xml 完整示例代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="logImpl" value="LOG4J"/>
	</settings>
	
	<!-- 给类起别名 -->
	<typeAliases>
		<package name="cn.hanquan.pojo"/>
	</typeAliases>
	
	<environments default="default">
	 <environment id="default">
	 	<transactionManager type="JDBC"></transactionManager>
	 	<dataSource type="POOLED">
	 		<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
	 		<property name="url" value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"/>
	 		<property name="username" value="root"/>
			<property name="password" value="g67108864"/>
	 	</dataSource>
	 </environment>
	</environments>

	<mappers>
<!-- 		<mapper resource="cn/hanquan/mapper/AccountMapper.xml" /> -->
<!-- 		<mapper resource="cn/hanquan/mapper/LogMapper.xml" /> -->
		<package name="cn.hanquan.mapper"></package>
	</mappers>
</configuration>

4.2 在 cn.hanquan.mapper 下新建接口

  • 接口中的方法名称与对应xml文件中的id相同
  • 接口中的返回值类型与xml文件中的resultType相同
package cn.hanquan.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Param;
import cn.hanquan.pojo.Log;

public interface LogMapper {

	/**
	 * 插入一条转账记录
	 * @param log 被插入的记录
	 * @return 成功影响的数据库行数
	 */
	int insLog(String accOut, String accIn, double money, String reason);

	/**
	 * 分页查询数据
	 * @param map 表示参数的键值对
	 * @return
	 */
	List<Log> selByPage(int pageStart, int pageSize);
	
	/**
	 * 查询总记录条数
	 * @return 总记录条数
	 */
	int selCount();

	/**
	 * 动态sql语句
	 * @param accIn 收款账号
	 * @param accOut 付款账号
	 * @return
	 */
	List<Log> selDynamic(@Param("accIn") String accIn, @Param("accOut") String accOut);
	
	/**
	 * 修改转账记录
	 * @param log
	 */
	void updReason(Log log);
}

4.3 在cn.hanquan.mapper 新建一个LogMapper.xml

  • namespace=""必须和接口全限定路径(包名+类名)一致
  • id="" 值必须和接口中方法名相同
  • 如果接口中方法为多个参数,省略parameterType=""。因为parameterType=""只能写一个参数。或者使用map
    在这里插入图片描述
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace必须和接口全限定路径一致 -->
<mapper namespace="cn.hanquan.mapper.LogMapper">
	
	<!-- 插入log信息 -->
	<insert id="insLog">
		insert into log values(default,#{0},#{1},#{2},#{3})
	</insert>
	
	<!-- limit分页查询 -->	
	<select id="selByPage" resultType="Log">
		select * from log limit #{0}, #{1}
	</select>
	
	<!-- 查询总数 -->
	<select id="selCount" resultType="int">
		select count(*) from log
	</select>
	
	<!-- 动态sql查询 -->
	<select id="selDynamic" resultType="Log">
		select * from log
		<where>
			<!-- OGNL 表达式,直接写key对象的属性,用and链接 -->
			<if test="accIn!=null and accIn!=''">
				and accIn=#{accIn}
			</if>
			<if test="accOut!=null and accOut!=''">
				and accOut=#{accOut}
			</if>
		</where>
	</select>
	
	<!-- 修改转账原因 -->
	<!-- id=#{id},的目的是防止<set>中没有内容,mybatis不生成set关键字导致的sql语法错误 -->
	<update id="updReason" parameterType="Log">
		update log
		<set>
			id=#{id},
			<if test="accIn!=null and accIn!=''">
				accin=#{accIn},
			</if>
			<if test="accOut!=null and accOut!=''">
				accout=#{accOut},
			</if>
			<if test="money!=null and money!=''">
				money=#{money},
			</if>
			<if test="reason!=null and reason!=''">
				reason=#{reason},
			</if>
		</set>
		where id=#{id}
	</update>
</mapper>
  1. 多参数实现办法
    5.1 在接口中声明方法
    在这里插入图片描述
    5.2 在mapper.xml 中添加
    5.2.1 #{}中使用0,1,2param1,param2
    在这里插入图片描述
  2. 可以使用注解方式
    6.1 在接口中声明方法
    实际上,mybatis 把参数转换为 map 了,其中@Param("key") 参数内容就是 map 的 value
    用这种方式,注解外面的变量名称String accin可以被写成任意名称,比如String accin666
    在这里插入图片描述
    6.2 在mapper.xml 中添加
    6.2.1 #{} 里面写@Param("内容")参数中内容
    在这里插入图片描述

二.动态SQL

  1. 根据不同的条件需要执行不同的SQL 命令,称为动态SQL
  2. MyBatis 中动态 SQL 在 mapper.xml 中添加逻辑判断等.
  3. If 使用(来源:sxt)
	<select id="selByAccinAccout" resultType="log">
		select * from log where 1=1
		<!-- OGNL 表达式,直接写key 或对象的属性.不需要添加任何特字符号-->
		<if test="accin!=null and accin!=''">
			and accin=#{accin}
		</if>
		<if test="accout!=null and accout!=''">
			and accout=#{accout}
		</if>
	</select>
  1. <where>
    4.1 当编写 where 标签时,如果内容中第一个是and,去掉第一个and
    4.2 如果<where>中有内容,会生成 where 关键字,如果没有内容,不生成 where 关键字
    4.3 使用示例(来源:sxt)
    4.3.1 使用<where>,比直接使用<if>少写一句where 1=1
<select id="selByAccinAccout" resultType="log">
	select * from log
	<where>
		<if test="accin!=null and accin!=''">
			and accin=#{accin}
		</if>
		<if test="accout!=null and accout!=''">
			and accout=#{accout}
		</if>
	</where>
</select>
  1. <choose><when><otherwise>
    5.1 只要有一个成立,其他都不执行
    5.2 代码示例(来源:sxt)
    5.2.1 如果 accin 和 accout 都不是null,不是"",生成的 sql 中只有where accin=?
<select id="selByAccinAccout" resultType="log">
	select * from log
	<where>
		<choose>
			<when test="accin!=null and accin!=''">
				and accin=#{accin}
			</when>
			<when test="accout!=null and accout!=''">
				and accout=#{accout}
			</when>
		</choose>
	</where>
</select>
  1. <set>用在修改SQL 中set 从句
    6.1 作用:去掉最后一个逗号
    6.2 作用:如果<set>里面有内容,就生成 set 关键字,如果没有,就不生成 set 关键字
    6.3 示例(来源:本项目源码)
    6.3.1 id=#{id} 目的:防止<set>中没有内容,mybatis 不生成 set 关键字导致的SQL语法错误.
<!-- 修改转账原因 -->
	<!-- id=#{id},的目的是防止<set>中没有内容,mybatis不生成set关键字导致的sql语法错误 -->
	<update id="updReason" parameterType="Log">
		update log
		<set>
			id=#{id},
			<if test="accIn!=null and accIn!=''">
				accin=#{accIn},
			</if>
			<if test="accOut!=null and accOut!=''">
				accout=#{accOut},
			</if>
			<if test="money!=null and money!=''">
				money=#{money},
			</if>
			<if test="reason!=null and reason!=''">
				reason=#{reason},
			</if>
		</set>
		where id=#{id}
	</update>
  1. Trim
    7.1 prefix前面添加内容
    7.2 prefixOverrides 去掉前面内容
    7.3 suffix后面添加内容
    7.4 suffixOverrieds 去掉后面内容
    7.5 执行顺序:先去掉内容,后添加内容
    7.6 代码示例(来源:sxt)
<update id="upd" parameterType="log">
	update log
		<trim prefix="set" suffixOverrides=",">
			a=a,
		</trim>
	where id=100
</update>
  1. <bind>
    8.1 作用:给参数重新赋值
    8.2 场景:模糊查询、在原内容前或后添加内容(如$符号)
    8.3 示例(来源:sxt)
<select id="selByLog" parameterType="log" resultType="log">
	<bind name="accin" value="'%'+accin+'%'"/>
	#{money}
</select>
  1. <foreach>标签
    9.1 循环参数内容,在SQL语句内容的前后添加内容、添加分隔符功能
    9.2 适用场景:in 查询、批量新增( 但是 mybatis 中 foreach 效率比较低)
    9.2.1 如果希望批量新增,SQL 命令是insert into log VALUES (default,1,2,3),(default,2,3,4),(default,3,4,5)
    9.2.2 session.openSession()必须指定: 底层JDBC 的PreparedStatement.addBatch();
factory.openSession(ExecutorType.BATCH);

9.3 示例
9.3.1 collection=”” 要遍历的集合
9.3.2 item 迭代变量, #{迭代变量名}获取内容
9.3.3 open 循环后左侧添加的内容
9.3.4 close 循环后右侧添加的内容
9.3.5 separator 每次循环时,元素之间的分隔符

<select id="selIn" parameterType="list" resultType="log">
	select * from log where id in
		<foreach collection="list" item="abc" open="(" close=")" separator=",">
			#{abc}
		</foreach>
</select>
  1. <sql><include>
    10.1 某些SQL 片段如果希望复用,可以使用定义这个片段
<sql id="mysql">
	id,accin,accout,money
</sql>

10.2 在<select><delete><update><insert>中使用<include>引用

<select id="">
	select <include refid="mysql"></include> from log
</select>

三、ThreadLocal 线程容器

  1. 线程容器,给线程绑定一个Object 内容,后只要线程不变,可以随时取出。
  2. 如果改变线程,无法取出内容.
  3. ThreadLocal的使用:语法示例
	final ThreadLocal<String> threadLocal = new ThreadLocal<>();
	threadLocal.set("测试");
	new Thread(){
		public void run() {
		String result = threadLocal.get();
		System.out.println("结果:"+result);
		};
	}.start();
  1. 使用ThreadLocal优化service层代码:不必反复写以下22-24行
    在这里插入图片描述
    通过写一个MyBatisUtil.java(在里面保存ThreadLocal),同时新建一个FilterOpenSessionInView.java
  • 在里面每次收到请求之后都调用一下MyBatisUtil.getSession()获取session对象(类似单例模式)
  • 处理完请求之后再调用一下MyBatisUtil.closeSession()关闭session
  • 这样可以保证执行的session.commit()与启动过滤器时得到的sessionMyBatisUtil.getSession()是同一个session

实现如下:

  • OpenSessionInView.java(Filter过滤器)
package cn.hanquan.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

import org.apache.ibatis.session.SqlSession;

import cn.hanquan.util.MyBatisUtil;

@WebFilter("/*")
public class OpenSessionInView implements Filter {

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain)
			throws IOException, ServletException {
		SqlSession session = MyBatisUtil.getSession();
		System.out.println("过滤器:获取session");
		try {
			filterChain.doFilter(req, resp);
			session.commit();
		} catch (Exception e) {
			session.rollback();
			e.printStackTrace();
		}finally {
			MyBatisUtil.closeSession();
			System.out.println("过滤器:关闭session");
		}
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
	}
}

  • MyBatisUtil.java(用于创建ThreadLocal,并从中获取唯一的session对象,单例)
package cn.hanquan.util;

import java.io.InputStream;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class MyBatisUtil {
	/**
	 * factory实例化耗费性能,因此保证只有一个factory
	 */
	private static SqlSessionFactory factory;

	/**
	 * 线程容器,给线程绑定一个Object内容,只要线程不变,可以随时取出
	 */
	private static ThreadLocal<SqlSession> threadLocal = new ThreadLocal<>();

	/**
	 * 初始化一个factory
	 */
	static {
		try {
			InputStream is = Resources.getResourceAsStream("mybatis.xml");
			factory = new SqlSessionFactoryBuilder().build(is);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 获取一个session对象
	 * @return session对象
	 */
	public static SqlSession getSession() {
		SqlSession session = threadLocal.get();
		if (session == null) {
			threadLocal.set(factory.openSession());
		}
		return threadLocal.get();
	}

	/**
	 * 关闭session,并且删除ThreadLocal线程容器中保留的session内容
	 */
	public static void closeSession() {
		SqlSession session = threadLocal.get();
		if (session != null) {
			session.close();
		}
		threadLocal.set(null);
	}
}
  • LogServiceImpl.java(Service层代码的优化,不用每次都重新创建SqlSessionFactory
package cn.hanquan.service.impl;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import cn.hanquan.mapper.LogMapper;
import cn.hanquan.pojo.Log;
import cn.hanquan.pojo.PageInfo;
import cn.hanquan.service.LogService;
import cn.hanquan.util.MyBatisUtil;

public class LogServiceImpl implements LogService {

	@Override
	public PageInfo showPage(int pageSize, int pageNum) throws IOException {
//		InputStream is = Resources.getResourceAsStream("mybatis.xml");
//		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
//		SqlSession session = factory.openSession();
		SqlSession session = MyBatisUtil.getSession();
		LogMapper logMapper = session.getMapper(LogMapper.class);

		PageInfo pageInfo = new PageInfo();
		pageInfo.setPageNum(pageNum);
		pageInfo.setPageSize(pageSize);

		// 查询结果
		int pageStart = pageSize * (pageNum - 1);
		List<Log> list = logMapper.selByPage(pageStart, pageSize);
		pageInfo.setDataList(list);
		System.out.println("LogService中的list:" + list);

		// 总页数
		int count = logMapper.selCount();
		pageInfo.setTotal((count % pageSize == 0 ? count / pageSize : count / pageSize + 1));
		System.out.println("LogService中的count:" + count);
		
//		session.close();
		return pageInfo;
	}

	@Override
	public List<Log> search(String accIn, String accOut) throws IOException {
//		InputStream is = Resources.getResourceAsStream("mybatis.xml");
//		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
//		SqlSession session = factory.openSession();
		SqlSession session = MyBatisUtil.getSession();

		LogMapper logMapper = session.getMapper(LogMapper.class);
		List<Log> list = logMapper.selDynamic(accIn, accOut);
		System.out.println("LogService中的list:"+list);
		
//		session.close();
		return list;
	}

	@Override
	public void modifyLog(Log log) throws IOException {
//		InputStream is = Resources.getResourceAsStream("mybatis.xml");
//		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
//		SqlSession session = factory.openSession();
		SqlSession session = MyBatisUtil.getSession();
		
		//调用修改
		LogMapper logMapper = session.getMapper(LogMapper.class);
		logMapper.updReason(log);
		System.out.println("LogService中的log:"+log);
		
//		session.commit();
//		session.close();
	}
}

四、缓存

  1. 应用程序和数据库交互的过程是一个相对比较耗时的过程
  2. 缓存存在的意义:让应用程序减少对数据库的访问,提升程序运行效率
  3. MyBatis 中,默认 SqlSession 缓存开启
    3.1 同一个 SqlSession 对象调用同一个<select>时,只有第一次调用会访问数据库,第一次之后,把查询结果缓存到 SqlSession 缓存区(内存)中,以后再调用时,直接从内存中拿数据。
    3.2 缓存的是statement 对象 (简单记忆:必须是同一个<select>)
    3.2.1 在myabtis 中,一个<select>对应一个 statement 对象
    3.3 有效范围必须是同一个 SqlSession 对象
  4. 缓存流程
    4.1 步骤一: 先去缓存区中找是否存在statement
    4.2 步骤二:返回结果
    4.3 步骤三:如果没有缓存statement 对象,去数据库获取数据
    4.4 步骤四:数据库返回查询结果
    4.5 步骤五:把查询结果放到对应的缓存区中
    在这里插入图片描述
  5. SqlSessionFactory 缓存
    5.1 又叫:二级缓存
    5.2 有效范围:同一个factory 内哪个SqlSession 都可以获取
    5.3 什么时候使用二级缓存:
    5.3.1 当数据频繁被使用,很少被修改
    5.4 使用二级缓存步骤
    5.4.1 在 mapper.xml 中添加
    5.4.2 如果不写readOnly=”true”,需要把实体类序列化 implements serializable
	<cache readOnly="true"></cache>

5.5 当SqlSession 对象close()时,或commit()时,会把 SqlSession 缓存的数据刷(flush)到 SqlSessionFactory 缓存区中

猜你喜欢

转载自blog.csdn.net/sinat_42483341/article/details/100522575