mybatis_And_package

@Mybatis_day01

框架

概念

框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;
另一种定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。
简而言之,框架其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。

框架解决什么问题

框架要解决的最重要的一个问题是技术整合的问题,在 J2EE 的 框架中,有着各种各样的技术,不同的软件企业需要从 J2EE 中选择不同的技术,这就使得软件企业最终的应用依赖于这些技术,技术自身的复杂性和技术的风险性将会直接对应用造成冲击。而应用是软件企业的核心,是竞争力的关键所在,因此应该将应用自身的设计和具体的实现技术解耦。这样,软件企业的研发将集中在应用的设计上,而不是具体的技术实现,技术实现是应用的底层支撑,它不应该直接对应用产生影响框架一般处在低层应用平台(如 J2EE)和高层业务逻辑之间的中间层。

框架的重要性

框架的重要性在于它实现了部分功能,并且能够很好的将低层应用平台和高层业务逻辑进行了缓和。为了实现软件工程中的“高内聚、低耦合”。把问题划分开来各个解决,易于控制,易于延展,易于分配资源。我们常见的MVC 软件设计思想就是很好的分层思想。
通过分层更好的实现了各个部分的职责,在每一层将再细化出不同的框架,分别解决各层关注的问题。

Mybatis概述

jdbc 问题

  • 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
  • Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java代码。
  • 使用 preparedStatement 向占有位符号传参数存在硬编码,因为 sql 语句的 where 条件不一定,可能多也可能少,修改
    sql 还要修改代码,系统不易维护。
  • 对结果集解析存在硬编码(查询列名),sql 变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成 pojo 对象解析比较方便

什么是mybatis

MyBatis 是一款优秀的持久层框架,它支持 SQL、存储过程以及高级映射。
如 JDBC中 我们需要有繁琐的流程,和设置参数,并且获取结果集的时候也需要循环遍历,并且手动封装到实体类对象中。
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。它内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。
MyBatis 可以使用 简单的 XML 或注解来配置和映射 接口和 Java 的 实体类。

MyBatis 有两种用法,一个是注解,一个是XML,各有千秋
注解 : 使用注解的话,方便一些,不需要有XML配置文件,可能看上去会好看一些,但是需要代码和SQL在一起
XML : 把SQL语句放到XML文件中,java代码中会感觉干净一些,并且,使用XML形式 写一些复杂的SQL语句会比较方便,也能统一管理,有其他同事接手工作的时候,或者其他同事优化数据库查询的时候,位置好找,修改便捷,不容易出错
所以一般常用的就是XML,可以做到sql分离
有时候也是混用,简单的就用注解,复杂的就用XML

使用场景 :

MyBatis就是负责操作数据库的,所以当我们需要操作数据库的时候,就可以使用mybatis。

MyBatis与Hibernate的区别

MyBatis :
1 入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。
2 可以进行更为细致的SQL优化,可以减少查询字段。
3 缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。
4 二级缓存机制不佳
整体 : 小巧、方便、高效、简单、直接(SQL操作)、半自动

Hibernate :
1 功能强大,数据库无关性好,对象关系(O/R)映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。
2 有更好的二级缓存机制,可以使用第三方缓存
3 缺点就是学习门槛不低,要精通门槛更高,而且怎么设计对象关系(O/R)映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行

形象举例 :
mybatis:机械工具,使用方便,拿来就用,但工作还是要自己来作,不过工具是活的,怎么使用,由我决定。
hibernate:智能机器人,但研发它(学习、熟练度)的成本很高,工作都可以拜托给他了,但仅限于它能做的事。

官方文档:https://mybatis.org/mybatis-3/zh/index.html
mybatis plus

项目集成

引入依赖

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

创建配置文件

在resource目录下创建mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <!-- 链接数据库只需要一个属性来配置文件,就会自动解析 -->
  <properties resource="jdbc.properties" />
  <!--  设置别名,然后就可以使用别名来代替前面的路径 -->
	<typeAliases>
		<typeAlias type="com.tledu.zrz.model.User" alias="User"/>
		<typeAlias type="com.tledu.zrz.model.Address" alias="Address"/>
	</typeAliases>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <!-- 映射文件 -->
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

创建jdbc.properties配置数据库

准备一些测试数据

create table t_user(
	id int(11) primary key auto_increment,
	username varchar(100),
	password varchar(100),
	nickname varchar(100),
	`type` int (2)
);
create table t_address(
	id int(11) primary key auto_increment,
	addr varchar(255),
	phone varchar(100),
	postcode varchar(100),
	user_id int(11),
	CONSTRAINT foreign key (user_id) references t_user(id)
);
insert into t_user(username,password,nickname,type) values('admin','123','超级管理员',1);
insert into t_address(addr,phone,postcode,user_id) values ('123','123','123',1);

创建对应实体

创建数据库操作的xml文件

这里以user为例,Address作为练习,在resource目录下创建一个User.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  <!-- 
  	namespace 是用于调用的时候的映射
   -->
<mapper namespace="User">
  <!-- 
    id : 表示调用的SQL的名字,相当于方法名
    parameterType : 表示传入参数的类型,写类全名,但是由于设置的别名,所以可以写User
    resultType : 结果集类型 
   -->
	<insert id="add" parameterType="User" >
	<!-- 这里的#username 就等于是用 ? 的方式,等方法调用的时候,会传递一个参数,就会自动映射到username的属性上 -->
		insert into t_user (username,password,nickname,type) values (#{username},#{password},#{nickname},#{type})
	</insert>
</mapper>

修改mybatis-config.xml中的映射关系

<mappers>
	<mapper resource="mapper/User.xml"/>
</mappers>

如果想把xml文件放到java目录中,则需要在maven的build中配置资源路径

<project>
  ...
  <build>
    ...
  	<resources>
            <resource>
                <!-- directory:指定资源文件的位置 -->
                <directory>src/main/java</directory>
                <includes>
                    <!-- “**” 表示任意级目录    “*”表示任意任意文件 -->
                    <!-- mvn resources:resources :对资源做出处理,先于compile阶段  -->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <!--  filtering:开启过滤,用指定的参数替换directory下的文件中的参数(eg. ${name}) -->
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
     </resources>
    ...
  </build>
  ...
</project>

执行该插入操作

编写一个servlet执行该操作

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String resource = "mybatis-config.xml";
        // 读取总配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 获取session工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取session
        SqlSession session = sqlSessionFactory.openSession();
        // 调用xml中的sql
        User user = new User();
        user.setUsername("1234");
        user.setPassword("1234");
        user.setNickname("1234");
        // 调用
        session.insert("User.add", user);
        // 提交
        session.commit();
    }
}

总结:到此为止,我们就是用mybatis的一个最基础的用法

日志日志打印

由于mybatis对sql进行了封装,这个时候我们在项目运行过程中,如果出现问题了就需要进行日志的打印,在这里可以通过log4j这个工具打印对应sql日志,帮助我们进行错误的排查。

引入日志的依赖

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

设置mybatis通过log4j打印日志

   <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>

在resource目录下添加log4j.properties配置文件

### Log4j配置 ###
#定义log4j的输出级别和输出目的地(目的地可以自定义名称,和后面的对应)
#[ level ] , appenderName1 , appenderName2
log4j.rootLogger=DEBUG,console,file
#-----------------------------------#
#1 定义日志输出目的地为控制台
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
####可以灵活地指定日志输出格式,下面一行是指定具体的格式 ###
#%c: 输出日志信息所属的类目,通常就是所在类的全名
#%m: 输出代码中指定的消息,产生的日志具体信息
#%n: 输出一个回车换行符,Windows平台为"/r/n",Unix平台为"/n"输出日志信息换行
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
#-----------------------------------#
#2 文件大小到达指定尺寸的时候产生一个新的文件
log4j.appender.file = org.apache.log4j.RollingFileAppender
#日志文件输出目录
log4j.appender.file.File=log/info.log
#定义文件最大大小
log4j.appender.file.MaxFileSize=10mb
###输出日志信息###
#最低级别
log4j.appender.file.Threshold=ERROR
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
#-----------------------------------#
#3 druid
log4j.logger.druid.sql=INFO
log4j.logger.druid.sql.DataSource=info
log4j.logger.druid.sql.Connection=info
log4j.logger.druid.sql.Statement=info
log4j.logger.druid.sql.ResultSet=info
#4 mybatis 显示SQL语句部分
log4j.logger.org.mybatis=DEBUG
#log4j.logger.cn.tibet.cas.dao=DEBUG
#log4j.logger.org.mybatis.common.jdbc.SimpleDataSource=DEBUG
#log4j.logger.org.mybatis.common.jdbc.ScriptRunner=DEBUG
#log4j.logger.org.mybatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG
#log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

之后控制台就可以打印出sql语句了

单元测试

我们现在在测试mybatis的时候,每次都需要启动tomcat服务,每次还需要开发一个接口,就比较麻烦,这个时候我们可以基于JUnit进行单元测试,帮我们快速测试代码。

添加依赖

这里在我们创建项目的时候maven已经为我们增加了junit的配置,所以我们这里就不用在重复添加了。

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

添加测试用例

import com.tledu.erp.model.User;
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 org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class UserDao {
    @Test
    public void insert() throws IOException {
        String resource = "mybatis-config.xml";
        // 读取总配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 获取session工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 获取session
        SqlSession session = sqlSessionFactory.openSession();
        // 调用xml中的sql
        User user = new User();
        user.setUsername("1234");
        user.setPassword("1234");
        user.setNickname("1234");
        // 调用
        session.insert("User.add", user);
        // 提交
        session.commit();
    }
}

运行测试样例

直接右键即可运行测试样例

可以执行mvn clean test 运行所有的测试样例

可以执行mvn clean test 运行所有的测试样例

如果没有自动执行对应的@Test的方法,则可以添加插件
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
        </includes>
    </configuration>
</plugin>

可以通过assertEquals进行断言操作

// 断定res = 1 如果res不等于1 则代表测试失败
assertEquals(res, 1);

Mapper三种形式

常用的三种mapper

  1. sqlSession执行对应语句,就是我们上面测试的那种
  2. 使用注解(简单sql)
  3. 利用接口代理(常用)

sqlSession执行对应语句

上面的测试就是第一种

使用注解

  • 注解方式不需要User.xml
  • 再对应的接口上添加对应的注解语句
  • mybatis-config.xml中添加映射配置
    在dao层接口上直接添加sql注解
public interface IUserMapper {
    @Select("select * from t_user where id = #{id}")
    User getById(int id);
}

配置mybatis

<mappers>
    <!--配置到对应的包-->
 		<package name="com.tledu.erp.mapper"/>
 </mappers>

运行测试样例

// 调用xml中的sql
        User user = new User();
        user.setUsername("1234");
        user.setPassword("1234");
        user.setNickname("1234");
        User user1 = session.getMapper(IUserMapper.class).getById(1);

对于一些简单sql可以采用这种方式

利用接口代理(常用)

  • 需要xml
  • 接口中不需要添加注解
  • mybatis-config.xml中添加映射配置

创建User.xml


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace 是用于调用的时候的映射,对应绑定的接口类
 -->
<mapper namespace="com.tledu.erp.mapper.IUserMapper">
    <insert id="add" parameterType="User" >
        <!-- 这里的#username 就等于是用 ? 的方式,等方法调用的时候,会传递一个参数,就会自动映射到username的属性上 -->
        insert into t_user (username,password,nickname) values (#{username},#{password},#{nickname})
    </insert>

    <select id="getList" resultType="User">
        select * from t_user
    </select>

    <select id="getById" parameterType="int" resultType="User">
        select * from t_user where id = #{id}
    </select>
</mapper>

在mubatis-config.xml中配置

<mapper resource="com/tledu/erp/mapper/User.xml"/>

工具类

我们每次都有很多重复的代码要加,mybatis-config.xml在整个生命周期只需要加载一次就行。SqlSessionFactory也只需要创建一个所以 我们把这些封装成方法

public class MyBatisUtil {
	private MyBatisUtil() {

	}
	private static SqlSessionFactory sessionFactory = null;
	static {
		String resource = "mybatis-config.xml";
		// 读取总配置文件
		InputStream inputStream = null;
		try {
			inputStream = Resources.getResourceAsStream(resource);
			// 获取session工厂
			sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public static SqlSession getSession() {
		return sessionFactory.openSession();
	}

	public static void closeSession(SqlSession session) {
		if (session != null) {
			session.close();
		}
	}
}

有了工具类调用就方便了

 SqlSession session = MyBatisUtil.getSession();
        User user = new User();
        user.setUsername("test");
        user.setPassword("test");
        user.setNickname("平民");
        session.getMapper(IUserMapper.class).add(user);
        session.commit();
        MyBatisUtil.closeSession(session);

实现CRUD的操作

@Mybatis_day02

Mybatis_2

#和$的区别

#{}表示一个占位符号
通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,
#{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。
可以自动对值添加 ’ ’ 单引号
表 示 拼 接 s q l 串 通 过 {}表示拼接 sql 串 通过 sql{}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换,
可 以 接 收 简 单 类 型 值 或 p o j o 属 性 值 , 如 果 p a r a m e t e r T y p e 传 输 单 个 简 单 类 型 值 , {}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值, pojoparameterType{}括号中只能是 value。
比如order by id 这种的,以id排序 那么这个id 是没有单引号的,就是简单的SQL拼接,所以我们应该使用${} 而不是#{}

当我们设置多个参数的时候我们一般通过注解的方式解决问题
// 接口方法
 int updateNickname( @Param("id") int id,@Param("nickname")String nickname);
    <update id="updateNickname">
        update t_user set nickname = #{nickname} where id = #{id}
    </update>

当出现这个错误的时候
There is no getter for property named ‘xxx’ in ‘class java.lang.String’
意思就是从String类型中获取不到xxx属性
这时候也需要通过@Param的注解来解决这个问题

一般我们获取变量值的时候使用#{}来读取属性,如果需要进行sql拼接的时候可以使用 , 使 用 {},使用 使{}的时候要注意防止sql注入。

paramerterType 和 resultType

paramerterType

SQL 语句传参,使用标签的 parameterType 属性来设定。该属性的取值可以是基本类型,引用类型(例如:String 类型),也可以是实体类类型(POJO 类)。
基本类型和String我们可以直接写类型名称,也可以使用包名.类名的方式。在这里,之所以我们可以直接写类名的原因就是因为这些这些常用类型,mybatis已经帮我们配置好了别名.
在这里插入图片描述

resultType

resultType 属性可以指定结果集的类型,它支持基本类型和实体类类型。
需要注意的是,它和 parameterType 一样,如果注册过类型别名的,可以直接使用别名。没有注册过的必须使用全限定类名。同时,当是实体类名称是,还有一个要求,实体类中的属性名称必须和查询语句中的列名保持一致,否则无法实现封装。

resultMap

resultMap,我们可以指定查询结果字段和实体属性字段的映射关系。

<resultMap id="userResult" type="User">
    <id column="id" property="id" />
    <result property="nickname" column="nickname" />
    <result property="schoolName" column="school_name" />
</resultMap>

mybatis-config.xml 配置文件

属性配置的两种方式

直接配置属性
<properties> 
  <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/> 
  <property name="jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
  <property name="jdbc.username" value="root"/> 
  <property name="jdbc.password" value="root"/> 
</properties>
读取配置文件

创建db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/erp16?useUnicode=true&characterEncoding=UTF-8
username=root
password=root

配置文件读取

<properties resource="db.properties" />
typeAliases属性
<typeAliases> 
  <!-- 单个别名定义 --> 
  <typeAlias alias="user" type="com.tledu.zrz.pojo.User"/> 
  <!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) --> 
  <package name="com.tledu.zrz.pojo"/> 
  <package name="其它包"/> 
</typeAliases>
mapper属性

Mappers是我们所说的映射器,用于通过mybatis配置文件去找到对应的mapper文件,关于这个属性有三种用法。

resource

使用相对于类路径的资源如:<mapper resource="com/tledu/zrz/dao/IUserDao.xml" />

class

使用 mapper 接口类路径
如:<mapper class="com.tledu.zrz.dao.UserDao"/>
注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。

package

注册指定包下的所有 mapper 接口
如:<package name="com.tledu.zrz.mapper"/>
注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中,并且这里如果希望能够扫描到包下面的xml文件的话,需要在maven中进行配置。

动态sql

if

<select id="list" parameterType="User" resultMap="userResult">
        select  * from t_user where 1=1
        <if test="username != null and username != ''">
            and username = #{username}
        </if>
        <if test="nickname != null and nickname != ''">
            and nickname like concat('%',#{nickname},'%')
        </if>
</select>

choose、when、otherwise

类似于Java中的switch case default

<select id="list" parameterType="User" resultMap="userResult">
        select * from t_user where 1=1
        <choose>
            <when test="id != null">
                and id = #{id}
            </when>
            <when test="username != null and username != ''">
                and username = #{username}
            </when>
            <otherwise>
                and nickname = #{nickname}
            </otherwise>
        </choose>
    </select>

where

在我们where条件不确定的时候,我们每次都需要加上一个1=1才能保证后面的拼接不会出现错误,使用where标签后,我们就不需要考虑拼接的问题了,直接在里面添加条件即可。通过where标签也可以让代码更具有语义化,方便维护代码

 <select id="list" parameterType="User" resultMap="userResult">
        select * from t_user where 
        <where>
            <if test="username != null and username != ''">
                and username = #{username}
            </if>
            <if test="nickname != null and nickname != ''">
                and nickname like concat('%',#{nickname},'%')
            </if>
        </where>
    </select>

set

在进行更新操作的时候,也可以用set标签添加更新条件,同样我们也就不需要在进行字符串拼接了,set标签会帮我们进行拼接。

    <update id="updateNickname">
        update t_user
        <set>
            <if test="nickname != null and nickname != ''">
                nickname = #{nickname},
            </if>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
        </set>
        where id = #{id}
    </update>

foreach

我们在开发项目中难免需要进行批量操作,这个时候就可以使用foreach进行循环添加。
例如批量插入

    <insert id="batchInsert">
        insert into t_user (username, password, nickname) VALUES
        <foreach collection="list" index="idx" item="item" separator=",">
            (#{item.username},#{item.password},#{item.nickname})
        </foreach>
    </insert>

in 查询

<select id="list" parameterType="User" resultMap="userResult">
        select * from t_user
        <where>
            <if test="user.username != null and user.username != ''">
                and username = #{user.username}
            </if>
            <if test="user.nickname != null and user.nickname != ''">
                and nickname like concat('%',#{user.nickname},'%')
            </if>
            and id in
            <foreach collection="idList" item="item" separator="," open="(" close=")">
                #{item}
            </foreach>
        </where>
    </select>

属性说明

  • List item
  • collection 需要遍历的列表
  • item 每一项的形参名
  • index 每一项索引名
  • separtor 分隔符
  • open 开始符号
  • close 关闭符号

联查

在项目中,某些实体类之间肯定有关联关系,比如一对一,一对多等,在mybatis 中可以通过association和collection,来处理这些关联关系。

1对1

在实现1对1映射的时候,可以通过association属性进行设置。在这里有三种方式
在地址表中,每个地址对应有一个创建用户,每次查询地址的时候希望查询到创建用户的内容

使用select
 <resultMap id="address" type="Address">
        <id column="id" property="id" />
        <association property="user" column="user_id" javaType="User" select="com.tledu.erp.dao.IUser2Dao.selectById"/>
</resultMap>

property配置了实体类对应的属性
column配置了关联字段
select对应了IUser2Dao中的查询语句
在执行sql的过程中就会同时调用在IUserDao中定义的sql进行联查。
这种方式会执行两次sql语句,效率相对较低,同时还需要先在IUserDao中进行定义后才能使用,比较麻烦

直接进行联查,在association中配置映射字段

这里可以直接写联查,需要转换的字段可以在association中进行配置。

    <resultMap id="address" type="Address" autoMapping="true">
        <id column="id" property="id" />
        <association property="user" column="user_id" javaType="User" >
          	<id column="user_id" property="id" />
            <result column="school_name" property="schoolName" />
        </association>
    </resultMap>

    <select id="selectOne" resultMap="address">
        select * from t_address left join t_user tu on tu.id = t_address.user_id where t_address.id = #{id}
    </select>

autoType代表自动封装,如果不填写,则需要添加所有的对应关系。
这种方式的问题是,当association需要被多次引用的时候,就需要进行多次重复的配置,所以我们还有第三种方式,引用resultMap。

嵌套的resultType
<resultMap id="addressMap" type="Address" autoMapping="true">
        <id column="id" property="id"/>
        <association property="user" column="user_id" resultMap="userMap">
        </association>
    </resultMap>

    <resultMap id="userMap" type="User" autoMapping="true">
        <id column="user_id" property="id" />
        <result column="school_name" property="schoolName"/>
    </resultMap>

    <select id="selectOne" resultMap="addressMap">
        select t_address.id,
               addr,
               phone,
               postcode,
               user_id,
               username,
               password,
               nickname,
               age,
               sex,
               school_name
        from t_address
                 left join t_user tu on tu.id = t_address.user_id
        where t_address.id = #{id}
    </select>
  • 通过这种方式,userMap就可以被多次引用了。
  • 通过别名保证查询的每一个元素是唯一的,以防止出现错乱的情况

mybatis官网提醒,需要设置id提高查询性能

1对多

对于address来说,一个地址对应一个创建用户,但是对于User来说,一个用户可能对应创建了多条地址信息,这种情况,在mybatis中就可以通过collections解决。
同样也是有三种方法

使用select

这里的用法和1对1的时候一致
在AddressDao中添加

    <select id="selectByUserId" resultType="Address">
      select * from t_address where user_id = #{userId}
    </select>

在UserDao中添加

    <resultMap id="userResult" type="User" autoMapping="true">
        <id column="id" property="id"/>
        <result property="nickname" column="nickname"/>
        <result property="schoolName" column="school_name"/>
        <collection property="addressList" column="id" autoMapping="true" select="com.tledu.erp.dao.IAddressDao.selectByUserId" >
        </collection>
    </resultMap>
    
    <select id="selectById" parameterType="int" resultMap="userResult">
        select *
        from t_user
        where id = #{id}
    </select>
直接进行联查,在collection中配置映射字段

这里可以直接写联查,需要转换的字段可以在collection中进行配置。

 <resultMap id="userResult" type="User" autoMapping="true">
        <id column="id" property="id"/>
        <result property="nickname" column="nickname"/>
        <result property="schoolName" column="school_name"/>
        <collection property="addressList" column="phone" ofType="Address" autoMapping="true">
            <id column="address_id" property="id" />
        </collection>
    </resultMap>


    <select id="selectById" parameterType="int" resultMap="userResult">
        select tu.id,
               username,
               password,
               nickname,
               age,
               sex,
               school_name,
               ta.id as address_id,
               addr,
               phone,
               postcode,
               user_id
        from t_user tu
                 left join t_address ta on tu.id = ta.user_id
        where tu.id = #{id}
    </select>

autoMapping代表自动封装,如果不填写,则需要添加所有的对应关系。
这里需要配置ofType来指定返回值类型
这种方式的问题是,当collection需要被多次引用的时候,就需要进行多次重复的配置,所以我们还有第三种方式,引用resultMap。

嵌套的resultType
 <resultMap id="userResult" type="User" autoMapping="true">
        <!--        <id column="id" property="id"/>-->
        <result property="nickname" column="nickname"/>
        <result property="schoolName" column="school_name"/>
        <collection property="addressList" column="phone" ofType="Address" resultMap="addressResultMap" autoMapping="true">
        </collection>
    </resultMap>

    <resultMap id="addressResultMap" type="Address" autoMapping="true">
        <id column="address_id" property="id" />
    </resultMap>


    <select id="selectById" parameterType="int" resultMap="userResult">
        select tu.id,
               username,
               password,
               nickname,
               age,
               sex,
               school_name,
               ta.id as address_id,
               addr,
               phone,
               postcode,
               user_id
        from t_user tu
                 left join t_address ta on tu.id = ta.user_id
        where tu.id = #{id}
    </select>

通过这种方式,userMap就可以被多次引用了。

@Mybatis_day03

mybatis中的连接池

什么是连接池

数据库连接是一项有限的昂贵资源,一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的性能低下。
数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由应用程序动态地对池中的连接进行申请、使用和释放。
对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。

总结:

  • 连接池是面向数据库连接的
  • 连接池是为了优化数据库连接资源

Mybatis中的连接池

在 Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。在 Mybatis 的配置文件中,通过<dataSource type="pooled”>来实 现 Mybatis 中连接池的配置。
在这里插入图片描述
在mybatis中数据库连接池可以分为以下三类

  • UNPOOLED 不使用连接池的数据源 POOLED
  • 使用连接池的数据源 JNDI
  • 使用JNDI实现的数据库连接池

UNPOOLED连接过程分析

UNPOOLED 不使用连接池的数据源,当 dateSource 的type属性被配置成了UNPOOLED,MyBatis 首先会实例化一个UnpooledDataSourceFactory工厂实例,然后通过.getDataSource() 方法返回一个UnpooledDataSource 实例对象引用,我们假定为dataSource。
使用 UnpooledDataSource 的 getConnection() ,每调用一次就会产生一个新的 Connection 实例对象。UnPooledDataSource 的 getConnection() 方法实现如下:

public class UnpooledDataSource implements DataSource {
    private ClassLoader driverClassLoader;
    private Properties driverProperties;
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
    private String driver;
    private String url;
    private String username;
    private String password;
    private Boolean autoCommit;
    private Integer defaultTransactionIsolationLevel;

    public UnpooledDataSource() {
    }

    public UnpooledDataSource(String driver, String url, String username, String password){
        this.driver = driver;
        this.url = url;
        this.username = username;
        this.password = password;
    }

    public Connection getConnection() throws SQLException {
        return this.doGetConnection(this.username, this.password);
    }

    private Connection doGetConnection(String username, String password) throws SQLException {
        Properties props = new Properties();
        if(this.driverProperties != null) {
            props.putAll(this.driverProperties);
        }

        if(username != null) {
            props.setProperty("user", username);
        }

        if(password != null) {
            props.setProperty("password", password);
        }

        return this.doGetConnection(props);
    }

    private Connection doGetConnection(Properties properties) throws SQLException {
        this.initializeDriver();
        Connection connection = DriverManager.getConnection(this.url, properties);
        this.configureConnection(connection);
        return connection;
    }
}

POOLED 数据源 连接池

PooledDataSource: 将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护。 MyBatis将连接池中的PooledConnection分为两种状态: 空闲状态(idle)和活动状态(active),这两种状态的PooledConnection对象分别被存储到PoolState容器内的idleConnections和activeConnections两个List集合中:
idleConnections:
空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从此集合中PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。
activeConnections:
活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,如果没有,则看此集合是否已满,如果未满,PooledDataSource会创建出一个PooledConnection,添加到此集合中,并返回
现在让我们看一下popConnection()方法到底做了什么:

  • 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。
  • 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步;
  • 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。
  • 线程等待,循环2步
/*
 * 传递一个用户名和密码,从连接池中返回可用的PooledConnection
 */
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.size() > 0)
            {
                // 连接池中有空闲连接,取出第一个
                conn = state.idleConnections.remove(0);
                if (log.isDebugEnabled())
                {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
            }
            else
            {
                // 连接池中没有空闲连接,则取当前正在使用的连接数小于最大限定值,
                if (state.activeConnections.size() < poolMaximumActiveConnections)
                {
                    // 创建一个新的connection对象
                    conn = new PooledConnection(dataSource.getConnection(), this);
                    @SuppressWarnings("unused")
                    //used in logging, if enabled
                    Connection realConn = conn.getRealConnection();
                    if (log.isDebugEnabled())
                    {
                        log.debug("Created connection " + conn.getRealHashCode() + ".");
                    }
                }
                else
                {
                    // Cannot create new connection 当活动连接池已满,不能创建时,取出活动连接池的第一个,即最先进入连接池的PooledConnection对象
                    // 计算它的校验时间,如果校验时间大于连接池规定的最大校验时间,则认为它已经过期了,利用这个PoolConnection内部的realConnection重新生成一个PooledConnection
                    //
                    PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    if (longestCheckoutTime > poolMaximumCheckoutTime)
                    {
                        // Can claim overdue connection
                        state.claimedOverdueConnectionCount++;
                        state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                        state.accumulatedCheckoutTime += longestCheckoutTime;
                        state.activeConnections.remove(oldestActiveConnection);
                        if (!oldestActiveConnection.getRealConnection().getAutoCommit())
                        {
                            oldestActiveConnection.getRealConnection().rollback();
                        }
                        conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                        oldestActiveConnection.invalidate();
                        if (log.isDebugEnabled())
                        {
                            log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                        }
                    }
                    else
                    {

                        //如果不能释放,则必须等待有
                        // Must wait
                        try
                        {
                            if (!countedWait)
                            {
                                state.hadToWaitCount++;
                                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;
                        }
                    }
                }
            }

            //如果获取PooledConnection成功,则更新其信息

            if (conn != null)
            {
                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++;
                    localBadConnectionCount++;
                    conn = null;
                    if (localBadConnectionCount > (poolMaximumIdleConnections + 3))
                    {
                        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;
}

当我们的程序中使用完Connection对象时,如果不使用数据库连接池,我们一般会调用 connection.close() 方法,关闭connection连接,释放资源
调用过close()方法的Connection对象所持有的资源会被全部释放掉,Connection对象也就不能再使用。那么,如果我们使用了连接池,我们在用完了Connection对象时,需要将它放在连接池中,该怎样做呢?
可能大家第一个在脑海里闪现出来的想法就是:我在应该调用con.close()方法的时候,不调用close()方法,将其换成将Connection对象放到连接池容器中的代码!
怎样实现Connection对象调用了close()方法,而实际是将其添加到连接池中
这是要使用代理模式,为真正的Connection对象创建一个代理对象,代理对象所有的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到连接池中。
MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库连接java.sql.Connection实例对象的包裹器。
PooledConnection对象内持有一个真正的数据库连接java.sql.Connection实例对象和一个java.sql.Connection的代理:

class PooledConnection implements InvocationHandler {
    private static final String CLOSE = "close";
    private static final Class<?>[] IFACES = new Class[]{Connection.class};
    private int hashCode = 0;
    private PooledDataSource dataSource;
    private Connection realConnection;
    private Connection proxyConnection;
    private long checkoutTimestamp;
    private long createdTimestamp;
    private long lastUsedTimestamp;
    private int connectionTypeCode;
    private boolean valid;

    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);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        //当close时候,会回收 connection , 不会真正的close
        if("close".hashCode() == methodName.hashCode() && "close".equals(methodName)) {
            this.dataSource.pushConnection(this);
            return null;
        } else {
            try {
                if(!Object.class.equals(method.getDeclaringClass())) {
                    this.checkConnection();
                }

                return method.invoke(this.realConnection, args);
            } catch (Throwable var6) {
                throw ExceptionUtil.unwrapThrowable(var6);
            }
        }
    }
}

总结

通过分析,大家应该对数据库的连接池技术有个大概的了解,需要知道mybatis中数据库连接池的大致实现流程,也对我们之后的程序开发很有帮助。同时,大家也要养成看源码的习惯,通过对源码进行分析,也可以提升自己的编程功底。

mybatis中的事务和隔离级别

jdbc中的事务

在 JDBC 中我们可以通过手动方式将事务的提交改为手动方式,通过 setAutoCommit()方法就可以调整。 通过 JDK 文档,我们找到该方法如下:
在这里插入图片描述
那么我们的 Mybatis 框架因为是对 JDBC 的封装,所以 Mybatis 框架的事务控制方式,本身也是用 JDBC的 setAutoCommit()方法来设置事务提交方式的。

mybatis中的事务提交方式

Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。 我们运行之前所写的代码:

   //用于在测试方法执行之前执行
    @BeforeEach
    public void init()throws Exception{
        // 设置为自动提交
        sqlSession = MybatisUtils.openSession(true);
        //获取dao的代理对象
        userMapper = sqlSession.getMapper(IUserMapper.class);
    }

    // 在测试结束之后执行
    @AfterEach
    public void destroy()throws Exception{
        //提交事务
		//sqlSession.commit();
        //释放资源
        sqlSession.close();
    }


    @Test
    public void testCommit(){
        User condition = new User();
        condition.setId(1);
        condition.setNickname("尚云科技1112");
        int i = userMapper.update(condition);
        assertEquals(i,1);
    }

@BeforeEach: 在每个方法之前加一下些操作
@AfterEach: 在每个方法之后加一些操作
这里我们设置了自动提交事务之后,就不需要在进行commit操作了

事务的回滚

对于我们开发过程,也会遇到报错的情况,这个时候为了保证数据的一致性我们就需要进行事务的回滚,比如我们有一个操作需要同时更新用户表和地址表,这个时候更新用户表的时候成功了,但是更新地址表的时候出现了一个特别长的字段,导致更新失败,这个时候,我们需要将数据进行回滚

 //用于在测试方法执行之前执行
    @BeforeEach
    public void init()throws Exception{
        // 设置为自动提交
        sqlSession = MybatisUtils.openSession();
        //获取dao的代理对象
        userMapper = sqlSession.getMapper(IUserMapper.class);
    }

    // 在测试结束之后执行
    @AfterEach
    public void destroy()throws Exception{
        //提交事务
        sqlSession.commit();
        //释放资源
        sqlSession.close();
    }

    @Test
    public void testRollback(){
        try{
            User condition = new User();
            condition.setId(1);
            condition.setNickname("尚云科技111299");
            int i = userMapper.update(condition);
            assertEquals(i,1);
            throw new Exception();
        }catch (Exception e){
            e.printStackTrace();
            // 进行数据回滚操作
            sqlSession.rollback();
        }
    }

事务的隔离级别

在我们学习数据库的时候,涉及到事务这一模块的时候经常会涉及到的三个名词就是

  • 脏读
  • 不可重复读
  • 幻读

脏读 :

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。

不可重复读 :

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

幻读 :

是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。

事务隔离:

这个时候我们就需要设置事务的隔离级别来解决这个问题
在这里插入图片描述
mybatis中也可以设置隔离级别,只不过增加了一个没有事务的属性

package org.apache.ibatis.session;

public enum TransactionIsolationLevel {
    NONE(0),
    READ_COMMITTED(2),
    READ_UNCOMMITTED(1),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int level;

    private TransactionIsolationLevel(int level) {
        this.level = level;
    }

    public int getLevel() {
        return this.level;
    }
}

延迟加载策略

什么是延迟加载

延迟加载:

就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.

好处:

先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。

坏处:

因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

需求:

查询地址(Address)信息并且关联查询用户(User)信息。
如果先查询地址(Address)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息。把对用户(User)信息的按需去查询就是延迟加载。
实现多表操作时,我们使用了resultMap来实现一对一,一对多关系的操作。主要是通过 association、collection 实现一对一及一对多映射。
association、collection 具备延迟加载功能。

association实现延迟加载

未实现延迟加载的时候

我们之前未实现延迟加载的时候,每次操作,都会直接查询出我们需要的数据
在这里插入图片描述
可以看到图中包含多条sql的执行

开启延迟加载

找到对应设置
https://mybatis.org/mybatis-3/zh/configuration.html#settings
在这里插入图片描述
通过lazyLoadingEnabled、aggressiveLazyLoading可以对延迟加载进行配置

  <settings>
        <setting name="logImpl" value="LOG4J"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

添加了懒加载配置之后,我们在进行列表查询的过程中就不会再做大量的关联查询了,可以提升列表查询的效率,在我们用到具体字段之后才会进行关联查询。

aggressiveLazyLoading

属性开启之后,我们获取到变量的任意属性,就会触发懒加载,而关闭之后,我们只有触发到关联属性时,才会触发懒加载

配置每个关联字段的加载方式

在关联字段上也可以通过设置fetchType来指定加载方式。对于当个关联属性指定fetchType优先级是高于全局配置

    <resultMap id="addressResultMap" type="Address">
        <association property="user" column="user_id" javaType="User"
                     select="com.tledu.erp.mapper.IUserMapper.selectById" fetchType="lazy"/>
    </resultMap>
collection实现懒加载

collection的懒加载实现和association基本类似

使用注解进行开发

mybatis常用注解说明

@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@ResultMap:实现引用@Results 定义的封装
@One:实现一对一结果集封装
@Many:实现一对多结果集封装

实现基本的CRUD

public interface IAddressDao {
    @Insert("insert into t_address (addr, phone, postcode, user_id) VALUES (#{addr},#{phone},#{postcode},#{userId})")
    int insert(Address address);

    @Delete("delete from t_address where id = #{id}")
    int delete(int id);

    @Update("update t_address set addr = #{addr} where id = #{id}")
    int update(Address address);

    @Select("select * from t_address where id = #{id}")
    Address selectById(int id);
}

使用Result进行映射

如何我们需要映射结果集的时候可以通过@Results注解进行映射

    @Select("select * from t_address where id = #{id}")
    @Results(id = "addressRes", value = {
            //id = true 标志这个字段是主键
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "addr", property = "addr"),
            @Result(column = "phone", property = "mobile"),
    })
    Address selectById(int id);

注解进行关联查询

1对1
    @Select("select * from t_address where id = #{id}")
    @Results(id = "addressRes", value = {
            //id = true 标志这个字段是主键
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "addr", property = "addr"),
            @Result(column = "phone", property = "mobile"),
            @Result(column = "user_id", property = "user",
                    one = @One(select = "com.tledu.erp.mapper.IUserMapper.selectById", fetchType = FetchType.EAGER))
    })
    Address selectById(int id);
1对多
    @Select("select * from t_user where id = #{id}")
    @Results(id = "addressRes", value = {
            //id = true 标志这个字段是主键
            @Result(id = true, column = "id", property = "id"),
            @Result(column = "id", property = "addressList",
                    many = @Many(select = "com.tledu.erp.mapper.IAddressMapper.listByUserId", fetchType = FetchType.EAGER))
    })
    User selectById(int id);

缓存

像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。
Mybatis 中缓存分为一级缓存,二级缓存
在这里插入图片描述

一级缓存

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。

测试
总结

一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
在这里插入图片描述

第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
得到用户信息,将用户信息存储到一级缓存中。
如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息(但是再查询时需要重新执行SQL去数据库查询),避免脏读。
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存
中获取用户信息。缓存中没有,重新执行SQL去数据库查找,然后再放入缓存中

二级缓存

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
在这里插入图片描述

首先开启 mybatis 的二级缓存。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中
如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 增删改的commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。
当然clearCache() 不会清空二级缓存的数据,如果希望清空二级缓存
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据

开启二级缓存
<setting name="cacheEnabled" value="true"/>
在映射文件中开启缓存支持
<mapper namespace="com.tledu.erp.mapper.IAddressMapper">
  <!-- 开启缓存支持-->
    <cache />
</mapper>

查询语句中需要指定useCache=“true”

<select id="selectOne" resultMap="addressResultMap" useCache="true">
        select *
        from t_address
        where id = #{id}
</select>
注意事项

当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这种就可以使用序列化方式来保存对象。

@Data
public class Address implements Serializable {
    private Integer id;
    private String addr;
    private String phone;
    private String postcode;
    private Integer userId;
    /**
     * 创建用户,关联用户表
     */
    private User user;
}

总结

在使用二级缓存的时候,需要注意配置mybatis-config.xml中 开启二级缓存

<setting name="cacheEnabled" value="true"/>
  • 然后再mapper映射文件中使用catch标签标注开启,并对需要换成的语句添加useCache=”true”
  • 在mapper的映射文件中使用,代表当前mapper是开启二级缓存的 在需要二级缓存的查询上增加useCache = true,代表当前查询是需要缓存的
  • 并且对应封装数据的实体类需要实现Serializable 接口
  • 对待缓存的数据,实现Serialization接口,代表这个数据是可序列化
  • 只有当sqlSession close之后,二级缓存才能生效
  • 当执行增删改操作的时候,必须执行commit()才能持久化到数据库中,同时二级缓存清空
  • session.clearCache()无法清除二级缓存,如果需要清除二级缓存,可以通过sqlSessionFactory.getConfiguration().getCache(“缓存id”).clear();
  • 但是当我们查询语句中,执行commit() 或者是close()关闭session,都不会清空二级缓存

分页的实现

为什么要分页

数据特别多的时候,单次请求返回大量的数据接口会非常慢。
对于数据量特别大的查询,我们都会采用分页查询

设计分页

  • 每页有多少个
  • 当前是在第几页
  • 数据的总数
  • 数据列表

基于这些属性设计分页的实体类

@Data
public class PageInfo<T> {
    /**
     * 每页有多少个
     */
    private int pageSize;
    /**
     * 当前是在第几页
     */
    private int currentPage;
    /**
     * 数据的总数
     */
    private int total;
    /**
     * 数据列表
     */
    private List<T> list;
    
    // 获取偏移量
    public int getOffset() {
        return (this.currentPage - 1) * this.pageSize;
    }
}

实现分页功能

创建分页查询的方法

	/**
     * 分页查询
     *
     * @param user     查询条件
     * @param offset   起始位置
     * @param pageSize 每页容量
     * @return 用户列表
     */
    List<User> page(@Param("user") User user, @Param("offset") int offset, @Param("pageSize") int pageSize);

    /**
     * 统计总数
     *
     * @param user 查询条件
     * @return 总数
     */
    int count(@Param("user") User user);
    <select id="page" resultType="User">
        select *
        from t_user
        <where>
            <if test="user.nickname != null and user.nickname != ''">
                and nickname like concat('%',#{user.nickname},'%')
            </if>
            <if test="user.username != null and user.username != ''">
                and username = #{user.username}
            </if>
        </where>
        limit #{offset},#{pageSize};
    </select>
    <select id="count" resultType="int">
        select count(*)
        from t_user
        <where>
            <if test="user.nickname != null and user.nickname != ''">
                and nickname like concat('%',#{user.nickname},'%')
            </if>
            <if test="user.username != null and user.username != ''">
                and username = #{user.username}
            </if>
        </where>
    </select>

测试

    @Test
    public void page(){
        PageInfo<User> pageInfo = new PageInfo<User>();
        pageInfo.setCurrentPage(1);
        pageInfo.setPageSize(10);
        User user = new User();
        user.setNickname("尚云");
        // 加上筛选条件,根据nickname 或 username进行筛选
        List<User> list = userMapper.page(user,pageInfo.getOffset(),pageInfo.getPageSize());
        pageInfo.setList(list);
        pageInfo.setTotal(userMapper.count(user));
        System.out.println(pageInfo);
    }

分页插件

引入依赖

https://pagehelper.github.io/

 <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.2.0</version>
        </dependency>

配置拦截器

在mybatis的配置文件中增加插件

<!--
    plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
    properties?, settings?,
    typeAliases?, typeHandlers?,
    objectFactory?,objectWrapperFactory?,
    plugins?,
    environments?, databaseIdProvider?, mappers?
-->
<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
        <property name="param1" value="value1"/>
	</plugin>
</plugins>

配置插件

https://pagehelper.github.io/docs/howtouse/#2-%E9%85%8D%E7%BD%AE%E6%8B%A6%E6%88%AA%E5%99%A8%E6%8F%92%E4%BB%B6

使用插件

    @Test
    public void testList() throws IOException {
        SqlSession session = MybatisUtils.openSession();
        User condition = new User();
        // 插件里提供的分页工具,在要查询之前,执行一下PageHelper.startPage(当前页数,每页的容量), 当使用工具时候,会导致懒加载失败
        // 加了这个操作,插件就会在sql语句中拼接limit限制,并且还会统计总个数
        PageHelper.startPage(1,5);
        List<User> users = session.getMapper(IUserMapper.class).list(condition);
        // 拿到结果之后通过PageInfo.of() 的方法,获得pageInfo
        com.github.pagehelper.PageInfo<User> list = com.github.pagehelper.PageInfo.of(users);
        System.out.println(users);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值