MyBatis技術原理淺析:使用MyBatis+Druid連接MySQL數據庫

(尊重勞動成果,轉載請註明出處:https://yangwenqiang.blog.csdn.net/article/details/95763605冷血之心的博客)

關注微信公衆號(文強的技術小屋),學習更多技術知識,一起遨遊知識海洋~

目錄

前言:

正文:

MyBatis是什麼?

MyBatis的核心組件:

SqlSessionFactoryBuilder:

SqlSessionFactory:

SqlSession:

Mapper:

Demo展示:使用MyBatis+Druid連接MySQL數據庫

數據庫中表的建立:

項目MyBatisTest建立

(1)User實體類:

(2)自定義的Druid數據庫連接池MyDruidDataSourceFactory :

(3)映射器的定義:

(4)MyBatis配置文件mybatis_config.xml文件:

(5)依賴配置pom.xml

(6)測試類Test.java

項目測試結果:

MyBatis運行原理總結:

創建SqlSessionFactory的過程

映射器的工作原理:

映射器Mapper文件的內部組成:

Mapper 接口的工作原理是JDK動態代理:

Dao接口中的方法重載?

不同的映射文件xml中的id值可以重複嗎?

Mapper中常見的標籤:

Mapper文件中的反射技術:

MyBatis中SqlSession的運行原理總結:

SqlSession的運行過程解析:

總結:

SqlSession下的四大對象:

SqlSession運行過程總結:

Other Question:

Mybatis中 # 和 $ 的區別:(能用#號就不要用$符號)

Mybatis的一級、二級緩存:

MyBatis的接口綁定?有哪些實現方式?

結束語:


前言:

        說到MyBatis,大部分的Java開發人員都略懂一二,即便僅僅是聽過該框架,並且簡單知道該框架的作用,也敢大言不慚的在簡歷上寫 “熟悉持久層框架MyBatis等”。結果,面試的時候又是一問三不知,問啥啥不會,實在是有點小尷尬。爲了可以更好學習MyBatis的技術原理,博主在此和大家一起對MyBatis的運行技術原理做一個簡單的總結。

正文:

MyBatis是什麼?

在這裏,我們先給出一個MyBatis的概念,接下來會對齊概念中所涉及到的特性進行介紹。

  • Mybatis是一個半ORM(對象關係映射)框架,它內部封裝了JDBC,開發時只需要關注SQL語句本身,不需要花費精力去處理加載驅動、創建連接、創建statement等繁雜的過程。程序員直接編寫原生態sql,可以嚴格控制sql執行性能,靈活度高(支持動態SQL語句)。
  • MyBatis 使用 XML 或註解來配置和映射原生信息,將 POJO映射成數據庫中的記錄,避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集。
  • 通過xml 文件或註解的方式將要執行的各種 statement 配置起來,並通過java對象和 statement中sql的動態參數進行映射生成最終執行的sql語句,最後由Mybatis框架執行sql並將結果映射爲Java對象並返回。(從執行sql到返回result的過程)。

       Mybatis相對於直接使用JDBC,具有簡單易上手,開發速度快,面向對象和數據庫可移植的優勢。那麼MyBatis相對於傳統的持久層框架Hibernate如何呢?

       我們知道,Hibernate是一種完全面向對象,並且對象和數據庫是一種全表映射關係,使用了HQL語句來避免了原生的MySQL語句的編寫。但是Hibernate存在無法靈活使用原生的SQL語句,全表映射效率低下,並且存在N+1的問題。MyBatis框架對對象關係映射做出了改進,並且支持動態SQL,可以更加靈活的對SQL語句進行優化,大大提高了其執行效率,逐漸稱爲了持久層框架的主流選擇。

MyBatis的核心組件:

       MyBatis中有如下四大核心組件,接下來我們一一介紹。

SqlSessionFactoryBuilder:

       SqlSessionFactoryBuilder的作用是一個構建器,通過XML配置文件或者Java編碼獲得資源來構建SqlSessionFactory,通過Builder可以構建多個SessionFactory。其生命週期一般只存在於方法的局部,用完即可回收。一般通過如下語句建立:

SqlSessionFactory  factory  = SqlSessionFactoryBuilder.build(input stream);

SqlSessionFactory:

        SqlSessionFactory的作用就是創建SqlSession,也就是創建一個會話。每次程序需要訪問數據庫,就需要使用到SqlSession。所以SqlSessionFactory應該在Mybatis應用的整個生命週期中。爲了減少每次都創建一個會話帶來的資源消耗,一般情況下都會使用單例模式來創建SqlSession。創建方式如下:

SqlSession sqlSession = SqlSessionFactory.openSession();

SqlSession:

       SqlSession就是一個會話,相當於JDBC中的Connection對象,既可以發送SQL去執行並返回結果,也可以獲取Mapper接口。SqlSession是一個享存不安全的對象,其生命週期應該是請求數據庫處理事務的過程中。每次創建的SqlSession對象必須及時關閉,或者會使得數據庫連接池的活動資源減少,影響系統性能。

Mapper:

        Mapper也叫做映射器,由Java接口和XML文件(或者是註解)共同組成,給出了對應的SQL和映射規則,主要負責發送SQL去執行,並且返回結果。通過下邊的語句來獲取Mapper,接下來就可以調用Mapper中的各個方法去獲取數據庫結果了。

XXMapper xxMapper = sqlSession.getMapper(XXMapper.class);

       介紹了MyBatis的四大核心組件,我們先來一個Demo進行一個項目展示吧。由於MyBatis是一個持久層框架,所以經常和Spring MVC等Web層框架搭配使用。但是,其實MyBatis其實是可以獨立使用的,我們在一個普通的非Web類型的Java程序中即可使用,因爲說白了,MyBatis就是對JDBC進行了一個封裝而已。

Demo展示:使用MyBatis+Druid連接MySQL數據庫

數據庫中表的建立:

建立一張測試表mybatis_test,語句如下:

create table mybatis_test(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

插入一條記錄,語句如下:

 insert into mybatis_test values(1,1,'zhangsan');

項目MyBatisTest建立

目錄結構圖如下:

下邊我們依次給出重要代碼與配置:

(1)User實體類:

package com.model;

public class User {
    private int id;
    private int k;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getK() {
        return k;
    }

    public void setK(int k) {
        this.k = k;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", k=" + k +
                ", name='" + name + '\'' +
                '}';
    }
}

(2)自定義的Druid數據庫連接池MyDruidDataSourceFactory :

package com.factory;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.apache.ibatis.datasource.DataSourceFactory;
import javax.sql.DataSource;
import java.util.Properties;

public class MyDruidDataSourceFactory implements DataSourceFactory {
    private DataSource dataSource;

    public DataSource getDataSource() {
        return this.dataSource;
    }

    public void setProperties(final Properties props) {
        try {
            this.dataSource = DruidDataSourceFactory.createDataSource(props);
        } catch (final RuntimeException e) {
            throw e;
        } catch (final Exception e) {
            throw new RuntimeException("init datasource error", e);
        }
    }
}

(3)映射器的定義:

UserMapper接口的定義:

package com.mapper;
import com.model.User;

public interface UserMapper {
    public User getUserInfo(int id);
}

UserMapper.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.mapper.UserMapper">

    <select id="getUserInfo" parameterType="int" resultType="User">
         select * from mybatis_test where id=#{id}
    </select>

</mapper>

(4)MyBatis配置文件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>
    
    <!--定義別名-->
    <typeAliases>
        <typeAlias type="com.model.User" alias="User" />
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="com.factory.MyDruidDataSourceFactory">
                <property name="driver" value="com.mysql.cj.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/test" />
                <property name="username" value="root" />
                <property name="password" value="123" />
                <property name="maxActive" value="30" />  <!--接下來是Druid連接池的屬性配置-->
                <property name="initialSize" value="30" />
                <property name="minIdle" value="30" />
                <property name="testWhileIdle" value="false" />
            </dataSource>
        </environment>
    </environments>

    <!--引入定義好的UserMapper文件-->
    <mappers>
        <mapper resource="UserMapper.xml" />
    </mappers>

</configuration>

(5)依賴配置pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ywq</groupId>
    <artifactId>mybatis.test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!--MyBatis依賴-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.0</version>
        </dependency>
        <!--阿里巴巴Druid依賴-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--MySQL相關依賴-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.28</version>
        </dependency>
    </dependencies>

</project>

此處爲什麼使用比較低的mysql連接版本5.1.28呢?因爲最新版本的8.0.16會有時區的限制,報錯如下:

java.sql.SQLException: The server time zone value '?й???????' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.

(6)測試類Test.java

package com;
import java.io.IOException;
import java.io.Reader;

import com.mapper.UserMapper;
import com.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;

public class Test {
    private static SqlSessionFactory factory;

    public static void main(String[] args) {
        initMyBatis();
        SqlSession session = factory.openSession();
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User userInfo = userMapper.getUserInfo(1);
        session.commit();
        System.out.println(userInfo);
        session.close();
    }

    /**
     * 初始化建立SqlSessionFactory
     */
    private static void initMyBatis() {
        String resource = "mybatis_config.xml";
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader(resource);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        factory = new SqlSessionFactoryBuilder().build(reader);
    }

}

項目測試結果:

      測試結果輸出如下圖所示:

 

接下來,我們繼續學習總結MyBatis的運行原理。

MyBatis運行原理總結:

創建SqlSessionFactory的過程

      我們先來看一下Test測試類中創建SqlSessionFactory的過程,通過讀取xml配置,然後使用SqlSessionFactorybuilder來build一個SqlSessionFactory。具體創建過程的步驟如下:

  1. 通過XMLConfigBuilder解析配置的XML文件,讀出配置參數,封裝到Configuration類中。

  2. 使用Configuration對象去創建SqlSessionFactory,SqlSessionFactory在MyBatis中是一個接口而不是一個類,所以提供了一個其默認實現類DefaultSqlSessionFactory

  3. 創建SqlSessionFactory的本質是一種Builder模式。

  4. Configuration類的作用:

    1. 讀入配置文件。

    2. 初始化基礎配置:

      1. 包括全局配置,別名,類型處理器,環境數據庫標識以及Mapper映射器等的初始化配置。

    3. 提供單例。

    4. 執行一些重要的對象方法和初始化配置信息。

映射器的工作原理:

        前面我們有介紹,映射器Mapper是四大核心組件之一,包括接口+xml文件兩個部分。既然只有接口,那麼映射器是如何運行的呢?

映射器Mapper文件的內部組成:

 一個映射器Mapper文件一般由以下的的三個部分組成:

  • MapperStatement:(MapperStatement)
    • 保存映射器的一個節點(select/insert/delete/ipdate)
  • SqlSource:
    • 是提供BoundSql對象的地方,是MappedStatement的一個屬性。
  • BoundSql:
    • 建立SQL和參數的地方,有3個屬性,即SQL,parameterObject,parameterMappings 
      • SQL屬性就是我們映射器中的一條SQL語句。
      • 後邊兩個屬性是對傳入參數進行規範與約束的。

        我們知道每一個Dao接口即是Mapper接口,接口的全限定名和xml映射文件中的namespace(命名空間) 唯一對應。接口中的方法名就是映射文件中Mapper的Statement的id屬性值。接口方法中的參數就是傳遞給映射文件中SQL的參數。

在Mybatis中,每一個<select>、<insert>、<update>、<delete>標籤,都會被解析爲一個MapperStatement對象。

       Mapper接口是沒有實現類的,當調用接口方法時,接口全限名+方法名拼接字符串作爲key值,可唯一定位一個MapperStatement。

  • 舉例:com.mapper.UserMapper.getUserInfo,可以唯一找到namespace爲com.mapper.UserMapper下面 id 爲 getUserInfo的 MapperStatement。

Mapper 接口的工作原理是JDK動態代理:

Mybatis運行時會使用JDK動態代理爲Mapper接口生成代理對象proxy,代理對象會攔截接口方法,轉而執行MapperStatement所代表的sql,然後將sql執行結果返回。

Dao接口中的方法重載?

那麼問題來了,當Dao接口中的方法,參數不同時,可以構成重載嗎?我們可以先來溫習下何爲重載?

何爲重載

  • 重載是指一個類裏面(包括父類的方法)存在方法名相同,但是參數不一樣的方法,參數不一樣可以是不同的參數個數、類型或順序
  • 如果僅僅是修飾符、返回值、throw的異常 不同,那麼這是2個相同的方法

瞭解了映射器的工作原理之後,我們應該可以得出下邊的結論:

 Mapper接口裏的方法,是不能重載的,因爲是使用 全限名+方法名 的保存和尋找策略。

我們繼續來看下邊的的問題。

不同的映射文件xml中的id值可以重複嗎?

前面我們說了,MyBatis使用全限名+方法名來尋找對應的MapperStatement,那麼看起來我們是不可以定義相同的id值的。

  • 不同的Xml映射文件,如果配置了namespace,那麼id可以重複;
  • 如果沒有配置namespace,那麼id不能重複;

原因:

  • namespace+id是作爲Map<String, MapperStatement>的key使用的,如果沒有namespace,就剩下id,那麼,id重複會導致數據互相覆蓋。有了namespace,自然id就可以重複,namespace不同,namespace+id自然也就不同。 

Mapper中常見的標籤:

  1. select | insert | updae | delete

  2. <resultMap>、<parameterMap>、<sql>、<include>、<selectKey>

  3. trim | where | set | foreach | if | choose | when | otherwise | bind   (動態SQL語句相關標籤)

我們來看下邊這兩個和標籤相關的問題。

resultMap和resultType的差別:

  • 兩者都是表示查詢結果集與java對象之間的一種關係,處理查詢結果集,映射到java對象。
  • resultMap:
    • 表示將查詢結果集中的列一一映射到bean對象的各個屬性。
  • resutType:
    • 表示的是bean中的對象類,此時可以省略掉resultMap標籤的映射,但是必須保證查詢結果集中的屬性 和 bean對象類中的屬性是一一對應的,此時大小寫不敏感,但是有限制。

parameterMap和parameterType的差別:

  • parameterMap:
    • 與resultMap方法類似,表示將查詢結果集中列值的類型一一映射到java對象屬性的類型上,在開發過程中不推薦這種方式。 
  • parameterType: 
    • parameterType直接將查詢結果列值類型自動對應到java對象屬性類型上,不再配置映射關係一一對應。 

Mapper文件中的反射技術:

Mybatis是如何將sql執行結果封裝爲目標對象並返回的?都有哪些映射形式?

  • 第一種是使用<resultMap>標籤,逐一定義數據庫列名和對象屬性名之間的映射關係。
  • 第二種是使用sql列的別名功能,將列的別名書寫爲對象屬性名。
  • 有了列名與屬性名的映射關係後,Mybatis通過反射創建對象,同時使用反射給對象的屬性逐一賦值並返回,那些找不到映射關係的屬性,是無法完成賦值的。

MyBatis中SqlSession的運行原理總結:

接下來,我們對SqlSession的運行原理做一個簡單的總結。

SqlSession的運行過程解析:

  • 映射器的動態代理:
    • 動態代理對接口的綁定,它的作用就是生成動態代理對象(佔位),而代理的方法則被放入了MapperProxy類中。
  • 爲什麼MyBatis中只用Mapper接口就可以運行SQL?
    • 映射器的XML文件的命名空間對應的便是這個接口的全路徑,根據全路徑和方法名便能夠綁定起來,通過動態代理技術,讓這個接口跑起來。
    • 然後採用命令模式,最後使用SqlSession接口的方法使得它能夠執行查詢。

總結:

映射器其實就是一個動態代理對象,進入到了MapperMethod的execute方法,然後就進入了SqlSession的刪除,更新,插入,選擇等方法。

那麼這些方法是如何執行的呢?

SqlSession下的四大對象:

  • Mapper執行的過程是通過Executor,StatementHandler,ParameterHandler和ResultHandler來完成數據庫操作和結果返回的。
  • Executor:執行器,由執行器來調度三個Handler來執行對應的SQL語句。
    • 三種執行器:SIMPLE;REUSE;BATCH
  • StatementHandler:
    • 數據庫會話器,專門處理數據庫會話的。
    • 是四大對象的核心,作用是使用數據庫的Statement(PreparedStatement)執行操作。
    • 對應執行器,也有三種數據庫會話器。
  • ParameterHandler:參數處理器,用於SQL對參數的處理,對預編譯語句進行參數設置的。
  • ResultHandler:結果處理器,進行最後數據集(ResultSet)的封裝返回處理的。

SqlSession運行過程總結:

  • 動態代理創建代理對象,使得這個接口跑起來,開始調用SqlSession中的方法。
  • Executor會先調用StatementHandler的prepare()方法預編譯SQL語句,同時設置一些基本運行的參數。
  • 然後parameterrize()方法啓動ParameterHandler設置參數,完成預編譯,接着就是執行查詢。
  • 最後通過ResultSetHandler封裝結果返回給調用者。

Other Question:

Mybatis中 # 和 $ 的區別:(能用#號就不要用$符號)

  • #符號將傳入的數據都當做一個字符串,會對自動傳入的數據加一個雙引號
  • $符號將傳入的數據直接顯示生成SQL中
  • #符號存在預編譯的過程,對問號賦值,防止SQL注入
  • $符號是直譯的方式,一般用在order by ${列名}語句中

Mybatis的一級、二級緩存:

  • 一級緩存(同一個SqlSession):
    • 基於 PerpetualCache 的 HashMap 本地緩存,其存儲作用域爲 Session,當 Session flush 或 close 之後,該 Session 中的所有 Cache 就將清空,默認打開一級緩存。
  • 二級緩存(同一個SqlSessionFactory):
    • 二級緩存與一級緩存其機制相同,默認也是採用 PerpetualCache,HashMap 存儲,不同在於其存儲作用域爲 Mapper(Namespace),並且可自定義存儲源,如 Ehcache。

默認不打開二級緩存,要開啓二級緩存,使用二級緩存屬性類需要實現Serializable序列化接口(可用來保存對象的狀態),可在它的映射文件中配置<cache/>。 對於緩存數據更新機制,當某一個作用域(一級緩存 Session/二級緩存Namespaces)的進行了C/U/D 操作後,默認該作用域下所有 select 中的緩存將被 clear。

MyBatis的接口綁定?有哪些實現方式?

接口綁定:

就是在MyBatis中任意定義接口,然後把接口裏面的方法和SQL語句綁定, 我們直接調用接口方法就可以,這樣比起原來了SqlSession提供的方法我們可以有更加靈活的選擇和設置。

接口綁定有兩種實現方式:

  • 通過註解綁定,就是在接口的方法上面加上 @Select、@Update等註解,裏面包含Sql語句來綁定
  • 通過xml裏面寫SQL來綁定, 在這種情況下,要指定xml映射文件裏面的namespace必須爲接口的全路徑名。

接口綁定方式的選擇:

  • 當Sql語句比較簡單時候,用註解綁定, 當SQL語句比較複雜時候,用xml綁定,一般用xml綁定的比較多。

 

結束語:

       這篇文章,是博主本人在學習MyBatis中過程中所做的筆記與總結,並且給出了一個簡單的Demo演示 使用MyBatis+druid來連接數據庫。根據Demo展示,我們闡述了幾個比較基本的MyBatis技術原理,(分析很淺)希望可以對大家有所幫助。

 

如果對你有幫助,記得點贊哦~歡迎大家關注我的博客,可以進羣366533258一起交流學習哦~

本羣給大家提供一個學習交流的平臺,內設菜鳥Java管理員一枚、精通算法的金牌講師一枚、Android管理員一枚、藍牙BlueTooth管理員一枚、Web前端管理一枚以及C#管理一枚。歡迎大家進來交流技術。

關注微信公衆號(文強的技術小屋),學習更多技術知識,一起遨遊知識海洋~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章