mybatis

环境:

  • JDK1.8
  • MySQL8.0.22
  • maven3.6.3
  • IDEA

官网:https://mybatis.net.cn/

alt

myba那个价tis是一款优秀的持久层框架,它支持定制化SQL、存储过程、高级映射。避免了几乎所有的JDBC代码和手动配置参数以及获取结果集。mybatis可以使用简单的xml或注解来配置和映射原生类型、接口和java的POJO类(Plain Old Java Object 普通老式java对象)为数据库中的记录。

mybatis入门程序

数据库准备

alt

导入依赖

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.10</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
</dependency>

增加配置文件mybatis-config.xml

==注意:单纯的mybatis不能用通配符添加mapper.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>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/karottes?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/karotte/dao/mapper/karotterMapper.xml"/>
    </mappers>
</configuration>

增加MybatisUtils工具类

public class MybatisUtil {
    private static SqlSessionFactory sqlSessionFactory;
    static{
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
    *获取SqlSession
    */
    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();
        //return sqlSessionFactory.openSession(true);//设置事务自动提交
    }
}

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Karotter {
    private int id;
    private String name;
    private String password;
    private String sex;
    private Date birthday;
    private String address;
    private String email;
    private int age;
}

mapper接口

public interface KarotterMapper {
    Karotter selectKarotterById(int id);
    List<Karotter> getAll();
}

mapper.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.karotte.dao.mapper.KarotterMapper">
    <select id="getAll" resultType="com.karotte.pojo.Karotter">
        select * from karotter
    </select>
    <select id="selectKarotterById" resultType="com.karotte.pojo.Karotter">
        select * from karotter where id = #{id}
    </select>
</mapper>

测试类

public class KarotterTest {
	@Test
    public void getAllTest(){
        try (SqlSession sqlSession = MybatisUtil.getSqlSession();) {
            KarotterMapper mapper = sqlSession.getMapper(KarotterMapper.class);
            List<Karotter> all = mapper.getAll();
            all.forEach((karotter)->{
                System.out.println(karotter);
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时运行会报错

Could not find resource com/karotte/dao/mapper/*.xml

maven由于约定大于配置,所以我们写的配置文件无法被导出或者生效,解决方案是pom文件中添加以下信息:

 <build>
     <resources>
         <resource>
             <directory>src/main/resource</directory>
             <includes>
                 <include>**/*.properties</include>
                 <include>**/*.xml</include>
             </includes>
             <filtering>true</filtering>
         </resource>
         <resource>
             <directory>src/main/java</directory>
             <includes>
                 <include>**/*.properties</include>
                 <include>**/*.xml</include>
             </includes>
             <filtering>true</filtering>
         </resource>
     </resources>
</build>

此时会有==警告==,数据库驱动过时

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
Karotter(id=1, name=卡卡罗特, password=45678, sex=1, birthday=Sat Jun 18 03:30:00 CST 2022, address=达到三大, email=wuqing@163.com, age=28)
Karotter(id=2, name=贝吉塔, password=kaka098, sex=1, birthday=Sat Jun 18 03:33:56 CST 2022, address=贝吉塔行星, email=wuqing@163.com, age=28)

将mybatis-config.xml数据库驱动改成最新的即可

<property name="driver" value="com.mysql.cj.jdbc.Driver"/> 

中文乱码

控制台报错:MalformedByteSequenceException: 2 字节的 UTF-8 序列的字节 2 无效。

在IDEA中将properties文件编码改为UTF-8即可。 点击File --> Settings --> Editor --> File Encodings alt

模糊查询

方式一:连接符$,sql注入,不安全

<select id="fuzzyQuery" resultType="com.bin.pojo.Book">
    select * from mybatis.book where bookName like '%${info}%';
</select>

方式二:占位符#

<select id="fuzzyQuery" resultType="com.bin.pojo.Book">
    select * from mybatis.book where bookName like 
    concat('%',#{info},'%');
</select>

总结:

  • #{}是预编译处理,mybatis在处理#{}时,会将其替换成"?",再调用PreparedStatement的set方法来赋值。
  • ${}是拼接字符串,将接收到的参数的内容不加任何修饰的拼接在SQL语句中,会引发SQL注入问题。

配置解析

核心配置文件(mybatis-config.xml)

配置元素前后顺序

properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers

生命周期和作用域

生命周期和作用域是至关重要的,错误的使用会导致非常严重的并发问题

SqlSessionFactoryBuilder

  • 一旦创建了SqlSessionFactory,就不需要了
  • 局部变量

SqlSessionFactory

  • 可以看做是数据库连接池
  • SqlSessionFactory一旦被创建就应该在运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例,因此SqlSessionFactory的最佳作用域是应用作用域
  • 最简单的就是使用单例模式或者静态单例模式

SqlSession

  • 连接到连接池的一个请求
  • SqlSession的实例不是线程安全的,因此不能被共享,所以最佳作用域是请求或者方法作用域
  • 用完之后就关闭,否则资源被占用

解决属性名和字段名不一致

sql中起别名

select id,name,`password` as pwd from karotter

resultMap-结果集映射

数据库字段类型和java类对应关系
JDBC Type Java Type
CHAR String
VARCHAR String
LONGVARCHAR String
NUMERIC java.math.BigDecimal
DECIMAL java.math.BigDecimal
BIT boolean
BOOLEAN boolean
TINYINT byte
SMALLINT short
INTEGER int
BIGINT long
REAL float
FLOAT double
DOUBLE double
BINARY byte[]
VARBINARY byte[]
LONGVARBINARY byte[]
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp
CLOB Clob
BLOB Blob
ARRAY Array
DISTINCT mapping of underlying type
STRUCT Struct
REF Ref
DATALINK java.net.URL[color=red][/color]
<resultMap id="karotte" type="com.karotte.pojo.Karotter">
    <result column="id" jdbcType="INTEGER" property="id" javaType="int" />
    <result column="name" jdbcType="VARCHAR" property="name" javaType="String" />
    <result column="password" jdbcType="VARCHAR" property="password" javaType="String" />
</resultMap>

<!-- 有中文怎么说 -->
<select id="getAll" resultMap="karotte">
    select * from karotter
</select>

上述resultMap里面虽然只有三个,但是查询结果所有字段都会返回,所以说定义的时候***==只需要将不一致的字段显式定义即可==***。

Karotter(id=1, name=卡卡罗特, password=45678, sex=1, birthday=Sat Jun 18 03:30:00 CST 2022, address=北京通州, email=wuqing@163.com, age=28)
Karotter(id=2, name=贝吉塔, password=kaka098, sex=1, birthday=Sat Jun 18 03:33:56 CST 2022, address=贝吉塔行星, email=wuqing@163.com, age=28)

复杂例子
<resultMap id="detailedBlogMap" type="com.karotte.pojo.Blog">
	<constructor>
		<idArg column="blog_id" javaType="int"/>
	</constructor>
    <result column="blog_title" property="title"/>
    <association property="author" javaType="com.karotte.pojo.Author">
        <id column="author_id" property="id"/>
        <result column="author_name" property="username"/>
        <result column="author_password" property="password"/>
	</association>
	<collection property="posts" ofType="com.karotte.pojo.Post">
        <id column="post_id" property="id"/>
        <result column="post_subject" property="subject"/>
        <discriminator javaType="int" column="draft">
            <case value="1" resultType="com.karotte.pojo.DraftPost"/>
            <case value="2" resultType="com.karotte.pojo.ApplePost"/>
        </discriminator>
	</collection>
</resultMap>

日志

日志工厂

配置文件中配即可

alt

  • SLF4J
  • LOG4J
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • NO_LOGGING
  • STDOUT_LOGGING:标准日志输出
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

log4j

  • log4j是Apache的开源项目,通过使用log4j,我们可以控制日志信息传输到控制台、文件、GUI组件等。
  • 控制每一条日志的输出格式
  • 定义日志信息级别
  • 通过一个配置文件来灵活的配置,不需要修改应用代码
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

log4j.properties

log4j.rootLogger=DEBUG,console,file
# 控制台(console)
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.ImmediateFlush=true
log4j.appender.console.Target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
# 日志文件(logFile)
log4j.appender.logFile=org.apache.log4j.FileAppender
log4j.appender.logFile.Threshold=DEBUG
log4j.appender.logFile.ImmediateFlush=true
log4j.appender.logFile.Append=true
log4j.appender.logFile.File=D:/logs/log.log4j
log4j.appender.logFile.layout=org.apache.log4j.PatternLayout
log4j.appender.logFile.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n
# 自定义Appender
log4j.appender.im = net.cybercorlin.util.logger.appender.IMAppender
log4j.appender.im.host = mail.cybercorlin.net
log4j.appender.im.username = username
log4j.appender.im.password = password
log4j.appender.im.recipient = corlin@cybercorlin.net
log4j.appender.im.layout=org.apache.log4j.PatternLayout
log4j.appender.im.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n

#日志的输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
import org.apache.log4j.Logger;
private static Logger logger = Logger.getLogger(KarotterTest.class);

日志级别

级别 描述
ALL 所有级别包括自定义级别。
DEBUG 调试消息日志。
ERROR 错误消息日志,应用程序可以继续运行。
FATAL 严重错误消息日志,必须中止运行应用程序。
INFO 信息消息。
OFF 最高可能的排名,旨在关闭日志记录。
TRACE 高于DEBUG。
WARN 用于警告消息。

mybatis流程

alt

动态sql

==只要最终的sql没有问题,随便拼==

<!--使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分 -->
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

<!-- 只是想从多个条件中选择一个使用。针对这种情况,MyBatis
 提供了 choose 元素,它有点像 Java 中的 switch 语句。 -->
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

<!-- 动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候) -->
<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

<!--bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。 -->
<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

<!--用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。 -->
<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

sql片段

<sql id="kaka">
    id,name,password
</sql>
<select id="getAll" resultMap="karotte">
    select 
    <include refid="kaka"></include>
    from karotter
</select>

缓存-了解,常用redis

mybatis缓存

  • mybatis包含了一个非常强大的查询缓存特性,它可以非常方便的定制和配置缓存。缓存可以极大的提高查询效率。
  • mybatis系统中默认定义了两级缓存:一级缓存和二级缓存
    • 默认情况只有一级缓存开启(SqlSession级别的缓存,也成为本地缓存)
    • 二级缓存需要手动开启和配置,基于namespace的缓存
    • 为了提高扩展性,mybatis定义了缓存接口Cache,我们可以通过实现Cache接口定义二级缓存

一级缓存

KarotterMapper mapper = sqlSession.getMapper(KarotterMapper.class);
List<Karotter> all = mapper.getAll();
List<Karotter> all1 = mapper.getAll();
sqlSession.clearCache();
List<Karotter> all2 = mapper.getAll();
List<Karotter> all3 = mapper.getAll();

//=======================================
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): ==>  Preparing: select * from karotter  
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): ==> Parameters:   
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): <==      Total: 12  
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): ==>  Preparing: select * from karotter  
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): ==> Parameters:   
[DEBUG] org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:137): <==      Total: 12 

一级缓存默认开启,只在一次sqlSession中有效,无法关闭

alt

alt

源码分析

SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。 Executor: SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。Executor有两个实现类,和一级缓存关联的是BaseExecutor。 BaseExecutor: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。 **PerpetualCache:**对Cache接口最基本实现,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。

一级缓存失效
  • 同一个用户使用不同的SqlSession对象导致无法看到一级缓存工作
  • 在一个SqlSession中使用条件查询不同一级缓存也会失效
  • 在一个SqlSession使用相同条件,但是,此时在查询之间进行数据修改操作会导致一级缓存失效
  • 在一个SqlSession使用相同查询条件此时手动刷新缓存时导致一级缓存失败
注意
  • MyBatis一级缓存的生命周期和SqlSession一致
  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据
  • mybatis和spring整合后进行mapper代理开发,不支持一级缓存

二级缓存

开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询

alt

==当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库==

二级缓存配置
  1. 需要在MyBatis核心配置文件,通过settings标签开发二级缓存。

    <setting name="cacheEnabled" value="true"/>
    
  2. 在对应的Mapper文件中添加cache标签

  3. cache标签属性

    • type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过

    • eviction: 定义回收的策略,常见的有FIFO,LRU

    • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒

    • size: 最多缓存对象的个数

    • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化

    • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存

    • cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache

      <cache-ref namespace="mapper.StudentMapper"/>
      

      参数详解参见:https://mybatis.net.cn/sqlmap-xml.html#cache

      <cache
        eviction="FIFO"
        flushInterval="60000"
        size="512"
        readOnly="true"/>
      
      可用的清除策略有:
      LRU – 最近最少使用:移除最长时间不被使用的对象。
      FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
      SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
      WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
      默认的清除策略是 LRU。
      
    自定义缓存
    <cache type="com.domain.something.MyCustomCache"/>
    

    type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。