MyBatis學習筆記

1、簡介

image-20210129225418682

環境說明:

  • jdk 8 +
  • MySQL 5.7.19
  • maven-3.6.1
  • IDEA

學習前需要掌握:

  • JDBC
  • MySQL
  • Java 基礎
  • Maven
  • Junit

1.1、什麼是 MyBatis ?

  • MyBatis 是一款優秀的持久層框架
  • 它支援訂製化 SQL、存儲過程以及高級映射。
  • MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設置參數以及獲取結果集。
  • MyBatis 可以使用簡單的 XML 或註解來配置和映射原生類型、介面和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 對象)為資料庫中的記錄。
  • MyBatis 本是 apache 的一個開源項目iBatis, 2010年這個項目由 apache software foundation 遷移到了 google code,並且改名為MyBatis 。
  • 2013年11月遷移到Github。

如何獲得 MyBatis?

1.2、持久化

數據持久化

  • 持久化就是將程式的數據在持久狀態和瞬時狀態轉化的過程
  • 記憶體:斷電即失
  • 資料庫(jdbc),io 文件持久化。
  • 生活:冷藏. 罐頭。

為什麼需要需要持久化?

  • 有一些對象,不能讓他丟掉。
  • 記憶體太貴了

1.3、持久層

Dao 層,Service 層,Controller 層…

  • 完成持久化工作的程式碼塊
  • 層界限十分明顯。

1.4 為什麼需要 Mybatis ?

  • 幫助程式猿將數據存入到資料庫中。
  • 方便
  • 傳統的 JDBC 程式碼太複雜了。簡化。框架。自動化。
  • 不用 MyBatis 也可以。更容易上手。 技術沒有高低之分
  • 優點:
    • 簡單易學
    • 靈活
    • SQL 和程式碼的分離,提高了可維護性。
    • 提供映射標籤,支援對象與資料庫的 ORM 欄位關係映射
    • 提供對象關係映射標籤,支援對象關係組建維護
    • 提供 XML 標籤,支援編寫動態 SQL 。

最重要的一點:使用的人多!

2、環境搭建

2.1、搭建實驗資料庫

CREATE DATABASE `mybatis`;

USE `mybatis`;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
`id` int(20) NOT NULL,
`name` varchar(30) DEFAULT NULL,
`pwd` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `user`(`id`,`name`,`pwd`) values (1,'狂神','123456'),(2,'張三','abcdef'),(3,'李四','987654');

2.2、導入 Maven 相關依賴

<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.5.2</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.47</version>
</dependency>

2.3、編寫 MyBatis 核心配置文件

可以根據幫助文檔來進行編寫

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
       PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
       "//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
   <environments default="development">
       <environment id="development">
           <transactionManager type="JDBC"/>
           <dataSource type="POOLED">
               <property name="driver" value="com.mysql.jdbc.Driver"/>
               <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
               <property name="username" value="root"/>
               <property name="password" value="123456"/>
           </dataSource>
       </environment>
   </environments>
   <mappers>
       <mapper resource="com/kuang/dao/userMapper.xml"/>
   </mappers>
</configuration>

2.4、編寫 MyBatis 工具類

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 java.io.IOException;
import java.io.InputStream;

public class MybatisUtils {

   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 getSession(){
       return sqlSessionFactory.openSession();
  }

}

2.5、創建實體類

public class User {
   
   private int id;  //id
   private String name;   //姓名
   private String pwd;   //密碼
   
   //構造,有參,無參
   //set/get
   //toString()
   
}

2.6、編寫 Mapper 介面

import com.kuang.pojo.User;
import java.util.List;

public interface UserMapper {
   List<User> selectUser();
}

2.7、編寫 Mapper.xml 配置文件

注意 namespace 不要寫錯!

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "//mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.kuang.dao.UserMapper">
    
 <select id="selectUser" resultType="com.kuang.pojo.User">
  	select * from user
 </select>
    
</mapper>

2.8、編寫測試類

**junit 包測試 **

public class MyTest {
   @Test
   public void selectUser() {
       SqlSession session = MybatisUtils.getSession();
       //方法一:
       //List<User> users = session.selectList("com.kuang.mapper.UserMapper.selectUser");
       
       //方法二:
       UserMapper mapper = session.getMapper(UserMapper.class);
       List<User> users = mapper.selectUser();

       for (User user: users){
           System.out.println(user);
      }
       session.close();
  }
}

出現的問題:

1、Maven 靜態資源過濾問題

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

3、CRUD操作配置解析

3.1、select 標籤

  • select標籤是mybatis中最常用的標籤之一

  • select語句有很多屬性可以詳細配置每一條SQL語句

    • SQL語句返回值類型。【完整的類名或者別名】
    • 傳入SQL語句的參數類型 。【萬能的Map,可以多嘗試使用】
    • 命名空間中唯一的標識符
    • 介面中的方法名與映射文件中的SQL語句ID 一一對應
    • id
    • parameterType
    • resultType

練習 1 :根據 id 查詢 用戶

1.在 UserMapper 中添加對應方法

public interface UserMapper {
   //查詢全部用戶
   List<User> selectUser();
   //根據id查詢用戶
   User selectUserById(int id);
}

2.在UserMapper.xml中添加 select 語句

<select id="selectUserById" resultType="com.anti.pojo.User">
	select * from user where id = #{id}
</select>

3.在測試類中測試

@Test
public void tsetSelectUserById() {
   SqlSession session = MybatisUtils.getSession();  //獲取SqlSession連接
   UserMapper mapper = session.getMapper(UserMapper.class);
   User user = mapper.selectUserById(1);
   System.out.println(user);
   session.close();
}

4.運行結果

運行結果

練習2:根據 密碼 和名字 查詢用戶

方法一:直接在方法中傳遞參數

  • ​ 在介面方法中的參數前加 @Param 屬性。
  • ​ SQL 語句編寫的時候,直接取 @Param 中設置的值即可,不需要到單獨設置參數類型。
//通過密碼和名字查詢用戶
User selectUserByNP(@Param("username") String username,@Param("pwd") String pwd);


//mapper.xml
<select id="selectUserByNP" resultType="com.kuang.pojo.User">
    select * from user where name = #{username} and pwd = #{pwd}
</select>

方法二:萬能Map

  • 在介面方法中,參數數直接傳遞Map。
User selectUserByNP2(Map<String,Object> map);
  • 在編寫SQL語句的時候,需要傳遞參數類型 parameterType="map"
<select id="selectUserByNP2" parameterType="map" resultType="com.kuang.pojo.User">
    select * from user where name = #{username} and pwd = #{pwd}
</select>
  • 在使用方法的時候,Map 的 KEY 為 SQL 中取的值即可。
@Test
public void test03(){
    SqlSession session = MybatisUtils.getSession();
    UserMapper mapper = session.getMapper(UserMapper.class);
    HashMap<String, Object> map = new HashMap<String, Object>();
    map.put("username","張三");
    map.put("pwd","abcdef");
    User user = mapper.selectUserByNP2(map);
    System.out.println(user);
}

如果參數過多,我們可以考慮直接使用 Map 實現,如果參數比較少,直接傳遞參數即可。

3.2、insert 標籤

我們一般使用 insert 標籤進行插入操作,它的配置和 select 標籤差不多.

練習1:增加一個用戶

1.在 UserMapper 介面中添加對應的方法

//添加一個用戶
int addUser(User user);

2、在UserMapper.xml中添加insert語句

<insert id="addUser" parameterType="com.anti.pojo.User">
    insert into user (id,name,pwd) values (#{id},#{name},#{pwd})
</insert>

3.測試

@Test
public void testAddUser() {
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);
   User user = new User(5,"王五","zxcvbn");
   int i = mapper.addUser(user);
   System.out.println(i);
   session.commit(); //提交事務,重點!不寫的話不會提交到資料庫
   session.close();
}

注意點:增、刪、改操作需要提交事務!

3.3、update 標籤

我們一般使用update標籤進行更新操作,它的配置和select標籤差不多。

練習:修改用戶的資訊

1、同理,編寫介面方法

//修改一個用戶
int updateUser(User user);

2、編寫對應的配置文件SQL

<update id="updateUser" parameterType="com.kuang.pojo.User">
  update user set name=#{name},pwd=#{pwd} where id = #{id}
</update>

3、測試

@Test
public void testUpdateUser() {
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);
   User user = mapper.selectUserById(1);
   user.setPwd("asdfgh");
   int i = mapper.updateUser(user);
   System.out.println(i);
   session.commit(); //提交事務,重點!不寫的話不會提交到資料庫
   session.close();
}

3.4、delete 標籤

需求:根據id刪除一個用戶

1、同理,編寫介面方法

//根據id刪除用戶
int deleteUser(int id);

2、編寫對應的配置文件SQL

<delete id="deleteUser" parameterType="int">
  delete from user where id = #{id}
</delete>

3、測試

@Test
public void testDeleteUser() {
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);
   int i = mapper.deleteUser(5);
   System.out.println(i);
   session.commit(); //提交事務,重點!不寫的話不會提交到資料庫
   session.close();
}

小結:

  • 所有的 增、刪、改操作都需要提交事務!
  • 所有的普通參數,盡量都寫上 @Param 參數,尤其是多個參數時,必須寫上!
  • 有時候根據業務的需求,可以考慮使用 map 傳遞參數!
  • 為了規範操作, 在 SQL 的配置文件中,我們盡量將 parameterTyperesultType 都寫上!

3.5、模糊查詢

第1種(推薦):在 Java程式碼中添加 SQL通配符。

List<User> users = mapper.selectLikeUser("%朱%");
<select id="selectLikeUser">
	select * from user where name like #{name}
</select>

第2種(不推薦):在 SQL 語句中拼接通配符,會引起 SQL 注入。

String name = "朱";
List<User> users = mapper.selectLikeUser(name);
<select id="selectLikeUser">
	select * from user where name like "%" #{name} "%"
</select>

4、配置解析

4.1、mybatis-config.xml 核心配置文件

  • MyBatis 的配置文件包含了會深深影響 MyBatis 行為的設置和屬性資訊。
  • 能配置的內容如下:
configuration(配置)
properties(屬性)
settings(設置)
typeAliases(類型別名)
typeHandlers(類型處理器)
objectFactory(對象工廠)
plugins(插件)
environments(環境配置)
environment(環境變數)
transactionManager(事務管理器)
dataSource(數據源)
databaseIdProvider(資料庫廠商標識)
mappers(映射器)
<!-- 注意元素節點的順序!順序不對會報錯 -->

我們可以閱讀 mybatis-config.xml 上面的 dtd 的頭文件!

4.2、environments 元素

   <environments default="development">
       <environment id="development">
           <transactionManager type="JDBC"/>
           <dataSource type="POOLED">
               <property name="driver" value="com.mysql.jdbc.Driver"/>
               <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
               <property name="username" value="root"/>
               <property name="password" value="123456"/>
           </dataSource>
       </environment>
   </environments>

配置 MyBatis 的多套運行環境,將 SQL 映射到多個不同的資料庫上,必須指定其中一個為默認運行環境(通過default指定)

  • 子元素節點:environment

    • dataSource 元素使用標準的 JDBC 數據源介面來配置 JDBC 連接對象的資源。

    • 數據源是必須配置的。

    • 有三種內建的數據源類型

      type="[UNPOOLED|POOLED|JNDI]")
      
    • UNPOOLED:這個數據源的實現只是每次被請求時打開和關閉連接。

    • POOLED:這種數據源的實現利用「池」的概念將 JDBC 連接對象組織起來 , 這是一種使得並發 Web 應用快速響應請求的流行處理方式。

    • JNDI:這個數據源的實現是為了能在如 Spring 或應用伺服器這類容器中使用,容器可以集中或在外部配置數據源,然後放置一個 JNDI 上下文的引用。

    • 數據源也有很多第三方的實現,比如dbcp,c3p0,druid等等….

  • 子元素節點:transactionManager – [ 事務管理器 ]

<!-- 語法 -->
<transactionManager type="[ JDBC | MANAGED ]"/>

4.3、mappers 元素

mappers

  • 映射器 : 定義映射SQL語句文件
  • 既然 MyBatis 的行為其他元素已經配置完了,我們現在就要定義 SQL 映射語句了。但是首先我們需要告訴 MyBatis 到哪裡去找到這些語句。Java 在自動查找這方面沒有提供一個很好的方法,所以最佳的方式是告訴 MyBatis 到哪裡去找映射文件。你可以使用相對於類路徑的資源引用, 或完全限定資源定位符(包括 file:/// 的 URL),或類名和包名等。映射器是MyBatis中最核心的組件之一,在MyBatis 3之前,只支援xml映射器,即:所有的SQL語句都必須在xml文件中配置。而從MyBatis 3開始,還支援介面映射器,這種映射器方式允許以Java程式碼的方式註解定義SQL語句,非常簡潔。
<!-- 使用相對於類路徑的資源引用 -->
<mappers>
	<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定資源定位符(URL) -->
<mappers>
	<mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
<!--
使用映射器介面實現類的完全限定類名
需要配置文件名稱和介面名稱一致,並且位於同一目錄下
-->
<mappers>
	<mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
<!--
將包內的映射器介面實現全部註冊為映射器
但是需要配置文件名稱和介面名稱一致,並且位於同一目錄下
-->
<mappers>
	<package name="org.mybatis.builder"/>
</mappers>

mapper文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.mapper.UserMapper">
   
</mapper>

namespace 中文意思:命名空間,作用如下:

  • ​ namespace 的命名必須跟某個介面同名
  • ​ 介面中的方法與映射文件中 sql 語句 id 應該一 一對應

namespace 和子元素的 id 聯合保證唯一 , 區別不同的mapper

綁定 DAO 介面

namespace 命名規則 : 包名 + 類名

4.4、properties 優化

資料庫這些屬性都是可外部配置且可動態替換的,既可以在典型的 Java 屬性文件中配置,亦可通過 properties 元素的子元素來傳遞。具體的請參考官方文檔

我們來優化我們的配置文件

第一步 ; 在resources資源目錄下新建一個 db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username=root
password=123456

第二步 : 將文件導入 properties 配置文件

<configuration>
   <!--導入properties文件-->
   <properties resource="db.properties"/>

   <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="mapper/UserMapper.xml"/>
   </mappers>
</configuration>

5、typeAliases優化

類型別名是為 Java 類型設置一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減少類完全限定名的冗餘。

<!--配置別名,注意順序-->
<typeAliases>
   <typeAlias type="com.anti.pojo.User" alias="User"/>
</typeAliases>

當這樣配置時,User 可以用在任何使用 com.kuang.pojo.User 的地方。

也可以指定一個包名,MyBatis 會在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
   <package name="com.anti.pojo"/>
</typeAliases>

每一個在包 com.anti.pojo 中的 Java Bean,在沒有註解的情況下,會使用 Bean 的首字母小寫的非限定類名來作為它的別名。

若有註解,則別名為其註解值。見下面的例子:

@Alias("user")
public class User {
  ...
}

去官網查看一下Mybatis默認的一些類型別名: //mybatis.org/mybatis-3/zh/configuration.html#typeAliases

6、其他配置瀏覽

6.1、設置

設置(settings)相關 => 查看幫助文檔

懶載入

日誌實現

快取開啟關閉

一個配置完整的 settings 元素的示例如下:

<settings>
 <setting name="cacheEnabled" value="true"/>
 <setting name="lazyLoadingEnabled" value="true"/>
 <setting name="multipleResultSetsEnabled" value="true"/>
 <setting name="useColumnLabel" value="true"/>
 <setting name="useGeneratedKeys" value="false"/>
 <setting name="autoMappingBehavior" value="PARTIAL"/>
 <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
 <setting name="defaultExecutorType" value="SIMPLE"/>
 <setting name="defaultStatementTimeout" value="25"/>
 <setting name="defaultFetchSize" value="100"/>
 <setting name="safeRowBoundsEnabled" value="false"/>
 <setting name="mapUnderscoreToCamelCase" value="false"/>
 <setting name="localCacheScope" value="SESSION"/>
 <setting name="jdbcTypeForNull" value="OTHER"/>
 <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

6.2、類型處理器

  • 無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。
  • 你可以重寫類型處理器或創建你自己的類型處理器來處理不支援的或非標準的類型。【了解即可】

6.3、對象工廠

  • MyBatis 每次創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成。
  • 默認的對象工廠需要做的僅僅是實例化目標類,要麼通過默認構造方法,要麼在參數映射存在的時候通過有參構造方法來實例化。
  • 如果想覆蓋對象工廠的默認行為,則可以通過創建自己的對象工廠來實現。【了解即可】

7、生命周期和作用域

理解我們目前已經討論過的不同作用域和生命周期類是至關重要的,因為錯誤的使用會導致非常嚴重的並發問題。

我們可以先畫一個流程圖,分析一下 Mybatis 的執行過程!

image-20201205171543934

7.1、作用域理解

  • SqlSessionFactoryBuilder 的作用在於創建 SqlSessionFactory,創建成功後,SqlSessionFactoryBuilder 就失去了作用,所以它只能存在於創建 SqlSessionFactory 的方法中,而不要讓其長期存在。因此 SqlSessionFactoryBuilder 實例的最佳作用域是方法作用域(也就是局部方法變數)。
  • SqlSessionFactory 可以被認為是一個資料庫連接池,它的作用是創建 SqlSession 介面對象。因為 MyBatis 的本質就是 Java 對資料庫的操作,所以 SqlSessionFactory 的生命周期存在於整個 MyBatis 的應用之中,所以一旦創建了 SqlSessionFactory,就要長期保存它,直至不再使用 MyBatis 應用,所以可以認為 SqlSessionFactory 的生命周期就等同於 MyBatis 的應用周期。
  • 由於 SqlSessionFactory 是一個對資料庫的連接池,所以它佔據著資料庫的連接資源。如果創建多個 SqlSessionFactory,那麼就存在多個資料庫連接池,這樣不利於對資料庫資源的控制,也會導致資料庫連接資源被消耗光,出現系統宕機等情況,所以盡量避免發生這樣的情況。
  • 因此在一般的應用中我們往往希望 SqlSessionFactory 作為一個單例,讓它在應用中被共享。所以說 SqlSessionFactory 的最佳作用域是應用作用域。
  • 如果說 SqlSessionFactory 相當於資料庫連接池,那麼 SqlSession 就相當於一個資料庫連接(Connection 對象),你可以在一個事務裡面執行多條 SQL,然後通過它的 commit、rollback 等方法,提交或者回滾事務。所以它應該存活在一個業務請求中,處理完整個請求後,應該關閉這條連接,讓它歸還給 SqlSessionFactory,否則資料庫資源就很快被耗費精光,系統就會癱瘓,所以用 try…catch…finally… 語句來保證其正確關閉。
  • 所以 SqlSession 的最佳的作用域是請求或方法作用域。

image-20201205171555725

8、ReusltMap

8.1、在進行簡單查詢時,查詢出來的值為 null

image-20201206133705016

可以看到查詢出來的結果集中 username 屬性為 null

8.2、原因分析

MyBatis 會根據這些查詢的列名(會將列名轉化為小寫,資料庫不區分大小寫) , 去對應的實體類中查找相應列名的 set方法 設值 , 由於找不到 setUsernmae() , 所以 username 返回 null ; 【自動映射】

image-20201206133911746

8.3、解決方法

1.【不推薦】修改實體類的屬性名,使其和資料庫欄位名一致。

image-20201206134056441

2.【不推薦】在 SQL 語句中使用別名對應實體類中的屬性名。

image-20201206134740333

3.【推薦】使用在 xxMapper.xml` 中使用 ResultMap 進行結果集映射。

image-20201206135146896

8.4、資料庫中,存在一對多,多對一的情況時

【多對一】的處理:

  • 多個學生對應一個老師
  • 如果對於學生這邊,就是一個多對一的現象,即從學生這邊關聯一個老師。

image-20201206135928746

image-20201206141735576

image-20201206141721977

image-20201206141754594

【一對多】的處理:

image-20201206142906498

image-20201206142852540

image-20201206142917246

8.5、 總結:

1、關聯 – association

2、集合 – collection

3、所以 association 是用於一對一和多對一,而 collection 是用於一對多的關係

4、JavaType ofType 都是用來指定對象類型的

  • JavaType 是用來指定 pojo 中屬性的類型
  • ofType指定的是映射到 List 集合屬性中 pojo 的類型。

注意說明:

1、保證SQL的可讀性,盡量通俗易懂

2、根據實際要求,盡量編寫性能更高的SQL語句

3、注意屬性名和欄位不一致的問題

4、注意一對多和多對一 中:欄位和屬性對應的問題

5、盡量使用Log4j,通過日誌來查看自己的錯誤

9、 Log日誌

9.1、思考

我們在測試 SQL 的時候,要是能夠在控制台輸出 SQL 的話,是不是就能夠有更快的排錯效率?

如果一個 資料庫相關的操作出現了問題,我們可以根據輸出的 SQL 語句快速排查問題。

對於以往的開發過程,我們會經常使用到debug模式來調節,跟蹤我們的程式碼執行過程。但是現在使用 Mybatis 是基於介面,配置文件的源程式碼執行過程。因此,我們必須選擇日誌工具來作為我們開發,調節程式的工具。

Mybatis內置的日誌工廠提供日誌功能,具體的日誌實現有以下幾種工具:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

具體選擇哪個日誌實現工具由 MyBatis 的內置日誌工廠確定。它會使用最先找到的(按上文列舉的順序查找)。如果一個都未找到,日誌功能就會被禁用。

9.2、標準日誌實現

指定 MyBatis 應該使用哪個日誌記錄實現。如果此設置不存在,則會自動發現日誌記錄實現。

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

測試,可以看到控制台有大量的輸出!我們可以通過這些輸出來判斷程式到底哪裡出了Bug

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
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.
Created connection 355115154.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@152aa092]
==>  Preparing: select * from teacher where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name
<==        Row: 1, 秦老師
====>  Preparing: select * from student 
====> Parameters: 
<====    Columns: id, name, tid
<====        Row: 1, 小明, 1
<====        Row: 2, 小紅, 1
<====        Row: 3, 小張, 1
<====        Row: 4, 小李, 1
<====        Row: 5, 小王, 1
<====      Total: 5
<==      Total: 1
Teacher(id=null, name=秦老師, students=[Student(id=1, name=小明), Student(id=2, name=小紅), Student(id=3, name=小張), Student(id=4, name=小李), Student(id=5, name=小王)])

9.3、使用 Log4j

簡介:

  • Log4j是Apache的一個開源項目
  • 通過使用Log4j,我們可以控制日誌資訊輸送的目的地:控制台,文本,GUI組件….
  • 我們也可以控制每一條日誌的輸出格式;
  • 通過定義每一條日誌資訊的級別,我們能夠更加細緻地控制日誌的生成過程。最令人感興趣的就是,這些可以通過一個配置文件來靈活地進行配置,而不需要修改應用的程式碼。

使用步驟:

1、導入log4j的包

<dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
</dependency>

2、配置文件編寫

#將等級為DEBUG的日誌資訊輸出到console和file這兩個目的地,console和file的定義在下面的程式碼
log4j.rootLogger=DEBUG,console,file

#控制台輸出的相關設置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件輸出的相關設置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%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

3、setting設置日誌實現

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

輸出結果:

[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 71706941.
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@446293d]
[com.anti.dao.TeacherMapper.getTeacherById]-==>  Preparing: select * from teacher where id = ? 
[com.anti.dao.TeacherMapper.getTeacherById]-==> Parameters: 1(Integer)
[com.anti.dao.TeacherMapper.student]-====>  Preparing: select * from student 
[com.anti.dao.TeacherMapper.student]-====> Parameters: 
[com.anti.dao.TeacherMapper.student]-<====      Total: 5
[com.anti.dao.TeacherMapper.getTeacherById]-<==      Total: 1
Teacher(id=null, name=秦老師, students=[Student(id=1, name=小明), Student(id=2, name=小紅), Student(id=3, name=小張), Student(id=4, name=小李), Student(id=5, name=小王)])

image-20201206145306565

10、分頁

在學習 MyBatis 等持久層框架的時候,會經常對數據進行增刪改查操作,使用最多的是對資料庫進行查詢操作,如果查詢大量數據的時候,我們往往使用分頁進行查詢,也就是每次處理小部分數據,這樣對資料庫壓力就在可控範圍內。

使用Limit實現分頁

#語法
SELECT * FROM table LIMIT page,pageSize

page = 當前頁數 = (page-1)*pageSize
pageSize = 一頁多少條數據

UserMapper.java

public interface UserMapper {
    //選擇全部用戶實現分頁
    List<User> selectUser(Map<String,Integer> map);
}

UserMapper.xml

<select id="selectUser" parameterType="map" resultType="com.anti.pojo.User">
    select * from user limit #{page},#{pageSize}
</select>

MyTest.java

@Test
public void test01(){
    SqlSession session = MybatisUtils.getSession();
    UserMapper mapper = session.getMapper(UserMapper.class);

    HashMap<String, Integer> map = new HashMap<String, Integer>();
    int page = 1; //第幾頁
    int limit = 10; //每頁多少數據
    map.put("page",(page-1) * limit);
    map.put("limit",limit);

    List<User> users = mapper.selectUser(map);
    for (User user : users) {
        System.out.println(user);
    }
    session.close();
}

結果 :

==>  Preparing: select * from user limit ?,? 
==> Parameters: 0(Integer), 10(Integer)
<==    Columns: uid, name, gender, birthday, dept, cno, address, phone, remark, password, type
<==        Row: 999, 管理員, M, 2020/09/02, AAA, 0, AAA, null, null, 123, 0
<==        Row: 10101, 怡香, M, 2020/02/08, Accounting, 1, Cameroon, 823-954-4217, null, 1, 1
<==        Row: 10102, 唯楓, M, 2020/01/25, Accounting, 2, Palestinian Territory, 978-827-9275, null, 2, 1
<==        Row: 10103, 海程, M, 2020/09/05, Human Resources, 3, Thailand, 978-712-9955, null, 3, 1
<==        Row: 10104, 琪煜, M, 2020/10/07, Accounting, 4, Palestinian Territory, 730-153-0025, null, 4, 1
<==        Row: 10105, 彥軍, F, 2020/11/05, Services, 5, China, 504-460-1356, null, 5, 1
<==        Row: 10106, 宇涵, F, 2020/11/08, Product Management, 6, Argentina, 252-143-6848, null, 6, 1
<==        Row: 10107, 辰華, M, 2019/11/25, Business Development, 7, Philippines, 884-928-7856, null, 7, 1
<==        Row: 10108, 曉烽, M, 2020/08/05, Engineering, 8, Philippines, 152-366-5638, null, 8, 1
<==        Row: 10109, 尹智, F, 2020/01/12, Human Resources, 9, Argentina, 803-602-3704, null, 9, 1
<==      Total: 10

11、使用註解開發

MyBatis最初配置資訊是基於 XML ,映射語句(SQL)也是定義在 XML 中的。而到MyBatis 3提供了新的基於註解的配置。不幸的是,java 註解的的表達力和靈活性十分有限。

最強大的 MyBatis 映射並不能用註解來構建

  • 註解主要分成 :
    • @select ()
    • @update ()
    • @Insert ()
    • @delete ()

注意:利用註解開發就不需要mapper.xml映射文件了 .

1.在介面中添加註解

public interface UserMapper {
	//查詢全部用戶
    @Select("select * from user")
    List<User> getUsers();
}

2.在mybatis的核心配置文件中注入

<!--使用class綁定介面-->
<mappers>
   <mapper class="com.anti.mapper.UserMapper"/>
</mappers>

3.測試

@Test
public void test01(){
    UserMapper mapper = MybatisUtils.getSession().getMapper(UserMapper.class);
    List<User> users = mapper.getUsers();
    for (User user : users) {
        System.out.println(user);
    }
}

4.結果

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3c0be339]
==>  Preparing: select * from user 
==> Parameters: 
<==    Columns: id, name, pwd
<==        Row: 1, 狂神, 123456
<==        Row: 2, 張三, abcdef
<==        Row: 3, 李四, 987654
<==        Row: 5, 王五, zxcvbn
<==      Total: 4
User(id=1, name=狂神, pwd=123456)
User(id=2, name=張三, pwd=abcdef)
User(id=3, name=李四, pwd=987654)
User(id=5, name=王五, pwd=zxcvbn)

Process finished with exit code 0

12、動態SQL

12.1、if 語句

<!--需求1:
根據作者名字和部落格名字來查詢部落格!
如果作者名字為空,那麼只根據部落格名字查詢,反之,則根據作者名來查詢
select * from blog where title = #{title} and author = #{author}
-->
<select id="queryBlogIf" parameterType="map" resultType="blog">
  select * from blog where
   <if test="title != null">
      title = #{title}
   </if>
   <if test="author != null">
      and author = #{author}
   </if>
</select>

這樣寫我們可以看到,如果 author 等於 null,那麼查詢語句為 select * from user where title=#{title},但是如果 title 為空呢?那麼查詢語句為 select * from user where and author=#{author},這是錯誤的 SQL 語句,如何解決呢?請看下面的 where 語句!

12.2、where 語句

<select id="queryBlogIf" parameterType="map" resultType="blog">
  select * from blog
   <where>
       <if test="title != null">
          title = #{title}
       </if>
       <if test="author != null">
          and author = #{author}
       </if>
   </where>
</select>

這個「where」標籤會知道如果它包含的標籤中有返回值的話,它就插入一個『where』。此外,如果標籤返回的內容是以AND 或OR 開頭的,則它會剔除掉。

12.3、set 語句

同理,上面的對於查詢 SQL 語句包含 where 關鍵字,如果在進行更新操作的時候,含有 set 關鍵詞,我們怎麼處理呢?

<!--注意set是用的逗號隔開-->
<update id="updateBlog" parameterType="map">
  update blog
     <set>
         <if test="title != null">
            title = #{title},
         </if>
         <if test="author != null">
            author = #{author}
         </if>
     </set>
  where id = #{id};
</update>

12.4、choose 語句

有時候,我們不想用到所有的查詢條件,只想選擇其中的一個,查詢條件有一個滿足即可,使用 choose 標籤可以解決此類問題,類似於 Java 的 switch 語句。

choose 與 if 的區別:

  • if 如果條件都滿足會帶出所有滿足條件的語句
  • choose 只會帶出最先滿足的條件的語句
  • choose 如果沒有滿足的條件會執行 otherwise 語句
<select id="queryBlogChoose" parameterType="map" resultType="blog">
  select * from blog
   <where>
       <choose>
           <when test="title != null">
                title = #{title}
           </when>
           <when test="author != null">
              and author = #{author}
           </when>
           <otherwise>
              and views = #{views}
           </otherwise>
       </choose>
   </where>
</select>

12.5、SQL 片段

有時候可能某個 sql 語句我們用的特別多,為了增加程式碼的重用性,簡化程式碼,我們需要將這些程式碼抽取出來,然後使用時直接調用。

提取SQL片段:

<sql id="if-title-author">
   <if test="title != null">
      title = #{title}
   </if>
   <if test="author != null">
      and author = #{author}
   </if>
</sql>

引用SQL片段:

<select id="queryBlogIf" parameterType="map" resultType="blog">
  select * from blog
   <where>
       <!-- 引用 sql 片段,如果refid 指定的不在本文件中,那麼需要在前面加上 namespace -->
       <include refid="if-title-author"></include>
       <!-- 在這裡還可以引用其他的 sql 片段 -->
   </where>
</select>

注意:

  1. 最好基於 單表來定義 sql 片段,提高片段的可重用性
  2. 在 sql 片段中不要包括 where

12.6、foreach 語句

需求:我們需要查詢 blog 表中 id 分別為1,2,3的部落格資訊

<select id="queryBlogForeach" parameterType="map" resultType="blog">
  select * from blog
   <where>
       <!--
       collection:指定輸入對象中的集合屬性
       item:每次遍歷生成的對象
       open:開始遍歷時的拼接字元串
       close:結束時拼接的字元串
       separator:遍歷對象之間需要拼接的字元串
       select * from blog where 1=1 and (id=1 or id=2 or id=3)
     -->
       <foreach collection="ids"  item="id" open="and (" close=")" separator="or">
          id=#{id}
       </foreach>
   </where>
</select>

小結:

其實動態 sql 語句的編寫往往就是一個拼接的問題,為了保證拼接準確,我們最好首先要寫原生的 sql 語句出來,然後在通過 mybatis 動態sql 對照著改,防止出錯。多在實踐中使用才是熟練掌握它的技巧。

13、快取

1、什麼是快取 [ Cache ]?

  • 存在記憶體中的臨時數據。
  • 將用戶經常查詢的數據放在快取(記憶體)中,用戶去查詢數據就不用從磁碟上(關係型資料庫數據文件)查詢,從快取中查詢,從而提高查詢效率,解決了高並發系統的性能問題。

2、為什麼使用快取?

  • 減少和資料庫的交互次數,減少系統開銷,提高系統效率。

3、什麼樣的數據能使用快取?

  • 經常查詢並且不經常改變的數據。

13.1、Mybatis快取

  • MyBatis包含一個非常強大的查詢快取特性,它可以非常方便地訂製和配置快取。快取可以極大的提升查詢效率。

  • MyBatis系統中默認定義了兩級快取:一級快取二級快取

    • 默認情況下,只有一級快取開啟。(SqlSession級別的快取,也稱為本地快取)

    • 二級快取需要手動開啟和配置,他是基於namespace級別的快取。

    • 為了提高擴展性,MyBatis定義了快取介面Cache。我們可以通過實現Cache介面來自定義二級快取

13.2、一級快取

一級快取也叫本地快取:

  • 與資料庫同一次會話期間查詢到的數據會放在本地快取中。

  • 以後如果需要獲取相同的數據,直接從快取中拿,沒必須再去查詢資料庫;

1.在mybatis中加入日誌,方便測試結果

2.編寫介面方法

//根據id查詢用戶
User queryUserById(@Param("id") int id);

3.介面對應的Mapper文件

<select id="queryUserById" resultType="user">
  select * from user where id = #{id}
</select>

4.測試

@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);
   User user2 = mapper.queryUserById(1);
   System.out.println(user2);
   System.out.println(user==user2);

   session.close();
}

5.結果分析

image-20201206213629212

13.3、一級快取失效的四種情況:

一級快取是SqlSession級別的快取,是一直開啟的,我們關閉不了它;

一級快取失效情況:沒有使用到當前的一級快取,效果就是,還需要再向資料庫中發起一次查詢請求!

1.sqlSession不同

@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   SqlSession session2 = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);
   UserMapper mapper2 = session2.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);
   User user2 = mapper2.queryUserById(1);
   System.out.println(user2);
   System.out.println(user==user2);

   session.close();
   session2.close();
}

觀察結果:發現發送了兩條SQL語句!

結論:每個sqlSession中的快取相互獨立

2.sqlSession相同,查詢條件不同

@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);
   UserMapper mapper2 = session.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);
   User user2 = mapper2.queryUserById(2);
   System.out.println(user2);
   System.out.println(user==user2);

   session.close();
}

觀察結果:發現發送了兩條SQL語句!很正常的理解

結論:當前快取中,不存在這個數據

3.sqlSession相同,兩次查詢之間執行了增刪改操作!

@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);

   HashMap map = new HashMap();
   map.put("name","kuangshen");
   map.put("id",4);
   mapper.updateUser(map);

   User user2 = mapper.queryUserById(1);
   System.out.println(user2);

   System.out.println(user==user2);

   session.close();
}

觀察結果:查詢在中間執行了增刪改操作後,重新執行了

結論:因為增刪改操作可能會對當前數據產生影響

4.sqlSession相同,手動清除一級快取

@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   UserMapper mapper = session.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);

   session.clearCache();//手動清除快取

   User user2 = mapper.queryUserById(1);
   System.out.println(user2);

   System.out.println(user==user2);

   session.close();
}

一級快取就是一個map

13.4、二級快取

1.開啟全局快取 【mybatis-config.xml】

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

2.去每個mapper.xml中配置使用二級快取,這個配置非常簡單;【xxxMapper.xml】

<cache/>

官方示例=====>查看官方文檔
<cache
 eviction="FIFO"
 flushInterval="60000"
 size="512"
 readOnly="true"
/>
這個更高級的配置創建了一個 FIFO 快取,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認為是只讀的,因此對它們進行修改可能會在不同執行緒中的調用者產生衝突。

3.程式碼測試

  • 所有的實體類先實現序列化介面
  • 測試程式碼
@Test
public void testQueryUserById(){
   SqlSession session = MybatisUtils.getSession();
   SqlSession session2 = MybatisUtils.getSession();

   UserMapper mapper = session.getMapper(UserMapper.class);
   UserMapper mapper2 = session2.getMapper(UserMapper.class);

   User user = mapper.queryUserById(1);
   System.out.println(user);
   session.close();

   User user2 = mapper2.queryUserById(1);
   System.out.println(user2);
   System.out.println(user==user2);

   session2.close();
}

image-20201206214729672

結論:

  • 只要開啟了二級快取,我們在同一個Mapper中的查詢,可以在二級快取中拿到數據
  • 查出的數據都會被默認先放在一級快取中
  • 只有會話提交或者關閉以後,一級快取中的數據才會轉到二級快取中

快取原理圖:

image-20201206214908158

13.5、第三方快取實現 EhCache

image-20210130005613005

Ehcache是一種廣泛使用的 java 分散式快取,用於通用快取;

1.要在應用程式中使用Ehcache,需要引入依賴的 jar 包

<!-- //mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-ehcache</artifactId>
   <version>1.1.0</version>
</dependency>

2.在mapper.xml中使用對應的快取即可

<mapper namespace = 「org.acme.FooMapper」 >
   <cache type = 「org.mybatis.caches.ehcache.EhcacheCache」 />
</mapper>

3.編寫ehcache.xml文件,如果在載入時未找到/ehcache.xml資源或出現問題,則將使用默認配置。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="//ehcache.org/ehcache.xsd"
        updateCheck="false">
   <!--
      diskStore:為快取路徑,ehcache分為記憶體和磁碟兩級,此屬性定義磁碟的快取位置。參數解釋如下:
      user.home – 用戶主目錄
      user.dir – 用戶當前工作目錄
      java.io.tmpdir – 默認臨時文件路徑
    -->
   <diskStore path="./tmpdir/Tmp_EhCache"/>
   
   <defaultCache
           eternal="false"
           maxElementsInMemory="10000"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="1800"
           timeToLiveSeconds="259200"
           memoryStoreEvictionPolicy="LRU"/>

   <cache
           name="cloud_user"
           eternal="false"
           maxElementsInMemory="5000"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="1800"
           timeToLiveSeconds="1800"
           memoryStoreEvictionPolicy="LRU"/>
   <!--
      defaultCache:默認快取策略,當ehcache找不到定義的快取時,則使用這個快取策略。只能定義一個。
    -->
   <!--
     name:快取名稱。
     maxElementsInMemory:快取最大數目
     maxElementsOnDisk:硬碟最大快取個數。
     eternal:對象是否永久有效,一但設置了,timeout將不起作用。
     overflowToDisk:是否保存到磁碟,當系統當機時
     timeToIdleSeconds:設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閑置時間無窮大。
     timeToLiveSeconds:設置對象在失效前允許存活時間(單位:秒)。最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0.,也就是對象存活時間無窮大。
     diskPersistent:是否快取虛擬機重啟期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
     diskSpoolBufferSizeMB:這個參數設置DiskStore(磁碟快取)的快取區大小。默認是30MB。每個Cache都應該有自己的一個緩衝區。
     diskExpiryThreadIntervalSeconds:磁碟失效執行緒運行時間間隔,默認是120秒。
     memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理記憶體。默認策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。
     clearOnFlush:記憶體數量最大時是否清除。
     memoryStoreEvictionPolicy:可選策略有:LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)。
     FIFO,first in first out,這個是大家最熟的,先進先出。
     LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,快取的元素有一個hit屬性,hit值最小的將會被清出快取。
     LRU,Least Recently Used,最近最少使用的,快取的元素有一個時間戳,當快取容量滿了,而又需要騰出地方來快取新的元素的時候,那麼現有快取元素中時間戳離當前時間最遠的元素將被清出快取。
  -->

</ehcache>

14、Mybatis詳細的執行流程

img