本篇文章是我爲接下來的 MyBatis 源碼分析系列文章寫的一個導讀文章。本篇文章從 MyBatis 是什麼(what),爲何要使用(why),以及如何使用(how)等三個角度進行了說明和演示。因爲文章的篇幅比較大,這裏特意拿出一章用於介紹本文的結構和內容。那下面咱們來看一下本文的章節安排:html
如上圖,本文的大部分篇幅主要集中在了第3章和第4章。第3章演示了幾種持久層技術的用法,並在此基礎上,分析了各類技術的使用場景。經過分析 MyBatis 的使用場景,說明了爲何要使用 MyBatis 這個問題。第4章主要用於介紹 MyBatis 的兩種不一樣的用法。在 4.1 節,演示單獨使用 MyBatis 的過程,演示示例涉及一對一
和一對多
的查詢場景。4.2 節則是介紹了 MyBatis 和 Spring 整合的過程,並在最後演示瞭如何在 Spring 中使用 MyBatis。除了這兩章內容,本文的第2章和第5章內容比較少,就不介紹了。java
以上就是本篇文章內容的預覽,若是這些內容你們都掌握,那麼就沒必要往下看了。固然,若是沒掌握或者是有興趣,那不妨繼續往下閱讀。好了,其餘的就很少說了,我們進入正題吧。mysql
MyBatis 的前身是 iBatis,其是 Apache 軟件基金會下的一個開源項目。2010年該項目從 Apache 基金會遷出,並更名爲 MyBatis。同期,iBatis 中止維護。spring
MyBatis 是一種半自動化的 Java 持久層框架(persistence framework),其經過註解或 XML 的方式將對象和 SQL 關聯起來。之因此說它是半自動的,是由於和 Hibernate 等一些可自動生成 SQL 的 ORM(Object Relational Mapping) 框架相比,使用 MyBatis 須要用戶自行維護 SQL。維護 SQL 的工做比較繁瑣,但也有好處。好比咱們可控制 SQL 邏輯,可對其進行優化,以提升效率。sql
MyBatis 是一個容易上手的持久層框架,使用者經過簡單的學習便可掌握其經常使用特性的用法。這也是 MyBatis 被普遍使用的一個緣由。數據庫
咱們在使用 Java 程序訪問數據庫時,有多種選擇。好比咱們可經過編寫最原始的 JDBC 代碼訪問數據庫,或是經過 Spring 提供的 JdbcTemplate 訪問數據庫。除此以外,咱們還能夠選擇 Hibernate,或者本篇的主角 MyBatis 等。在有多個可選項的狀況下,咱們爲何選擇 MyBatis 呢?要回答這個問題,咱們須要將 MyBatis 與這幾種數據庫訪問方式對比一下,高下立判。固然,技術之間一般沒有高下之分。從應用場景的角度來講,符合應用場景需求的技術纔是合適的選擇。那下面我會經過寫代碼的方式,來比較一下這幾種數據庫訪問技術的優缺點,並會在最後說明 MyBatis 的適用場景。apache
這裏,先把本節所用到的一些公共類和配置貼出來,後面但凡用到這些資源的地方,你們能夠到這裏進行查看。本章所用到的類以下:編程
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 訪問數據庫的過程。
前面說過,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 直接訪問數據庫過程是怎樣的。
在初學 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 那樣搞一堆配置了。
上面說了一大堆 JDBC 的壞話,有點過意不去,因此下面來吐槽一下 MyBatis 吧。與 JDBC 相比,MyBatis 缺點比較明顯,它的配置比較多,特別是 SQL 映射文件。若是一個大型項目中有幾十上百個 Dao 接口,就須要有同等數量的 SQL 映射文件,這些映射文件須要用戶自行維護。不過與 JDBC 相比,維護映射文件不是什麼問題。否則若是把同等數量的 SQL 像 JDBC 那樣寫在代碼中,那維護的代價才叫大,搞很差還會翻車。除了配置文件的問題,你們會發現使用 MyBatis 訪問數據庫好像過程也很繁瑣啊。它的步驟大體以下:
如上,若是每次執行一個 SQL 要通過上面幾步,那和 JDBC 比較起來,也沒什優點了。不過這裏你們須要注意,SqlSessionFactoryBuilder 和 SqlSessionFactory 以及 SqlSession 等對象的做用域和生命週期是不同的,這一點在 MyBatis 官方文檔中說的比較清楚,我這裏照搬一下。SqlSessionFactoryBuilder 對象用於構建 SqlSessionFactory,只要構建好,這個對象就能夠丟棄了。SqlSessionFactory 是一個工廠類,一旦被建立就應該在應用運行期間一直存在,不該該丟棄或重建。SqlSession 不是線程安全的,因此不該被多線程共享。官方推薦的使用方式是有按需建立,用完即銷燬。所以,以上步驟中,第一、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 則是構建在基礎服務之上的框架,它們的目標是不一樣的。
上一節演示了 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 進行了一層比較薄的封裝,相關對比能夠參考上一節的部分分析,這裏再也不贅述。
本節會像以前的章節同樣,我會先寫代碼進行演示,而後再對比 Hibernate 和 MyBatis 的區別。須要特別說明的是,我在工做中沒有用過 Hibernate,對 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&characterEncoding=utf8&autoReconnect=true&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 區別不大,這裏就不貼出來了。
下面看一下測試代碼的運行結果:
在 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 是一個比較合適的選擇。
本節用了大量的篇幅介紹常見持久層框架的用法,並進行了較爲詳細的分析和對比。看完這些,相信你們對這些框架應該也有了更多的瞭解。好了,其餘的就很少說了,咱們繼續往下看吧。
本章,咱們一塊兒來看一下 MyBatis 是如何使用的。在上一章,我簡單演示了一下 MyBatis 的使用方法。不過,那個太簡單了,本章咱們來演示一個略爲複雜的例子。不過,這個例子複雜度和真實的項目仍是有差距,僅作演示使用。
本章包含兩節內容,第一節演示單獨使用 MyBatis 的過程,第二節演示 MyBatis 是如何和 Spring 進行整合的。那其餘的就很少說了,下面開始演示。
本節演示的場景是我的網站的做者和文章之間的關聯場景。在一個網站中,一篇文章對應一名做者,一個做者對應多篇文章。下面咱們來看一下做者
和文章
的定義,以下:
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
的 <resultMap/>。除了要注意一對多的配置,這裏還要下面這行配置:
<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
的 <resultMap/>。除了一對一的配置外,這裏還有一個自定義類型處理器ArticleTypeHandler
須要你們注意。這個自定義類型處理器用於處理ArticleTypeEnum
枚舉類型。你們若是注意看前面貼的ArticleTypeEnum
的源碼,會發現每一個枚舉值有本身的編號定義。好比JAVA
的編號爲1
,DUBBO
的編號爲2
,SPRING
的編號爲8
。因此這裏咱們不能再使用EnumOrdinalTypeHandler
對ArticleTypeHandler
進行類型轉換,須要自定義一個類型轉換器。那下面咱們來看一下這個類型轉換器的定義。
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 的文檔和一些書籍,我還真不知道它們的存在,孤陋寡聞了。因此,對於這部分特性,本文也不會進行說明。
綜上所述,本節所演示的是一個比較簡單的示例,並不是完整示例,望周知。
在上一節,我演示了單獨使用 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; } }
如上代碼,爲了證實咱們的整合配置生效了,上面專門寫了一個方法,用於輸出ApplicationContext
中bean
的信息。下面來看一下testOne2One
測試方法的輸出結果。
如上所示,bean name 的前兩行就是咱們的 Dao 接口的名稱,它們的實現類則是 JDK 的動態代理生成的。而後testOne2One
方法也正常運行了,由此可知,咱們的整合配置生效了。
到此,本篇文章就接近尾聲了。本篇文章對 MyBatis 是什麼,爲什麼要使用,以及如何使用等三個方面進行闡述和演示。總的來講,本文的篇幅應該說清楚了這三個問題。本篇文章的篇幅比較大,讀起來應該比較辛苦。不過好在內容不難,理解起來應該沒什麼問題。本篇文章的篇幅超出了我以前的預期,文章太大,出錯的機率也會隨之上升。因此若是文章有錯誤的地方,但願你們可以指明。
好了,本篇文章就到這裏了,感謝你們的閱讀。
更新時間 | 標題 |
---|---|
2018-07-16 | MyBatis 源碼分析系列文章導讀 |
2018-07-20 | MyBatis 源碼分析 - 配置文件解析過程 |
本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:coolblog.xyz
本文同步發佈在個人我的博客: http://www.coolblog.xyz
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。