MyBatis 源碼分析系列文章導讀


1.本文速覽

本篇文章是我為接下來的 MyBatis 源碼分析系列文章寫的一個導讀文章。本篇文章從 MyBatis 是什么(what),為什么要使用(why),以及如何使用(how)等三個角度進行了說明和演示。由於文章的篇幅比較大,這里特地拿出一章用於介紹本文的結構和內容。那下面我們來看一下本文的章節安排:

如上圖,本文的大部分篇幅主要集中在了第3章和第4章。第3章演示了幾種持久層技術的用法,並在此基礎上,分析了各種技術的使用場景。通過分析 MyBatis 的使用場景,說明了為什么要使用 MyBatis 這個問題。第4章主要用於介紹 MyBatis 的兩種不同的用法。在 4.1 節,演示單獨使用 MyBatis 的過程,演示示例涉及一對一一對多的查詢場景。4.2 節則是介紹了 MyBatis 和 Spring 整合的過程,並在最后演示了如何在 Spring 中使用 MyBatis。除了這兩章內容,本文的第2章和第5章內容比較少,就不介紹了。

以上就是本篇文章內容的預覽,如果這些內容大家都掌握,那么就不必往下看了。當然,如果沒掌握或者是有興趣,那不妨繼續往下閱讀。好了,其他的就不多說了,咱們進入正題吧。

2.什么是 MyBatis

MyBatis 的前身是 iBatis,其是 Apache 軟件基金會下的一個開源項目。2010年該項目從 Apache 基金會遷出,並改名為 MyBatis。同期,iBatis 停止維護。

MyBatis 是一種半自動化的 Java 持久層框架(persistence framework),其通過注解或 XML 的方式將對象和 SQL 關聯起來。之所以說它是半自動的,是因為和 Hibernate 等一些可自動生成 SQL 的 ORM(Object Relational Mapping) 框架相比,使用 MyBatis 需要用戶自行維護 SQL。維護 SQL 的工作比較繁瑣,但也有好處。比如我們可控制 SQL 邏輯,可對其進行優化,以提高效率。

MyBatis 是一個容易上手的持久層框架,使用者通過簡單的學習即可掌握其常用特性的用法。這也是 MyBatis 被廣泛使用的一個原因。

3.為什么要使用 MyBatis

我們在使用 Java 程序訪問數據庫時,有多種選擇。比如我們可通過編寫最原始的 JDBC 代碼訪問數據庫,或是通過 Spring 提供的 JdbcTemplate 訪問數據庫。除此之外,我們還可以選擇 Hibernate,或者本篇的主角 MyBatis 等。在有多個可選項的情況下,我們為什么選擇 MyBatis 呢?要回答這個問題,我們需要將 MyBatis 與這幾種數據庫訪問方式對比一下,高下立判。當然,技術之間通常沒有高下之分。從應用場景的角度來說,符合應用場景需求的技術才是合適的選擇。那下面我會通過寫代碼的方式,來比較一下這幾種數據庫訪問技術的優缺點,並會在最后說明 MyBatis 的適用場景。

這里,先把本章所用到的一些公共類和配置貼出來,后面但凡用到這些資源的地方,大家可以到這里進行查看。本章所用到的類如下:

public class Article {
    private Integer id;
    private String title;
    private String author;
    private String content;
    private Date createTime;
    
    // 省略 getter/setter 和 toString
}

數據庫相關配置放在了 jdbc.properties 文件中,詳細內容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/coolblog?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE
jdbc.username=root
jdbc.password=****

表記錄如下:

下面先來演示 MyBatis 訪問數據庫的過程。

3.1 使用 MyBatis 訪問數據庫

前面說過,MyBatis 是一種半自動化的 Java 持久化框架,使用 MyBatis 需要用戶自行維護 SQL。這里,我們把 SQL 放在 XML 中,文件名稱為 ArticleMapper.xml。相關配置如下:

<mapper namespace="xyz.coolblog.dao.ArticleDao">
    <resultMap id="articleResult" type="xyz.coolblog.model.Article">
        <id property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="author" column="author"/>
        <result property="content" column="content"/>
        <result property="createTime" column="create_time"/>
    </resultMap>
    
    <select id="findByAuthorAndCreateTime" resultMap="articleResult">
        SELECT
            `id`, `title`, `author`, `content`, `create_time`
        FROM
            `article`
        WHERE
            `author` = #{author} AND `create_time` > #{createTime}
    </select>
</mapper>

上面的 SQL 用於從article表中查詢出某個作者從某個時候到現在所寫的文章記錄。在 MyBatis 中,SQL 映射文件需要與數據訪問接口對應起來,比如上面的配置對應xyz.coolblog.dao.ArticleDao接口,這個接口的定義如下:

public interface ArticleDao {
    List<Article> findByAuthorAndCreateTime(@Param("author") String author, @Param("createTime") String createTime);
}

要想讓 MyBatis 跑起來,還需要進行一些配置。比如配置數據源、配置 SQL 映射文件的位置信息等。本節所使用到的配置如下:

<configuration>
    <properties resource="jdbc.properties"/>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <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="mapper/ArticleMapper.xml"/>
    </mappers>
</configuration>

到此,MyBatis 所需的環境就配置好了。接下來把 MyBatis 跑起來吧,相關測試代碼如下:

public class MyBatisTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void prepare() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        inputStream.close();
    }

    @Test
    public void testMyBatis() throws IOException {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            ArticleDao articleDao = session.getMapper(ArticleDao.class);
            List<Article> articles = articleDao.findByAuthorAndCreateTime("coolblog.xyz", "2018-06-10");
        } finally {
            session.commit();
            session.close();
        }
    }
}

在上面的測試代碼中,prepare 方法用於創建SqlSessionFactory工廠,該工廠的用途是創建SqlSession。通過 SqlSession,可為我們的數據庫訪問接口ArticleDao接口生成一個代理對象。MyBatis 會將接口方法findByAuthorAndCreateTime和 SQL 映射文件中配置的 SQL 關聯起來,這樣調用該方法等同於執行相關的 SQL。

上面的測試代碼運行結果如下:

如上,大家在學習 MyBatis 框架時,可以配置一下 MyBatis 的日志,這樣可把 MyBatis 的調試信息打印出來,方便觀察 SQL 的執行過程。在上面的結果中,==>符號所在的行表示向數據庫中輸入的 SQL 及相關參數。<==符號所在的行則是表示 SQL 的執行結果。上面輸入輸出不難看懂,這里就不多說了。

關於 MyBatis 的優缺點,這里先不進行總結。后面演示其他的框架時再進行比較說明。

演示完 MyBatis,下面,我們來看看通過原始的 JDBC 直接訪問數據庫過程是怎樣的。

3.2 使用 JDBC 訪問數據庫

3.2.1 JDBC 訪問數據庫的過程演示

在初學 Java 編程階段,多數朋友應該都是通過直接寫 JDBC 代碼訪問數據庫。我這么說,大家應該沒異議吧。這種方式的代碼流程一般是加載數據庫驅動,創建數據庫連接對象,創建 SQL 執行語句對象,執行 SQL 和處理結果集等,過程比較固定。下面我們再手寫一遍 JDBC 代碼,回憶一下初學 Java 的場景。

public class JdbcTest {

    @Test
    public void testJdbc() {
        String url = "jdbc:mysql://localhost:3306/myblog?user=root&password=1234&useUnicode=true&characterEncoding=UTF8&useSSL=false";

        Connection conn = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(url);

            String author = "coolblog.xyz";
            String date = "2018.06.10";
            String sql = "SELECT id, title, author, content, create_time FROM article WHERE author = '" + author + "' AND create_time > '" + date + "'";

            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            List<Article> articles = new ArrayList<>(rs.getRow());
            while (rs.next()) {
                Article article = new Article();
                article.setId(rs.getInt("id"));
                article.setTitle(rs.getString("title"));
                article.setAuthor(rs.getString("author"));
                article.setContent(rs.getString("content"));
                article.setCreateTime(rs.getDate("create_time"));
                articles.add(article);
            }
            System.out.println("Query SQL ==> " + sql);
            System.out.println("Query Result: ");
            articles.forEach(System.out::println);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

代碼比較簡單,就不多說了。下面來看一下測試結果:

上面代碼的步驟比較多,但核心步驟只有兩部,分別是執行 SQL 和處理查詢結果。從開發人員的角度來說,我們也只關心這兩個步驟。如果每次為了執行某個 SQL 都要寫很多額外的代碼。比如打開驅動,創建數據庫連接,就顯得很繁瑣了。當然我們可以將這些額外的步驟封裝起來,這樣每次調用封裝好的方法即可。這樣確實可以解決代碼繁瑣,冗余的問題。不過,使用 JDBC 並非僅會導致代碼繁瑣,冗余的問題。在上面的代碼中,我們通過字符串對 SQL 進行拼接。這樣做會導致兩個問題,第一是拼接 SQL 可能會導致 SQL 出錯,比如少了個逗號或者多了個單引號等。第二是將 SQL 寫在代碼中,如果要改動 SQL,就需要到代碼中進行更改。這樣做是不合適的,因為改動 Java 代碼就需要重新編譯 Java 文件,然后再打包發布。同時,將 SQL 和 Java 代碼混在一起,會降低代碼的可讀性,不利於維護。關於拼接 SQL,是有相應的處理方法。比如可以使用 PreparedStatement,同時還可解決 SQL 注入的問題。

除了上面所說的問題,直接使用 JDBC 訪問數據庫還會有什么問題呢?這次我們將目光轉移到執行結果的處理邏輯上。從上面的代碼中可以看出,我們需要手動從 ResultSet 中取出數據,然后再設置到 Article 對象中。好在我們的 Article 屬性不多,所以這樣做看起來也沒什么。假如 Article 對象有幾十個屬性,再用上面的方式接收查詢結果,會非常的麻煩。而且可能還會因為屬性太多,導致忘記設置某些屬性。以上的代碼還有一個問題,用戶需要自行處理受檢異常,這也是導致代碼繁瑣的一個原因。哦,還有一個問題,差點忘了。用戶還需要手動管理數據庫連接,開始要手動獲取數據庫連接。使用好后,又要手動關閉數據庫連接。不得不說,真麻煩。

沒想到直接使用 JDBC 訪問數據庫會有這么多的問題。如果在生產環境直接使用 JDBC,怕是要被 Leader 打死了。當然,視情況而定。如果項目非常小,且對數據庫依賴比較低。直接使用 JDBC 也很方便,不用像 MyBatis 那樣搞一堆配置了。

3.2.2 MyBatis VS JDBC

上面說了一大堆 JDBC 的壞話,有點過意不去,所以下面來吐槽一下 MyBatis 吧。與 JDBC 相比,MyBatis 缺點比較明顯,它的配置比較多,特別是 SQL 映射文件。如果一個大型項目中有幾十上百個 Dao 接口,就需要有同等數量的 SQL 映射文件,這些映射文件需要用戶自行維護。不過與 JDBC 相比,維護映射文件不是什么問題。不然如果把同等數量的 SQL 像 JDBC 那樣寫在代碼中,那維護的代價才叫大,搞不好還會翻車。除了配置文件的問題,大家會發現使用 MyBatis 訪問數據庫好像過程也很繁瑣啊。它的步驟大致如下:

  1. 讀取配置文件
  2. 創建 SqlSessionFactoryBuilder 對象
  3. 通過 SqlSessionFactoryBuilder 對象創建 SqlSessionFactory
  4. 通過 SqlSessionFactory 創建 SqlSession
  5. 為 Dao 接口生成代理類
  6. 調用接口方法訪問數據庫

如上,如果每次執行一個 SQL 要經過上面幾步,那和 JDBC 比較起來,也沒什優勢了。不過這里大家需要注意,SqlSessionFactoryBuilder 和 SqlSessionFactory 以及 SqlSession 等對象的作用域和生命周期是不一樣的,這一點在 MyBatis 官方文檔中說的比較清楚,我這里照搬一下。SqlSessionFactoryBuilder 對象用於構建 SqlSessionFactory,只要構建好,這個對象就可以丟棄了。SqlSessionFactory 是一個工廠類,一旦被創建就應該在應用運行期間一直存在,不應該丟棄或重建。SqlSession 不是線程安全的,所以不應被多線程共享。官方推薦的使用方式是有按需創建,用完即銷毀。因此,以上步驟中,第1、2和第3步只需執行一次。第4和第5步需要進行多次創建。至於第6步,這一步是必須的。所以比較下來,MyBatis 的使用方式還是比 JDBC 簡單的。同時,使用 MyBatis 無需處理受檢異常,比如 SQLException。另外,把 SQL 寫在配置文件中,進行集中管理,利於維護。同時將 SQL 從代碼中剝離,在提高代碼的可讀性的同時,也避免拼接 SQL 可能會導致的錯誤。除了上面所說這些,MyBatis 會將查詢結果轉為相應的對象,無需用戶自行處理 ResultSet。

總的來說,MyBatis 在易用性上要比 JDBC 好太多。不過這里拿 MyBatis 和 JDBC 進行對比並不太合適。JDBC 作為 Java 平台的數據庫訪問規范,它僅提供一種訪問數據庫的能力。至於使用者覺得 JDBC 流程繁瑣,還要自行處理異常等問題,這些還真不怪 JDBC。比如 SQLException 這個異常,JDBC 沒法處理啊,拋給調用者處理也是理所應當的。至於繁雜的步驟,這僅是從使用者的角度考慮的,從 JDBC 的角度來說,這里的每個步驟對於完成一個數據訪問請求來說都是必須的。至於 MyBatis,它是構建在 JDBC 技術之上的,對訪問數據庫的操作進行了簡化,方便用戶使用。綜上所述,JDBC 可看做是一種基礎服務,MyBatis 則是構建在基礎服務之上的框架,它們的目標是不同的。

3.3 使用 Spring JDBC 訪問數據庫

上一節演示了 JDBC 訪問數據的過程,通過演示及分析,大家應該感受到了直接使用 JDBC 的一些痛點。為了解決其中的一些痛點,Spring JDBC 應運而生。Spring JDBC 在 JDBC 基礎上,進行了比較薄的包裝,易用性得到了不少提升。那下面我們來看看如何使用 Spring JDBC。

我們在使用 Spring JDBC 之前,需要進行一些配置。這里我把配置信息放在了 application.xml 文件中,后面寫測試代碼時,讓容器去加載這個配置。配置內容如下:

<context:property-placeholder location="jdbc.properties"/>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>

如上,JdbcTemplate封裝了一些訪問數據庫的方法,下面我們會通過此對象訪問數據庫。演示代碼如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application.xml")
public class SpringJdbcTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testSpringJdbc() {
        String author = "coolblog.xyz";
        String date = "2018.06.10";
        String sql = "SELECT id, title, author, content, create_time FROM article WHERE author = '" + author + "' AND create_time > '" + date + "'";
        List<Article> articles = jdbcTemplate.query(sql, (rs, rowNum) -> {
                    Article article = new Article();
                    article.setId(rs.getInt("id"));
                    article.setTitle(rs.getString("title"));
                    article.setAuthor(rs.getString("author"));
                    article.setContent(rs.getString("content"));
                    article.setCreateTime(rs.getDate("create_time"));
                    return article;
            });

        System.out.println("Query SQL ==> " + sql);
        System.out.println("Spring JDBC Query Result: ");
        articles.forEach(System.out::println);
    }
}

測試結果如下:

從上面的代碼中可以看得出,Spring JDBC 還是比較容易使用的。不過它也是存在一定缺陷的,比如 SQL 仍是寫在代碼中。又比如,對於較為復雜的結果(數據庫返回的記錄包含多列數據),需要用戶自行處理 ResultSet 等。不過與 JDBC 相比,使用 Spring JDBC 無需手動加載數據庫驅動,獲取數據庫連接,以及創建 Statement 對象等操作。總的來說,易用性上得到了不少的提升。

這里就不對比 Spring JDBC 和 MyBatis 的優缺點了。Spring JDBC 僅對 JDBC 進行了一層比較薄的封裝,相關對比可以參考上一節的部分分析,這里不再贅述。

3.4 使用 Hibernate 訪問數據庫

本節會像之前的章節一樣,我會先寫代碼進行演示,然后再對比 Hibernate 和 MyBatis 的區別。需要特別說明的是,我在工作中沒有用過 Hibernate,對 Hibernate 也僅停留在了解的程度上。本節的測試代碼都是現學現賣的,可能有些地方寫的會有問題,或者不是最佳實踐。所以關於測試代碼,大家看看就好。若有不妥之處,也歡迎指出。

3.4.1 Hibernate 訪問數據庫的過程演示

使用 Hibernate,需要先進行環境配置,主要是關於數據庫方面的配置。這里為了演示,我們簡單配置一下。如下:

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/myblog?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;rewriteBatchedStatements=TRUE</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">****</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
        <property name="hibernate.show_sql">true</property>

        <mapping resource="mapping/Article.hbm.xml" />
    </session-factory>
</hibernate-configuration>

下面再配置一下實體類和表之間的映射關系,也就是上面配置中出現的Article.hbm.xml。不過這個配置不是必須的,可用注解進行替換。

<hibernate-mapping package="xyz.coolblog.model">
    <class table="article" name="Article">
        <id name="id" column="id">
            <generator class="native" />
        </id>
        <property name="title" column="title" />
        <property name="author" column="author" />
        <property name="content" column="content" />
        <property name="createTime" column="create_time" />
    </class>
</hibernate-mapping>

測試代碼如下:

public class HibernateTest {

    private SessionFactory buildSessionFactory;

    @Before
    public void init() {
        Configuration configuration = new Configuration();
        configuration.configure("hibernate.cfg.xml");
        buildSessionFactory = configuration.buildSessionFactory();
    }

    @After
    public void destroy() {
        buildSessionFactory.close();
    }

    @Test
    public void testORM() {
        System.out.println("-----------------------------✨ ORM Query ✨--------------------------");

        Session session = null;
        try {
            session = buildSessionFactory.openSession();
            int id = 6;
            Article article = session.get(Article.class, id);
            System.out.println("ORM Query Result: ");
            System.out.println(article);
            System.out.println();
        } finally {
            if (Objects.nonNull(session)) {
                session.close();
            }
        }

    }

    @Test
    public void testHQL() {
        System.out.println("-----------------------------✨ HQL Query ✨+--------------------------");
        Session session = null;
        try {
            session = buildSessionFactory.openSession();
            String hql = "from Article where author = :author and create_time > :createTime";
            Query query = session.createQuery(hql);
            query.setParameter("author", "coolblog.xyz");
            query.setParameter("createTime", "2018.06.10");

            List<Article> articles = query.list();
            System.out.println("HQL Query Result: ");
            articles.forEach(System.out::println);
            System.out.println();
        } finally {
            if (Objects.nonNull(session)) {
                session.close();
            }
        }
    }

    @Test
    public void testJpaCriteria() throws ParseException {
        System.out.println("---------------------------✨ JPA Criteria ✨------------------------");

        Session session = null;
        try {
            session = buildSessionFactory.openSession();
            CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
            CriteriaQuery<Article> criteriaQuery = criteriaBuilder.createQuery(Article.class);
    
            // 定義 FROM 子句
            Root<Article> article = criteriaQuery.from(Article.class);
    
            // 構建查詢條件
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
            Predicate greaterThan = criteriaBuilder.greaterThan(article.get("createTime"), sdf.parse("2018.06.10"));
            Predicate equal = criteriaBuilder.equal(article.get("author"), "coolblog.xyz");
    
            // 通過具有語義化的方法構建 SQL,等價於 SELECT ... FROM article WHERE ... AND ...
            criteriaQuery.select(article).where(equal, greaterThan);
    
            Query<Article> query = session.createQuery(criteriaQuery);
            List<Article> articles = query.getResultList();
    
            System.out.println("JPA Criteria Query Result: ");
            articles.forEach(System.out::println);
        } finally {
            if (Objects.nonNull(session)) {
                session.close();
            }
        }

    }
}

這里我寫了三種不同的查詢方法,對於比較簡單的查詢,可以通過OID的方式進行,也就是testORM方法中對應的代碼。這種方式不需要寫 SQL,完全由 Hibernate 去生成。生成的 SQL 如下:

select 
    article0_.id as id1_0_0_, 
    article0_.title as title2_0_0_, 
    article0_.author as author3_0_0_, 
    article0_.content as content4_0_0_, 
    article0_.create_time as create_t5_0_0_ 
from 
    article article0_ 
where 
    article0_.id=?

第二種方式是通過HQL進行查詢,查詢過程對應測試類中的testHQL方法。這種方式需要寫一點 HQL,並為其設置相應的參數。最終生成的 SQL 如下:

select 
    article0_.id as id1_0_, 
    article0_.title as title2_0_, 
    article0_.author as author3_0_, 
    article0_.content as content4_0_, 
    article0_.create_time as create_t5_0_ 
from 
    article article0_ 
where 
    article0_.author=? and create_time>?

第三種方式是通過 JPA Criteria 進行查詢,JPA Criteria 具有類型安全、面向對象和語義化的特點。使用 JPA Criteria,我們可以用寫 Java 代碼的方式進行數據庫操作,無需手寫 SQL。第二種方式和第三種方式進行的是同樣的查詢,所以生成的 SQL 區別不大,這里就不貼出來了。

下面看一下測試代碼的運行結果:

3.4.2 MyBatis VS Hibernate

在 Java 中,就持久層框架來說,MyBatis 和 Hibernate 都是很熱門的框架。關於這兩個框架孰好孰壞,在網上也有很廣泛的討論。不過就像我前面說到那樣,技術之間通常沒有高低之分,適不適合才是應該關注的點。這兩個框架之前的區別是比較大的,下面我們來聊聊。

從映射關系上來說,Hibernate 是把實體類(POJO)和表進行了關聯,是一種完整的 ORM (O/R mapping) 框架。而MyBatis 則是將數據訪問接口(Dao)與 SQL 進行了關聯,本質上算是一種 SQL 映射。從使用的角度來說,使用 Hibernate 通常不需要寫 SQL,讓框架自己生成就可以了。但 MyBatis 則不行,再簡單的數據庫訪問操作都需要有與之對應的 SQL。另一方面,由於 Hibernate 可自動生成 SQL,所以進行數據庫移植時,代價要小一點。而由於使用 MyBatis 需要手寫 SQL,不同的數據庫在 SQL 上存在着一定的差異。這就導致進行數據庫移植時,可能需要更改 SQL 的情況。不過好在移植數據庫的情況很少見,可以忽略。

上面我從兩個維度對 Hibernate 和 MyBatis 進行了對比,但目前也只是說了他們的一些不同點。下面我們來分析一下這兩個框架的適用場景。

Hibernate 可自動生成 SQL,降低使用成本。但同時也要意識到,這樣做也是有代價的,會損失靈活性。比如,如果我們需要手動優化 SQL,我們很難改變 Hibernate 生成的 SQL。因此對於 Hibernate 來說,它適用於一些需求比較穩定,變化比較小的項目,譬如 OA、CRM 等。

與 Hibernate 相反,MyBatis 需要手動維護 SQL,這會增加使用成本。但同時,使用者可靈活控制 SQL 的行為,這為改動和優化 SQL 提供了可能。所以 MyBatis 適合應用在一些需要快速迭代,需求變化大的項目中,這也就是為什么 MyBatis 在互聯網公司中使用的比較廣泛的原因。除此之外,MyBatis 還提供了插件機制,使用者可以按需定制插件。這也是 MyBatis 靈活性的一個體現。

分析到這里,大家應該清楚了兩個框架之前的區別,以及適用場景。樓主目前在一家汽車相關的互聯網公司,公司發展的比較快,項目迭代的也比較快,各種小需求也比較多。所以,相比之下,MyBatis 是一個比較合適的選擇。

3.5 本章小結

本節用了大量的篇幅介紹常見持久層框架的用法,並進行了較為詳細的分析和對比。看完這些,相信大家對這些框架應該也有了更多的了解。好了,其他的就不多說了,我們繼續往下看吧。

4.如何使用 MyBatis

本章,我們一起來看一下 MyBatis 是如何使用的。在上一章,我簡單演示了一下 MyBatis 的使用方法。不過,那個太簡單了,本章我們來演示一個略為復雜的例子。不過,這個例子復雜度和真實的項目還是有差距,僅做演示使用。

本章包含兩節內容,第一節演示單獨使用 MyBatis 的過程,第二節演示 MyBatis 是如何和 Spring 進行整合的。那其他的就不多說了,下面開始演示。

4.1 單獨使用

本節演示的場景是個人網站的作者和文章之間的關聯場景。在一個網站中,一篇文章對應一名作者,一個作者對應多篇文章。下面我們來看一下作者文章的定義,如下:

public class AuthorDO implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private SexEnum sex;
    private String email;
    private List<ArticleDO> articles;

    // 省略 getter/setter 和 toString
}

public class ArticleDO implements Serializable {
    private Integer id;
    private String title;
    private ArticleTypeEnum type;
    private AuthorDO author;
    private String content;
    private Date createTime;

    // 省略 getter/setter 和 toString
}

如上,AuthorDO 中包含了對一組 ArticleDO 的引用,這是一對多的關系。ArticleDO 中則包含了一個對 AuthorDO 的引用,這是一對一的關系。除此之外,這里使用了兩個常量,一個用於表示性別,另一個用於表示文章類型,它們的定義如下:

public enum SexEnum {
    MAN,
    FEMALE,
    UNKNOWN;
}

public enum ArticleTypeEnum {
    JAVA(1),
    DUBBO(2),
    SPRING(4),
    MYBATIS(8);

    private int code;

    ArticleTypeEnum(int code) {
        this.code = code;
    }

    public int code() {
        return code;
    }

    public static ArticleTypeEnum find(int code) {
        for (ArticleTypeEnum at : ArticleTypeEnum.values()) {
            if (at.code == code) {
                return at;
            }
        }

        return null;
    }
}

本篇文章使用了兩張表,分別用於存儲文章和作者信息。這兩種表的內容如下:

下面來看一下數據庫訪問層的接口定義,如下:

public interface ArticleDao {
    ArticleDO findOne(@Param("id") int id);
}

public interface AuthorDao {
    AuthorDO findOne(@Param("id") int id);
}

與這兩個接口對應的 SQL 被配置在了下面的兩個映射文件中。我們先來看一下第一個映射文件 AuthorMapper.xml 的內容。

<!-- AuthorMapper.xml -->
<mapper namespace="xyz.coolblog.dao.AuthorDao">

    <resultMap id="articleResult" type="Article">
        <id property="id" column="article_id" />
        <result property="title" column="title"/>
        <result property="type" column="type"/>
        <result property="content" column="content"/>
        <result property="createTime" column="create_time"/>
    </resultMap>

    <resultMap id="authorResult" type="Author">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result property="email" column="email"/>
        <collection property="articles" ofType="Article" resultMap="articleResult"/>
    </resultMap>

    <select id="findOne" resultMap="authorResult">
        SELECT
            au.id, au.name, au.age, au.sex, au.email,
            ar.id as article_id, ar.title, ar.type, ar.content, ar.create_time
        FROM
            author au, article ar
        WHERE
            au.id = ar.author_id AND au.id = #{id}
    </select>
</mapper>

注意看上面的<resultMap/>配置,這個標簽中包含了一個一對多的配置<collection/>,這個配置引用了一個 id 為articleResult。除了要注意一對多的配置,這里還要下面這行配置:

<result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

前面說過 AuthorDO 的sex屬性是一個枚舉,但這個屬性在數據表中是以整型值進行存儲的。所以向數據表寫入或者查詢數據時,要進行類型轉換。寫入時,需要將SexEnum轉成int。查詢時,則需要把int轉成SexEnum。由於這兩個是完全不同的類型,不能通過強轉進行轉換,所以需要使用一個中間類進行轉換,這個中間類就是 EnumOrdinalTypeHandler。這個類會按照枚舉順序進行轉換,比如在SexEnum中,MAN的順序是0。存儲時,EnumOrdinalTypeHandler 會將MAN替換為0。查詢時,又會將0轉換為MAN。除了EnumOrdinalTypeHandler,MyBatis 還提供了另一個枚舉類型處理器EnumTypeHandler。這個則是按照枚舉的字面值進行轉換,比如該處理器將枚舉MAN和字符串 "MAN" 進行相互轉換。

上面簡單分析了一下枚舉類型處理器,接下來,繼續往下看。下面是 ArticleMapper.xml 的配置內容:

<!-- ArticleMapper.xml -->
<mapper namespace="xyz.coolblog.dao.ArticleDao">

    <resultMap id="authorResult" type="Author">
        <id property="id" column="author_id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result property="email" column="email"/>
    </resultMap>

    <resultMap id="articleResult" type="Article">
        <id property="id" column="id" />
        <result property="title" column="title"/>
        <result property="type" column="type" typeHandler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
        <result property="content" column="content"/>
        <result property="createTime" column="create_time"/>
        <association property="author" javaType="Author" resultMap="authorResult"/>
    </resultMap>

    <select id="findOne" resultMap="articleResult">
        SELECT
            ar.id, ar.author_id, ar.title, ar.type, ar.content, ar.create_time,
            au.name, au.age, au.sex, au.email
        FROM
            article ar, author au
        WHERE
            ar.author_id = au.id AND ar.id = #{id}
    </select>
</mapper>

如上,ArticleMapper.xml 中包含了一個一對一的配置<association/>,這個配置引用了另一個 id 為authorResult。除了一對一的配置外,這里還有一個自定義類型處理器ArticleTypeHandler需要大家注意。這個自定義類型處理器用於處理ArticleTypeEnum枚舉類型。大家如果注意看前面貼的ArticleTypeEnum的源碼,會發現每個枚舉值有自己的編號定義。比如JAVA的編號為1DUBBO的編號為2SPRING的編號為8。所以這里我們不能再使用EnumOrdinalTypeHandlerArticleTypeHandler進行類型轉換,需要自定義一個類型轉換器。那下面我們來看一下這個類型轉換器的定義。

public class ArticleTypeHandler extends BaseTypeHandler<ArticleTypeEnum> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, ArticleTypeEnum parameter, JdbcType jdbcType)
        throws SQLException {
        // 獲取枚舉的 code 值,並設置到 PreparedStatement 中
        ps.setInt(i, parameter.code());
    }

    @Override
    public ArticleTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 從 ResultSet 中獲取 code
        int code = rs.getInt(columnName);
        // 解析 code 對應的枚舉,並返回
        return ArticleTypeEnum.find(code);
    }

    @Override
    public ArticleTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return ArticleTypeEnum.find(code);
    }

    @Override
    public ArticleTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return ArticleTypeEnum.find(code);
    }
}

對於自定義類型處理器,可繼承 BaseTypeHandler,並實現相關的抽象方法。上面的代碼比較簡單,我也進行了一些注釋。應該比較好理解,這里就不多說了。

前面貼了實體類,數據訪問類,以及 SQL 映射文件。最后還差一個 MyBatis 的配置文件,這里貼出來。如下:

<!-- mybatis-congif.xml -->
<configuration>
    <properties resource="jdbc.properties"/>

    <typeAliases>
        <typeAlias alias="Article" type="xyz.coolblog.model.ArticleDO"/>
        <typeAlias alias="Author" type="xyz.coolblog.model.AuthorDO"/>
    </typeAliases>

    <typeHandlers>
        <typeHandler handler="xyz.coolblog.mybatis.ArticleTypeHandler" javaType="xyz.coolblog.constant.ArticleTypeEnum"/>
    </typeHandlers>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <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="mapper/AuthorMapper.xml"/>
        <mapper resource="mapper/ArticleMapper.xml"/>
    </mappers>
</configuration>

下面通過一個表格簡單解釋配置中出現的一些標簽。

標簽名稱 用途
properties 用於配置全局屬性,這樣在配置文件中,可以通過占位符 ${} 進行屬性值配置
typeAliases 用於定義別名。如上所示,這里把xyz.coolblog.model.ArticleDO的別名定義為Article,這樣在 SQL 映射文件中,就可以直接使用別名,而不用每次都輸入長長的全限定類名了
typeHandlers 用於定義全局的類型處理器,如果這里配置了,SQL 映射文件中就不需要再次進行配置。前面為了講解需要,我在 SQL 映射文件中也配置了 ArticleTypeHandler,其實是多余的
environments 用於配置事務,以及數據源
mappers 用於配置 SQL 映射文件的位置信息

以上僅介紹了一些比較常用的配置,更多的配置信息,建議大家去閱讀MyBatis 官方文檔

到這里,我們把所有的准備工作都做完了。那么接下來,寫點測試代碼測試一下。

public class MyBatisTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void prepare() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        inputStream.close();
    }
    
    @Test
    public void testOne2One() {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            ArticleDao articleDao = session.getMapper(ArticleDao.class);
            ArticleDO article = articleDao.findOne(1);

            AuthorDO author = article.getAuthor();
            article.setAuthor(null);

            System.out.println();
            System.out.println("author info:");
            System.out.println(author);
            System.out.println();
            System.out.println("articles info:");
            System.out.println(article);
        } finally {
            session.close();
        }
    }

    @Test
    public void testOne2Many() {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            AuthorDao authorDao = session.getMapper(AuthorDao.class);
            AuthorDO author = authorDao.findOne(1);

            List<ArticleDO> arts = author.getArticles();
            List<ArticleDO> articles = Arrays.asList(arts.toArray(new ArticleDO[arts.size()]));
            arts.clear();

            System.out.println();
            System.out.println("author info:");
            System.out.println(author);
            System.out.println();
            System.out.println("articles info:");
            articles.forEach(System.out::println);
        } finally {
            session.close();
        }
    }
}

第一個測試方法用於從數據庫中查詢某篇文章,以及相應作者的信息。它的運行結果如下:

第二個測試方法用於查詢某位作者,及其所寫的所有文章的信息。它的運行結果如下:

到此,MyBatis 的使用方法就介紹完了。由於我個人在平時的工作中,也知識使用了 MyBatis 的一些比較常用的特性,所以本節的內容也比較淺顯。另外,由於演示示例比較簡單,這里也沒有演示 MyBatis 比較重要的一個特性 -- 動態 SQL。除了以上所述,有些特性由於沒有比較好的場景去演示,這里也就不介紹了。比如 MyBatis 的插件機制,緩存等。對於一些較為生僻的特性,比如對象工廠,鑒別器。如果不是因為閱讀了 MyBatis 的文檔和一些書籍,我還真不知道它們的存在,孤陋寡聞了。所以,對於這部分特性,本文也不會進行說明。

綜上所述,本節所演示的是一個比較簡單的示例,並非完整示例,望周知。

4.2 在 Spring 中使用

在上一節,我演示了單獨使用 MyBatis 的過程。在實際開發中,我們一般都會將 MyBatis 和 Spring 整合在一起使用。這樣,我們就可以通過 bean 注入的方式使用各種 Dao 接口。MyBatis 和 Spring 原本是兩個完全不相關的框架,要想把兩者整合起來,需要一個中間框架。這個框架一方面負責加載和解析 MyBatis 相關配置。另一方面,該框架還會通過 Spring 提供的拓展點,把各種 Dao 接口及其對應的對象放入 bean 工廠中。這樣,我們才可以通過 bean 注入的方式獲取到這些 Dao 接口對應的 bean。那么問題來了,具有如此能力的框架是誰呢?答案是mybatis-spring。那其他的不多說了,下面開始演示整合過程。

我的測試項目是基於 Maven 構建的,所以這里先來看一下 pom 文件的配置。

<project>
    <!-- 省略項目坐標配置 -->

    <properties>
        <spring.version>4.3.17.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- 省略其他依賴 -->
    </dependencies>
</project>

為了減少配置文件所占的文章篇幅,上面的配置經過了一定的簡化,這里只列出了 MyBatis 和 Spring 相關包的坐標。繼續往下看,下面將 MyBatis 中的一些類配置到 Spring 的配置文件中。

<!-- application-mybatis.xml -->
<beans>
    <context:property-placeholder location="jdbc.properties"/>

    <!-- 配置數據源 -->
    <bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
        <property name="driver" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 配置 mybatis-config.xml 路徑 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 給 SqlSessionFactory 配置數據源,這里引用上面的數據源配置 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置 SQL 映射文件 -->
        <property name="mapperLocations" value="mapper/*.xml"/>
    </bean>

    <!-- 配置 MapperScannerConfigurer -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 配置 Dao 接口所在的包 -->
        <property name="basePackage" value="xyz.coolblog.dao"/>
    </bean>
</beans>

如上,上面就是將 MyBatis 整合到 Spring 中所需的一些配置。這里,我們將數據源配置到 Spring 配置文件中。配置完數據源,接下來配置 SqlSessionFactory,SqlSessionFactory 的用途大家都知道,不用過多解釋了。再接下來是配置 MapperScannerConfigurer,這個類顧名思義,用於掃描某個包下的數據訪問接口,並將這些接口注冊到 Spring 容器中。這樣,我們就可以在其他的 bean 中注入 Dao 接口的實現類,無需再從 SqlSession 中獲取接口實現類。至於 MapperScannerConfigurer 掃描和注冊 Dao 接口的細節,這里先不說明,后續我會專門寫一篇文章分析。

將 MyBatis 配置到 Spring 中后,為了讓我們的程序正常運行,這里還需要為 MyBatis 提供一份配置。相關配置如下:

<!-- mybatis-config.xml -->
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
    <typeAliases>
        <typeAlias alias="Article" type="xyz.coolblog.model.ArticleDO"/>
        <typeAlias alias="Author" type="xyz.coolblog.model.AuthorDO"/>
    </typeAliases>

    <typeHandlers>
        <typeHandler handler="xyz.coolblog.mybatis.ArticleTypeHandler" javaType="xyz.coolblog.constant.ArticleTypeEnum"/>
    </typeHandlers>
</configuration>

這里的 mybatis-config.xml 和上一節的配置不太一樣,移除了數據源和 SQL 映射文件路徑的配置。需要注意的是,對於 <settings/> 必須配置在 mybatis-config.xml 中。其他的配置都不是必須項,可放在 Spring 的配置文件中,這里偷了個懶。

到此,Spring 整合 MyBatis 的配置工作就完成了,接下來寫點測試代碼跑跑看。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-mybatis.xml")
public class SpringWithMyBatisTest implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    /** 自動注入 AuthorDao,無需再通過 SqlSession 獲取 */ 
    @Autowired
    private AuthorDao authorDao;

    @Autowired
    private ArticleDao articleDao;

    @Before
    public void printBeanInfo() {
        ListableBeanFactory lbf = applicationContext;
        String[] beanNames = lbf.getBeanDefinitionNames();
        Arrays.sort(beanNames);

        System.out.println();
        System.out.println("----------------☆ bean name ☆---------------");
        Arrays.asList(beanNames).subList(0, 5).forEach(System.out::println);
        System.out.println();

        AuthorDao authorDao = (AuthorDao) applicationContext.getBean("authorDao");
        ArticleDao articleDao = (ArticleDao) applicationContext.getBean("articleDao");

        System.out.println("-------------☆ bean class info ☆--------------");
        System.out.println("AuthorDao  Class: " + authorDao.getClass());
        System.out.println("ArticleDao Class: " + articleDao.getClass());
        System.out.println("\n--------xxxx---------xxxx---------xxx---------\n");
    }


    @Test
    public void testOne2One() {
        ArticleDO article = articleDao.findOne(1);

        AuthorDO author = article.getAuthor();
        article.setAuthor(null);

        System.out.println();
        System.out.println("author info:");
        System.out.println(author);
        System.out.println();
        System.out.println("articles info:");
        System.out.println(article);
    }

    @Test
    public void testOne2Many() {
        AuthorDO author = authorDao.findOne(1);

        System.out.println();
        System.out.println("author info:");
        System.out.println(author);
        System.out.println();
        System.out.println("articles info:");
        author.getArticles().forEach(System.out::println);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

如上代碼,為了證明我們的整合配置生效了,上面專門寫了一個方法,用於輸出ApplicationContextbean的信息。下面來看一下testOne2One測試方法的輸出結果。

如上所示,bean name 的前兩行就是我們的 Dao 接口的名稱,它們的實現類則是 JDK 的動態代理生成的。然后testOne2One方法也正常運行了,由此可知,我們的整合配置生效了。

5.總結

到此,本篇文章就接近尾聲了。本篇文章對 MyBatis 是什么,為何要使用,以及如何使用等三個方面進行闡述和演示。總的來說,本文的篇幅應該說清楚了這三個問題。本篇文章的篇幅比較大,讀起來應該比較辛苦。不過好在內容不難,理解起來應該沒什么問題。本篇文章的篇幅超出了我之前的預期,文章太大,出錯的概率也會隨之上升。所以如果文章有錯誤的地方,希望大家能夠指明。

好了,本篇文章就到這里了,感謝大家的閱讀。

參考

附錄:MyBatis 源碼分析系列文章列表

更新時間 標題
2018-09-11 MyBatis 源碼分析系列文章合集
2018-07-16 MyBatis 源碼分析系列文章導讀
2018-07-20 MyBatis 源碼分析 - 配置文件解析過程
2018-07-30 MyBatis 源碼分析 - 映射文件解析過程
2018-08-17 MyBatis 源碼分析 - SQL 的執行過程
2018-08-19 MyBatis 源碼分析 - 內置數據源
2018-08-25 MyBatis 源碼分析 - 緩存原理
2018-08-26 MyBatis 源碼分析 - 插件機制

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
作者:田小波
本文同步發布在我的個人博客:http://www.tianxiaobo.com

cc
本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM