项目收获总结--MyBatis的知识收获

MyBatis的知识收获

一、概述

最近几天公司项目开发上线完成,做个收获总结吧~ 今天记录MyBatis的收获和提升。

二、获取自动生成的(主)键值

insert 方法总是返回一个 int 值 ,这个值代表的是插入的行数。若表的主键id采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。

xml 复制代码
<insert id="insertUser" usegeneratedkeys="true" keyproperty=" id">
    insert into table_name (name, age) values (#{name}, #{age})
</insert>
java 复制代码
User user = new User();
user.setName("fred");
user.setAge(18);
int rows = mapper.insertUser(user);
// 完成后,id 已经被设置到user对象中
system.out.println("插入数据条数:" + rows);
system.out.println("插入数据的主键为:" + user.getid());
三、将sql执行结果封装为目标返回对象的方式和原理

方式:

java 复制代码
第一种是使用 <resultMap> 标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用 sql 列的别名功能(as 关键字),将列的别名书写为对象属性名。

原理:

java 复制代码
使用列名与属性名的映射关系,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
四、延迟加载实现原理

Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。

原理是使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来, 然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。

不光是 Mybatis,几乎所有的ORM框架,包括 Hibernate,支持延迟加载的原理都是一样的。

五、批量插入

第一种:采用SqlSession批量插入模式

java 复制代码
List <User> users = new ArrayList();
// 注意这里 executortype.batch
SqlSession sqlsession = SqlSessionFactory.openSession(executortype.Batch);
try {
    UserMapper mapper = Sqlsession.getMapper(UserMapper.Class);
for (User user: users) {
    mapper.insertUser(user);
}
sqlsession.commit();
} catch (Exception e) {
    e.printStackTrace();
    sqlSession.rollback();
    throw e;
}
finally {
    sqlsession.close();
}

sql:

xml 复制代码
<insert id="insertUser">
    insert into userTable (name, age) values (#{name}, #{age})
</insert>

第二种:标签在XML映射文件中构建批量插入的SQL语句

xml 复制代码
<insert id="insertBatch">
    INSERT INTO userTable (column1, column2, ...)
    VALUES
    <foreach collection="list" item="item" index="index" separator=",">
        (#{item.field1}, #{item.field2}, ...)
    </foreach>
</insert>

需要传入一个实体类的List集合,column1, column2, ... 是表中的列名,field1, field2, ... 是你的实体类中的属性名。若数据集特别大,可能需要考虑数据库事务的隔离级别、批次大小、以及可能出现的内存溢出等问题。

六、自带分页与分页插件原理

Mybatis 本身使用 RowBounds 对象进行分页,针对 ResultSet 结果集执行的内存分页,而非物理分页。

但是在sql 直接用limit等关键字可以物理分页,也可使用分页插件来完成物理分页。

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

七、Mapper(Dao)接口与XML映射文件关系

Mapper(Dao)接口的全限名,就是xml映射文件中的 namespace 的值,接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给SQL的参数。

Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个 <select>、<insert>、<update>、<delete>标签,都会被解析为一个 MapperStatement 对象。

Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

八、模糊查询like语句

Mapper接口中定义方法:

java 复制代码
List<Item> findItemsByName(@Param("name") String name);

Mapper XML文件中编写SQL查询:

xml 复制代码
<select id="findItemsByName" resultType="Item">
  SELECT * FROM item WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

CONCAT('%', #{name}, '%')用于拼接SQL语句中的模糊查询字符串。

而SQL语句中拼接通配符,会有SQL注入风险:

xml 复制代码
<select id="findItemsByName" resultType="Item">
  SELECT * FROM item WHERE name LIKE"%"#{name}"%"
</select>
九、#{}和${}的区别

#{} 是预编译处理,${}是字符串替换。

Mybatis 在处理#{}时,会将 sql 中的#{}替换为'?'号,调用PreparedStatement的set方法来赋值;

Mybatis 在处理{}时,就是把{}替换成变量的值。

使用#{}可以有效的防止 SQL 注入,提高系统安全性。

十、二级缓存

二级缓存的和一级缓存功能一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。

一级缓存是基于sqlSession的,二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper执行sql查询到的数据,也存储在相同的二级缓存区域中。

开启二级缓存

一级缓存默认开启,但二级缓存需要手动开启。

首先在全局配置文件sqlMapConfig.xml文件中加入配置:

xml 复制代码
<!--开启二级缓存-->
<settings>
   <setting name="cacheEnabled" value="true"/>
</settings>

然后在UserMapper.xml文件中开启缓存(若是基于注解形式进行查询,可以在mapper查询接口上添加@CacheNamespace注解开启二级缓存)。
要进行二级缓存的Pojo类必须实现Serializable接口

方式一:在xml文件中配置:

xml 复制代码
<mapper namespace="com.demo.mapper.UserMapper">
   <!-- 定义二级缓存 -->
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
    <select>select * from table</select>
    <delete>delete from table where id = #{id}</delete>
    ...........
</mapper>

清除策略有:

xml 复制代码
LRU -- 最近最少使用:移除最长时间不被使用的对象。
FIFO -- 先进先出:按对象进入缓存的顺序来移除它们。
SOFT -- 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK -- 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。

方式二:@CacheNamespace

java 复制代码
@Mapper
@CacheNamespace(eviction = FifoCache.class, flushInterval = 10000, size = 500, readWrite = true)
public interface UserMapper {
    // 定义 SQL 映射语句
}
案例实战

MybatisPlus整合Redis实现分布式二级缓存

MyBatis 内置的实现 PerpetualCache在分布式环境下存在问题,无法使用,因此使用implementation属性用于指定自定义缓存实现类,接下来整合Redis来实现分布式的二级缓存。

1.pom文件:

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.4.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.3</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.22</version>
</dependency>

配置文件:

yaml 复制代码
mybatis-plus:
    mapper-locations: classpath:mybatis/mapper/*.xml
    configuration:
      cache-enabled: true

Mapper接口上开启二级缓存:

java 复制代码
@CacheNamespace(
   implementation = MybatisRedisCache.class,
   eviction = MybatisRedisCache.class)
@Mapper
public interface UserMapper extends BaseMapper<User> {

}

或者使用xml映射,UserMapper.xml加入:

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">
<mapper namespace="com.lagou.mapper.IUserMapper">
    <!--二级缓存类地址-->
	<cache type="org.mybatis.caches.redis.RedisCache" />
	<select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
	    select * from user
	</select> 
</mapper>

配置RedisTemplate:

java 复制代码
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfiguration {
    private static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();
    private static final GenericJackson2JsonRedisSerializer JACKSON__SERIALIZER = new GenericJackson2JsonRedisSerializer();
    @Bean
    @Primary
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        //设置缓存过期时间
        RedisCacheConfiguration redisCacheCfg = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON__SERIALIZER));
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheCfg)
                .build();
    }
    @Bean
    @Primary
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        // key序列化
        redisTemplate.setKeySerializer(STRING_SERIALIZER);
        // value序列化
        redisTemplate.setValueSerializer(JACKSON__SERIALIZER);
        // Hash key序列化
        redisTemplate.setHashKeySerializer(STRING_SERIALIZER);
        // Hash value序列化
        redisTemplate.setHashValueSerializer(JACKSON__SERIALIZER);
        // 设置支持事务
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    @Bean
    public RedisSerializer<Object> redisSerializer() {
        //创建JSON序列化器
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //必须设置,否则无法将JSON转化为对象,会转化成Map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

自定义缓存类:

java 复制代码
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
@Slf4j
public class MybatisRedisCache implements Cache {
    // redisson 读写锁
    private final RReadWriteLock redissonReadWriteLock;
    // redisTemplate
    private final RedisTemplate redisTemplate;
    // 缓存Id
    private final String id;
    //过期时间 10分钟
    private final long expirationTime = 1000*60*10;
    public MybatisRedisCache(String id) {
        this.id = id;
        //获取redisTemplate
        this.redisTemplate = SpringUtil.getBean(RedisTemplate.class);
        //创建读写锁
        this.redissonReadWriteLock = SpringUtil.getBean(RedissonClient.class).getReadWriteLock("mybatis-cache-lock:"+this.id);
    }
    @Override
    public void putObject(Object key, Object value) {
        //使用redis的Hash类型进行存储
        redisTemplate.opsForValue().set(getCacheKey(key),value,expirationTime, TimeUnit.MILLISECONDS);
    }
    @Override
    public Object getObject(Object key) {
        try {
            //根据key从redis中获取数据
            Object cacheData = redisTemplate.opsForValue().get(getCacheKey(key));
            log.debug("[Mybatis 二级缓存]查询缓存,cacheKey={},data={}",getCacheKey(key), JSONUtil.toJsonStr(cacheData));
            return cacheData;
        } catch (Exception e) {
            log.error("缓存出错",e);
        }
        return null;
    }
    @Override
    public Object removeObject(Object key) {
        if (key != null) {
            log.debug("[Mybatis 二级缓存]删除缓存,cacheKey={}",getCacheKey(key));
            redisTemplate.delete(key.toString());
        }
        return null;
    }
    @Override
    public void clear() {
        log.debug("[Mybatis 二级缓存]清空缓存,id={}",getCachePrefix());
        Set keys = redisTemplate.keys(getCachePrefix()+":*");
        redisTemplate.delete(keys);
    }
    @Override
    public int getSize() {
        Long size = (Long) redisTemplate.execute((RedisCallback<Long>) RedisServerCommands::dbSize);
        return size.intValue();
    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.redissonReadWriteLock;
    }
    @Override
    public String getId() {
        return this.id;
    }
    public String getCachePrefix(){
        return "mybatis-cache:%s".formatted(this.id);
    }
    private String getCacheKey(Object key){
        return getCachePrefix()+":"+key;
    }
}

最后调用接口执行SQL即可测试缓存效果。

相关推荐
wljt6 小时前
公司二-面试题
java·开发语言·mybatis
VimorphTech10 小时前
文本已经改了,旧的音频生成结果还能回写吗?
java·数据库·mybatis
麻瓜code14 小时前
双写一致性:为什么是“删缓存“,而不是“改缓存“
java·缓存·mybatis
郭小李1 天前
mybatis 动态 sql
java·sql·mybatis
知守观1 天前
技术考古:iBATIS 2.3.4 时代的持久层,是如何用 Map 玩转 307 个存储过程的?
java·后端·mybatis
captain3761 天前
MyBatis Plus(1)
java·服务器·mybatis
代码不停2 天前
MyBatis进阶
spring boot·mybatis
captain3762 天前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
captain3762 天前
MyBatis(进阶1)
mybatis
郭小李3 天前
mybatis的 Mapper.xml语法
xml·java·mybatis