1、Mybatis

Mybatis

优点

缺点

简单使用

1、导入mybatis依赖

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

2、导入目标数据库的对应驱动

目前使用的是mysql5.7.28

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

3、创建表

CREATE TABLE `student` (
  `obj_id` varchar(70) NOT NULL COMMENT '主键',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`obj_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4、创建表对应实体类

注意:一般放在entity包下,体现该类为数据表实体

一般命名要求:表名和实体类类名相同,实体类首字母大写;表字段名和属性名相同

public class Student {

    private String obj_id;

    private String name;

    private String age;

    // getter、setter
}

5、编写持久层接口

注意:一般放在dao包下,体现该接口为数据表持久层接口

public interface StudentDao {
    
    /**
     * 查询全部学生
     * @return
     */
    List<Student> selectAll();
}

6、编写映射xml文件

注意:一般放在mapper包中,体现是持久层接口的映射文件;如果是Maven项目,一般放在resources/mapper包中。文件名一般与持久层接口名保持一致,例如:StudentDao.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用于区别不同映射文件的SQL语句,必须是唯一的,一般是对应持久层接口的全限定名-->
<mapper namespace="top.ygang.mybatisdemo.dao.StudentDao">
    
    <!-- id作为SQL语句的唯一标识,对应接口中的方法名 -->
    <!-- resultType:返回对象的类全名 -->
    <select id="selectAll" resultType="top.ygang.mybatisdemo.entity.Student">
        select * from student
    </select>

</mapper>

7、创建核心配置文件

命名固定为:mybatis-config.xml。并且放在src目录;如果是Maven项目,放在resources目录

<?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="STDOUT_LOGGING"/>
    </settings>
    <!-- 配置环境,可以包含多个环境,default属性值指定使用哪个环境节点配置 -->
    <environments default="dev">
        <!-- 环境节点的配置 -->
        <environment id="dev">
            <!-- 事务管理器 -->
            <transactionManager type="JDBC"/>
            <!-- 数据源,type:设置数据源的类型,pooled为使用默认连接池 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 管理sql映射文件 -->
    <mappers>
        <!-- 持久层接口映射文件路径 -->
        <mapper resource="mapper/StudentDao.xml"/>
    </mappers>
</configuration>

7、进行使用

public class Start {

    public static void main(String[] args) throws Exception{
        // 读取核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 由mybatis读核心配置文件,将读到的的信息缓存在SqlSessionFactory工厂对象中
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 从工厂中获取SqlSession对象,该对象可以进行数据库交互
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 获取持久层接口实现类对象
        StudentDao studentDao = sqlSession.getMapper(StudentDao.class);

        // 调用方法,执行sql
        List<Student> students = studentDao.selectAll();
        System.out.println(students);

        // 关闭资源,释放数据库链接
        sqlSession.close();
    }
}

少量SQL

在少量简单SQL的情况下,我们并不需要特别声明一个持久层接口的映射文件,我们可以使用mybatis提供的注解@Select@Update@Delete@Insert直接在持久层接口上面进行绑定sql。

例如上面的例子,我们的StudentDao接口中只有一个sql,而且简单,我们就不需要写StudentDao.xml ,而是将StudentDao修改下

public interface StudentDao {

    /**
     * 查询全部学生
     * @return
     */
    @Select("select * from student")
    List<Student> selectAll();
}

由于我们没有了StudentDao.xml,所以核心配置文件也需要改一下

<mappers>
    <!-- 持久层接口映射文件路径 -->
	<!-- <mapper resource="mapper/StudentDao.xml"/> -->
    <!-- 持久层接口全限定名 -->
    <mapper class="top.ygang.mybatisdemo.dao.StudentDao"/>
</mappers>

参数的获取

在MyBatis中,我们可以使用#{}${}来获取接口形参,二者的区别在于

单参数

接口只有一个参数的情况下,我们直接可以使用#{fieldName}获取

//接口
GoodType findById(int id);
<!--xml映射文件-->
<select id="findById">
    select * from goodtype where id=#{id}
</select>

多参数

方式一:通过在xml映射文件标签中参数以arg0-argn按照形参的顺序

//接口
boolean update1(String name,int id);
<!--xml映射文件-->
<update id="update1">
    update goodbyte set tname=#{arg0} where id=#{arg1}
</update>

方式二:通过在xml映射文件标签中参数以param1-paramn按照形参的顺序

//接口
boolean update2(String name,int id);
<!--xml映射文件-->
<update id="update2">
    update goodbyte set tname=#{param1} where id=#{param2}
</update>

方式三:采用在接口形参前添加注解@Param的方式**(常用)**

//接口
boolean update3(@Param("na")String name,@Param("i")int id);
<!--xml映射文件-->
<update id="update3">
    update goodbyte set tname=#{n} where id=#{i}
</update>

方式四:如果多个参数是同一个自定义对象的属性,使用对象

//接口
boolean update4(GoodType goodType);
<!--xml映射文件,#{xx}:xx为属性名-->
<update id="update4">
    update goodbyte set tname=#{tname} where id=#{tid}
</update>

方式五:使用Map集合的方式。缺点是,在编写sql语句的时候,无法确定map集合的key

//接口
boolean update5(Map<String,Object> map);
<!--xml映射文件,#{xx}:xx为map集合中对应的key-->
<update id="update5">
    update goodbyte set tname=#{key1} where id=#{key2}
</update>

核心配置文件

这个顺序是mybatis-config.xml中,configuration的子标签声明顺序,固定不可变

<?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>
    <!--
        The content of element type "configuration" must match
        "(properties?,settings?,typeAliases?,typeHandlers?,
		objectFactory?,objectWrapperFactory?,
        reflectorFactory?,plugins?,environments?,
		databaseIdProvider?,mappers?)".
    -->
    <!--引入properties文件,此后就可以在当前文件中使用${}的方式访问value-->
    <properties resource="jdbc.properties"></properties>
    
    <!--日志实现类,STDOUT_LOGGING为默认标准输出日志-->
    <setting name="logImpl" value="STDOUT_LOGGING"/>

    <!--
        typeAliases:设置类型别名,即为某个具体的类型(全类名)设置一个别名
        在MyBatis的范围中,就可以使用别名表示一个具体的类型
    -->

    <typeAliases>
        <!--
        typeAlias标签属性:
            type:设置需要起别名的标签
            alias:设置某个类型的别名
            (有默认的别名,默认别名就是类名(不是全类名噢),且不区分大小写)
        -->
        <!--<typeAlias type="top.ygang.entity.Student" alias="Student"></typeAlias>-->
        <!--通过包设置类型别名,指定包下所有的类型将全部拥有默认的别名,即类名且不区分大小写-->
        <!--
        <package name="top.ygang.entity"/>
        -->
    </typeAliases>


    <!--
        environments:配置连接数据库的环境
        属性:
        default:设置默认使用环境的id
    -->
    <environments default="development">
        <!--
            environment:设置一个具体的连接数据库的环境
            属性:
            id:设置环境的唯一标识,不能重复
        -->
        <environment id="development">
            <!--
                transactionManager:设置事务管理器
                属性:
                type:设置事务的方式
                type="JDBC|MANAGED"
                JDBC:表示使用JDBC中原生的事务管理方式
                MANAGED:被管理,例如Spring,mybatis的事务都交给Spring来管理
            -->
            <transactionManager type="JDBC"/>
            <!--
                dataSource:设置数据源
                属性:
                type:设置数据源的类型
                type:"POOLED|UNPOOLED|JNDI"
                POOLED:表示使用数据库连接池
                UNPOOLED:不使用数据库连接池
                JNDI:使用上下文中的数据源
            -->
            <dataSource type="POOLED">
                <!--<property name="driver" value="com.mysql.jdbc.Driver"/>-->
                <!--<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/>-->
                <!--<property name="username" value="root"/>-->
                <!--<property name="password" value="123456"/>-->
                <!--配置信息通过${}读取jdbc.properties中的配置信息进行替换-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--引入映射文件-->
    <mappers>
        <!--<mapper resource="mappers/UserMapper.xml"/>-->
        <!--
            以包的方式引入映射文件,但是必须满足两个条件:
            1、mapper接口和映射文件所在的包必须保持一致
            2、mapper接口的名字必须和映射文件的名字保持一致
        -->
        <!--
        <package name="com.atguigu.mybatis.mapper"/>
        -->
    </mappers>
</configuration>

配置log4j

1、引入依赖

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

2、配置日志实现

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

3、CLASSPATH下添加log4j.properties

TypeHandler

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

默认的TypeHandlers

img

自定义类型转换器

例如现在表里的某个字段是int类型,我们需要将他转换成Boolean类型。当大于等于0时为true;小于0时为false,我们就需要自定义类型转换器

1、继承BaseTypeHandler,并且使用注解@MappedJdbcTypes指定该转换器处理的数据库字段类型;使用@MappedTypes指明该转换器处理的Java类型

// DB字段类型
@MappedJdbcTypes(JdbcType.INTEGER)
// Java类型
@MappedTypes(Boolean.class)
public class MyTypeHandler extends BaseTypeHandler<Boolean> {
    /**
     * 增删改调用,从Java类型转DB类型
     */
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int columnIndex, Boolean aBoolean, JdbcType jdbcType) throws SQLException {
        preparedStatement.setObject(columnIndex,aBoolean ? 1 : -1);
    }
    /**
     * 查询调用,从DB类型转Java类型
     */
    @Override
    public Boolean getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        int val = resultSet.getInt(columnName);
        return val >= 0;
    }
    /**
     * 查询调用,从DB类型转Java类型
     */
    @Override
    public Boolean getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        int val = resultSet.getInt(columnIndex);
        return val >= 0;
    }
    /**
     * 查询调用,从DB类型转Java类型
     */
    @Override
    public Boolean getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        int val = callableStatement.getInt(columnIndex);
        return val >= 0;
    }
}

配置转换器

全局配置

这种配置方法将会在整个项目中生效

<typeHandlers>
    <typeHandler handler="top.ygang.mybatisdemo.MyTypeHandler"/>
</typeHandlers>

局部配置

对于查询,可以在resultMapresult标签中配置

<resultMap id="result_map" type="top.ygang.mybatisdemo.entity.Student">
    <result typeHandler="top.ygang.mybatisdemo.MyTypeHandler" column="age" property="age"/>
</resultMap>

对于增删改

可以在insertdeleteupdate标签中配置

update student set name = 'tom' where obj_id = #{id,typeHandler=top.ygang.mybatisdemo.MyTypeHandler}

MaBatis映射文件中的标签

说明: clipboard.png

MyBatis加载

MyBatis根据对关联对象查询的select语句的执行时机,分为三种类型:直接加载(默认)、侵入式延迟加载、深度延迟加载。

懒加载(延迟加载)

就是按需加载,我们需要什么的时候再去进行什么操作。而且先从单表查询,需要时再从关联表去关联查询,能大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。

要求

1、必须是分布查询

2、关联关系使用association(对一)或collection(对多)

侵入式延迟加载

执行主加载对象的查询时,不会执行对关联对象的查询,只有在访问主加载对象详情时或关联对象详情时,才会执行关联对象的查询

<!--在setting节点中加入-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="true"/>

深度延迟加载(推荐)

执行主加载对象的查询时,不会执行对关联对象的查询,访问主加载对象详情时也不会执行对关联对象的查询,只有在访问关联对象的详情时,才会执行关联对象的查询

<!--在setting节点中加入-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>

开启延迟

(可选)在<association><collection>中添加**fetchType = "lazy"**属性开启延迟,但这里默认为开启,可以不加

缓存

引入缓存之后,第一次去查询数据库,第二次以后,如果数据库的数据没有发生改变,我们就不去查询数据库,我们查缓存。使用缓存之后,就减少了查询数据库的次数,从而提高了查询效率

mybatis中的缓存可以分为一级缓存和二级缓存

哪些数据使用放在缓存中,哪些数据不适合放在缓存中

1)不频繁更新的数据可以放在缓存中,频繁发生更新的数据,不要放在缓存中

2)财务数据不要放在缓存中,不要的数据可以放在缓存中

一级缓存

一级缓存mybatis自带,不要额外配置,属于sqlSession级别的缓存(和SqlSession相关的),默认开启

说明: clipboard.png

验证

Goods find1 = goodsDaoMapper.findByGid(1);
System.out.println(find1.getDname());
System.out.println("**********************************");
Goods find2 = goodsDaoMapper.findByGid(1);
System.out.println(find2.getDname());

结果日志

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 798244209.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2f943d71]
==>  Preparing: select * from goods where gid=? 
==> Parameters: 1(Integer)
<==    Columns: gid, dname, buyprice, saleprice, remarks, tid, picPath, quantity, state
<==        Row: 1, java, 50, 80, 11111, 1, img/1.jpg, 1000, 1
<==      Total: 1
java
**********************************
java
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2f943d71]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@2f943d71]
Returned connection 798244209 to pool.

两次相同的查询,只向数据库发送了一次SQL语句,说明,第二次查询是从缓存中查询,并不是从数据库查询

一级缓存失效

情况一

第一次查询的数据和第二次查询的数据不一样

情况二

如果查询的是同样的数据,调用了更新方法(增删改DML)的方法

情况三

调用了刷新方法

sqlSession.clearCache();

情况四

SqlSession对象不一样

情况五

(select标签中)flushCache="true"属性

二级缓存

如果系统需要二级缓存,那么需要人为配置,二级别缓存属于Mapper级别、SqlSessionFactory级别的缓存

将查询出的数据保存在二级缓存中需要通过调用sqlSession的commit方法才可以进行保存在二级缓存

主要用来解决一级缓存不能跨会话(SqlSession)共享的问题,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步

如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库

说明: clipboard.png

配置二级缓存的步骤

1、在核心配置文件的<setting>节点中添加

<setting name="cacheEnabled" value="true"></setting>

2、在xml映射文件中,配置

<!--
可选属性:
type="哪种缓存机制"
size="缓存的资源数量"
eviction="缓存回收策略"
flushInterval="刷新的间隔"
readOnly="只读true\false"
    true:会给所有的查询的用户,返回同一个的java对象
    false:会给每一个查询的用户,返回同一个java对象的克隆的副本,也就是不同的对象
-->
<cache></cache>

3、封装类实现序列化接口java.io.Serializable

要求

1、二级缓存是将对象通过序列化流存储到磁盘文件,需要封装类实现序列化,如果是多表联合查询,那么关联表对应的实体类也需要序列化

2、两个SqlSession对象必须是同一个SqlSessionFactory创建

验证

SqlSession sqlSession1 = factory.openSession();
SqlSession sqlSession2 = factory.openSession();
GoodsDaoMapper mapper1 = sqlSession1.getMapper(GoodsDaoMapper.class);
GoodsDaoMapper mapper2 = sqlSession2.getMapper(GoodsDaoMapper.class);
Goods byGid1 = mapper1.findByGid(1);
System.out.println(byGid1.getDname());
sqlSession1.commit();
System.out.println("***********************************");
Goods byGid2 = mapper2.findByGid(1);
System.out.println(byGid2.getDname());
sqlSession1.close();
sqlSession2.close();

结果日志

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [dao.GoodsDaoMapper]: 0.0
Opening JDBC Connection
Created connection 1297149880.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4d50efb8]
==>  Preparing: select * from goods where gid=? 
==> Parameters: 1(Integer)
<==    Columns: gid, dname, buyprice, saleprice, remarks, tid, picPath, quantity, state
<==        Row: 1, java, 50, 80, 11111, 1, img/1.jpg, 1000, 1
<==      Total: 1
java
***********************************
Cache Hit Ratio [dao.GoodsDaoMapper]: 0.5
java
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4d50efb8]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4d50efb8]
Returned connection 1297149880 to pool.

同一SqlSessionFactory对象,所产生的不同的SqlSession对象,也可以共享缓存

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据查询。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方法

1、使用Redis替换mybatis缓存,即使查询出来的数据为null,也会存入缓存

2、使用布隆过滤器,对一些不存在的条件进行过滤

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方法

1、设置热点数据缓存永不过期

2、利用代理或装饰模式,对mybatis源码进行切入,对数据库查询进行加锁

3、使用mybatis的拦截器,对源码中的query方法进行加锁

缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方法

1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

2、如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

3、设置热点数据永远不过期