第 3-2 課:如何優雅地使用 MyBatis XML 配置版

MyBatis 是現如今最流行的 ORM 框架之一,我們先來了解一下什麼是 ORM 框架。

ORM 框架

對象關係映射(Object Relational Mapping,ORM)模式是一種爲了解決面向對象與關係數據庫存在的互不匹配的現象的技術。簡單的說,ORM 是通過使用描述對象和數據庫之間映射的元數據,將程序中的對象自動持久化到關係數據庫中。

爲什麼需要 ORM?

當你開發一個應用程序的時候(不使用 O/R Mapping),可能會寫不少數據訪問層代碼,用來從數據庫保存、刪除、讀取對象信息等;在 DAL 中寫了很多的方法來讀取對象數據、改變狀態對象等任務,而這些代碼寫起來總是重複的。針對這些問題 ORM 提供瞭解決方案,簡化了將程序中的對象持久化到關係數據庫中的操作。

ORM 框架的本質是簡化編程中操作數據庫的編碼,在 Java 領域發展到現在基本上就剩兩家最爲流行,一個是宣稱可以不用寫一句 SQL 的 Hibernate,一個是以動態 SQL 見長的 MyBatis,兩者各有特點。在企業級系統開發中可以根據需求靈活使用,會發現一個有趣的現象:傳統企業大都喜歡使用 Hibernate,而互聯網行業通常使用 MyBatis。

MyBatis 介紹

MyBatis 是一款標準的 ORM 框架,被廣泛的應用於各企業開發中。MyBatis 最早是 Apache 的一個開源項目 iBatis,2010 年這個項目由 Apache Software Foundation 遷移到了 Google Code,並且改名爲 MyBatis,2013 年 11 月又遷移到 Github。從 MyBatis 的遷移史,也可以看出源碼託管平臺的發展史,GitHub 目前已經成爲世界上最大的開源軟件託管平臺,建議大家多多關注這個全球最大的同性社交網站。

MyBatis 支持普通的 SQL 查詢,存儲過程和高級映射的優秀持久層框架。MyBatis 消除了幾乎所有的 JDBC 代碼和參數的手工設置以及對結果集的檢索封裝。MaBatis 可以使用簡單的 XML 或註解用於配置和原始映射,將接口和 Java 的 POJO(Plain Old Java Objects,普通的 Java 對象)映射成數據庫中的記錄。

作爲一款使用廣泛的開源軟件,它的特點有哪些呢?

優點

  • SQL 被統一提取出來,便於統一管理和優化
  • SQL 和代碼解耦,將業務邏輯和數據訪問邏輯分離,使系統的設計更清晰、更易維護、更易單元測試
  • 提供映射標籤,支持對象與數據庫的 ORM 字段關係映射
  • 提供對象關係映射標籤,支持對象關係組件維護
  • 靈活書寫動態 SQL,支持各種條件來動態生成不同的 SQL

缺點

  • 編寫 SQL 語句時工作量很大,尤其是字段多、關聯表多時,更是如此
  • SQL 語句依賴於數據庫,導致數據庫移植性差

MyBatis 幾個重要的概念

Mapper 配置可以使用基於 XML 的 Mapper 配置文件來實現,也可以使用基於 Java 註解的 MyBatis 註解來實現,甚至可以直接使用 MyBatis 提供的 API 來實現。

Mapper 接口是指自行定義的一個數據操作接口,類似於通常所說的 DAO 接口。早期的 Mapper 接口需要自定義去實現,現在 MyBatis 會自動爲 Mapper 接口創建動態代理對象。Mapper 接口的方法通常與 Mapper 配置文件中的 select、insert、update、delete 等 XML 結點存在一一對應關係。

Executor,MyBatis 中所有的 Mapper 語句的執行都是通過 Executor 進行的,Executor 是 MyBatis 的一個核心接口。

SqlSession,是 MyBatis 的關鍵對象,是執行持久化操作的獨享,類似於 JDBC 中的 Connection,SqlSession 對象完全包含以數據庫爲背景的所有執行 SQL 操作的方法,它的底層封裝了 JDBC 連接,可以用 SqlSession 實例來直接執行被映射的 SQL 語句。

SqlSessionFactory,是 MyBatis 的關鍵對象,它是單個數據庫映射關係經過編譯後的內存鏡像。SqlSessionFactory 對象的實例可以通過 SqlSessionFactoryBuilder 對象類獲得,而 SqlSessionFactoryBuilder 則可以從 XML 配置文件或一個預先定製的 Configuration 的實例構建出。

MyBatis 的工作流程如下:

  • 首先加載 Mapper 配置的 SQL 映射文件,或者是註解的相關 SQL 內容。
  • 創建會話工廠,MyBatis 通過讀取配置文件的信息來構造出會話工廠(SqlSessionFactory)。
  • 創建會話。根據會話工廠,MyBatis 就可以通過它來創建會話對象(SqlSession),會話對象是一個接口,該接口中包含了對數據庫操作的增、刪、改、查方法。
  • 創建執行器。因爲會話對象本身不能直接操作數據庫,所以它使用了一個叫做數據庫執行器(Executor)的接口來幫它執行操作。
  • 封裝 SQL 對象。在這一步,執行器將待處理的 SQL 信息封裝到一個對象中(MappedStatement),該對象包括 SQL 語句、輸入參數映射信息(Java 簡單類型、HashMap 或 POJO)和輸出結果映射信息(Java 簡單類型、HashMap 或 POJO)。
  • 操作數據庫。擁有了執行器和 SQL 信息封裝對象就使用它們訪問數據庫了,最後再返回操作結果,結束流程。

在我們具體的使用過程中,就是按照上述的流程來執行。

什麼是 MyBatis-Spring-Boot-Starter

mybatis-spring-boot-starter 是 MyBatis 幫助我們快速集成 Spring Boot 提供的一個組件包,使用這個組件可以做到以下幾點:

  • 構建獨立的應用
  • 幾乎可以零配置
  • 需要很少的 XML 配置

mybatis-spring-boot-starter 依賴於 MyBatis-Spring 和 Spring Boot,最新版 1.3.2 需要 MyBatis-Spring 1.3 以上,Spring Boot 版本 1.5 以上。

注意 mybatis-spring-boot-starter 是 MyBatis 官方開發的 Starter,而不是 Spring Boot 官方開發的啓動包,其實是 MyBatis 看 Spring Boot 市場使用度非常高,因此主動開發出 Starter 包進行集成,但這一集成確實解決了很多問題,使用起來比以前簡單很多。mybatis-spring-boot-starter 主要提供了兩種解決方案,一種是簡化後的 XML 配置版,一種是使用註解解決一切問題。

MyBatis 以前只有 XML 配置這種使用的形式,到了後來註解使用特別廣泛,MyBatis 也順應潮流提供了註解的支持,從這裏可以看出 MyBatis 一直都跟隨着主流技術的變化來完善自己。接下來給大家介紹一下如何使用 XML 版本。

XML 版本保持映射文件的方式,最新版的使用主要體現在不需要實現 Dao 的實現層,系統會自動根據方法名在映射文件中找到對應的 SQL。

初始化腳本

爲了方便項目演示,需要在 test 倉庫創建 users 表,腳本如下:

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
  `userName` varchar(32) DEFAULT NULL COMMENT '用戶名',
  `passWord` varchar(32) DEFAULT NULL COMMENT '密碼',
  `user_sex` varchar(32) DEFAULT NULL,
  `nick_name` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

關鍵依賴包

當然任何模式都需要首先引入 mybatis-spring-boot-starter 的 pom 文件,現在最新版本是 1.3.2。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

application 配置

application.properties 添加相關配置:

mybatis.config-location=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.neo.model

spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

其中:

  • mybatis.config-location,配置 mybatis-config.xml 路徑,mybatis-config.xml 中配置 MyBatis 基礎屬性;
  • mybatis.mapper-locations,配置 Mapper 對應的 XML 文件路徑;
  • mybatis.type-aliases-package,配置項目中實體類包路徑;
  • spring.datasource.*,數據源配置。

Spring Boot 啓動時數據源會自動注入到 SqlSessionFactory 中,使用 SqlSessionFactory 構建 SqlSessionFactory,再自動注入到 Mapper 中,最後我們直接使用 Mapper 即可。

啓動類

在啓動類中添加對 Mapper 包掃描 @MapperScan,Spring Boot 啓動的時候會自動加載包路徑下的 Mapper。

@Spring BootApplication
@MapperScan("com.neo.mapper")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

或者直接在 Mapper 類上面添加註解 @Mapper,建議使用上面那種,不然每個 mapper 加個註解也挺麻煩的。

示例演示

MyBatis 公共屬性

mybatis-config.xml 主要配置常用的 typeAliases,設置類型別名,爲 Java 類型設置一個短的名字。它只和 XML 配置有關,存在的意義僅在於用來減少類完全限定名的冗餘。

<configuration>
    <typeAliases>
        <typeAlias alias="Integer" type="java.lang.Integer" />
        <typeAlias alias="Long" type="java.lang.Long" />
        <typeAlias alias="HashMap" type="java.util.HashMap" />
        <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
        <typeAlias alias="ArrayList" type="java.util.ArrayList" />
        <typeAlias alias="LinkedList" type="java.util.LinkedList" />
    </typeAliases>
</configuration>

這樣我們在使用 Mapper.xml 的時候,需要引入可以直接這樣寫:

resultType="Integer" 
//或者
parameterType="Long"

添加 User 的映射文件

第一步,指明對應文件的 Mapper 類地址:

<mapper namespace="com.neo.mapper.UserMapper" >

第二部,配置表結構和類的對應關係:

<resultMap id="BaseResultMap" type="com.neo.model.User" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="userName" property="userName" jdbcType="VARCHAR" />
    <result column="passWord" property="passWord" jdbcType="VARCHAR" />
    <result column="user_sex" property="userSex" javaType="com.neo.enums.UserSexEnum"/>
    <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
</resultMap>

這裏爲了更好的貼近工作情況,將類的兩個字段和數據庫字段設置爲不一致,其中一個使用了枚舉。使用枚舉有一個非常大的優點,插入此屬性的數據會自動進行校驗,如果不是枚舉的內容會報錯。

第三步,寫具體的 SQL 語句,比如這樣:

<select id="getAll" resultMap="BaseResultMap"  >
   SELECT 
   *
   FROM users
</select>

MyBatis XML 有一個特點是可以複用 XML,比如我們公用的一些 XML 片段可以提取出來,在其他 SQL 中去引用。例如:

<sql id="Base_Column_List" >
    id, userName, passWord, user_sex, nick_name
</sql>

<select id="getAll" resultMap="BaseResultMap"  >
   SELECT 
   <include refid="Base_Column_List" />
   FROM users
</select>  

這個例子就是,上面定義了需要查詢的表字段,下面 SQL 使用 include 引入,避免了寫太多重複的配置內容。

下面是常用的增、刪、改、查的例子:

<select id="getOne" parameterType="Long" resultMap="BaseResultMap" >
    SELECT 
   <include refid="Base_Column_List" />
   FROM users
   WHERE id = #{id}
</select>

<insert id="insert" parameterType="com.neo.model.User" >
   INSERT INTO 
           users
           (userName,passWord,user_sex) 
       VALUES
           (#{userName}, #{passWord}, #{userSex})
</insert>

<update id="update" parameterType="com.neo.model.User" >
   UPDATE 
           users 
   SET 
       <if test="userName != null">userName = #{userName},</if>
       <if test="passWord != null">passWord = #{passWord},</if>
       nick_name = #{nickName}
   WHERE 
           id = #{id}
</update>

<delete id="delete" parameterType="Long" >
   DELETE FROM
            users 
   WHERE 
            id =#{id}
</delete>

上面 update 的 SQL 使用了 if 標籤,可以根據不同的條件生產動態 SQL,這就是 MyBatis 最大的特點。

編寫 Dao 層的代碼

public interface UserMapper {

    List<UserEntity> getAll();

    UserEntity getOne(Long id);

    void insert(UserEntity user);

    void update(UserEntity user);

    void delete(Long id);
}

注意:這裏的方法名需要和 XML 配置中的 id 屬性一致,不然會找不到方法去對應執行的 SQL。

測試使用

按照 Spring 一貫使用形式,直接將對應的 Mapper 注入即可。

@Resource
private UserMapper userMapper;

如果使用的是 Idea,這塊的註解經常會報“could not autowire”,Eclipse 卻沒有問題,其實代碼是正確的,這是 Idea 的誤報。可以選擇降低 Autowired 檢測的級別,不要提示就好。

在 File | Settings | Editor | Inspections 選項中使用搜索功能找到 Autowiring for Bean Class,將 Severity 的級別由之前的 error 改成 warning 即可。

接下來直接使用 userMapper 進行數據庫操作即可。

@Test
public void testUser()  {
    //增加
    userMapper.insert(new User("aa", "a123456", UserSexEnum.MAN));
    //刪除
    int count=userMapper.delete(2l);
    User user = userMapper.getOne(1l);
    user.setNickName("smile");
    //修改
    userMapper.update(user);
    //查詢
    List<User> users = userMapper.getAll();
}

在示例代碼中,寫了兩份的使用示例,一個是 Test,一個在 Controller 層,方便大家下載查看。

分頁查詢

多條件分頁查詢是實際工作中最常使用的功能之一,MyBatis 特別擅長處理這類的問題。在實際工作中,會對分頁進行簡單的封裝,方便前端使用。另外在 Web 開發規範使用中,Web 層的參數會以 param 爲後綴的對象進行傳參,以 result 結尾的實體類封裝返回的數據。

下面給大家以 User 多條件分頁查詢爲例進行講解。

先定義一個分頁的基礎類:

public class PageParam {
    private int beginLine;       //起始行
    private Integer pageSize = 3;
    private Integer currentPage=0;        // 當前頁
    //getter setter省略
    public int getBeginLine() {
        return pageSize*currentPage;//自動計算起始行
    }
}

默認每頁 3 條記錄,可以根據前端傳參進行修改。

user 的查詢條件參數類繼承分頁基礎類:

public class UserParam extends PageParam{
    private String userName;
    private String userSex;
    //getter setter省略
}

接下來配置具體的 SQL,先將查詢條件提取出來。

<sql id="Base_Where_List">
    <if test="userName != null  and userName != ''">
        and userName = #{userName}
    </if>
    <if test="userSex != null and userSex != ''">
        and user_sex = #{userSex}
    </if>
</sql>

從對象 UserParam 中獲取分頁信息和查詢條件,最後進行組合。

<select id="getList" resultMap="BaseResultMap" parameterType="com.neo.param.UserParam">
    select
    <include refid="Base_Column_List" />
    from users
    where 1=1
    <include refid="Base_Where_List" />
    order by id desc
    limit #{beginLine} , #{pageSize}
</select>

前端需要展示總共的頁碼,因此需要統計出查詢結果的總數。

<select id="getCount" resultType="Integer" parameterType="com.neo.param.UserParam">
    select
    count(1)
    from users
    where 1=1
    <include refid="Base_Where_List" />
</select>

Mapper 中定義的兩個方法和配置文件相互對應。

public interface UserMapper {
    List<UserEntity> getList(UserParam userParam);
    int getCount(UserParam userParam);
}

具體使用:

@Test
public void testPage() {
    UserParam userParam=new UserParam();
    userParam.setUserSex("WOMAN");
    userParam.setCurrentPage(1);
    List<UserEntity> users=userMapper.getList(userParam);
    long count=userMapper.getCount(userParam);
    Page page = new Page(userParam,count,users);
    System.out.println(page);
}

在實際使用中,只需要傳入 CurrentPage 參數即可,默認 0 就是第一頁,傳 1 就是第二頁的內容,最後將結果封裝爲 Page 返回給前端。

public class Page<E> implements Serializable {
    private int currentPage = 0; //當前頁數
    private long totalPage;       //總頁數
    private long totalNumber;    //總記錄數
    private List<E> list;        //數據集
}

Page 將分頁信息和數據信息進行封裝,方便前端顯示第幾頁、總條數和數據,這樣分頁功能就完成了。

多數據源處理

接下來爲大家介紹如何使用 MyBatis 配置多數據源使用。

配置文件

首先我們需要配置兩個不同的數據源:

mybatis.config-location=classpath:mybatis/mybatis-config.xml

spring.datasource.one.jdbc-url=jdbc:mysql://localhost:3306/test1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.one.username=root
spring.datasource.one.password=root
spring.datasource.one.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.two.jdbc-url=jdbc:mysql://localhost:3306/test2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.two.username=root
spring.datasource.two.password=root
spring.datasource.two.driver-class-name=com.mysql.cj.jdbc.Driver

注意,需要提前在 test1 和 test2 庫中創建好 User 表結構。

第一個數據源以 spring.datasource.one.* 爲前綴連接數據庫 test1,第二個數據源以 spring.datasource.two.* 爲前綴連接數據庫 test2。

同時需要將上述的 UserMapper.xml 文件複製兩份到 resources/mybatis/mapper/one 和 resources/mybatis/mapper/two 目錄下各一份。

數據源配置

爲兩個數據源創建不同的 Mapper 包路徑,將以前的 UserMapper 複製到包 com.neo.mapper.one 和 com.neo.mapper.two 路徑下,並且分別重命名爲:User1Mapper、User2Mapper。

配置第一個數據源,新建 DataSource1Config。

首先加載配置的數據源:

@Bean(name = "oneDataSource")
@ConfigurationProperties(prefix = "spring.datasource.one")
@Primary
public DataSource testDataSource() {
    return DataSourceBuilder.create().build();
}

注意,在多數據源中只能指定一個 @Primary 作爲默認的數據源使用。

根據創建的數據源,構建對應的 SqlSessionFactory。

@Bean(name = "oneSqlSessionFactory")
@Primary
public SqlSessionFactory testSqlSessionFactory(@Qualifier("oneDataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/one/*.xml"));
    return bean.getObject();
}

代碼中需要指明需要加載的 Mapper xml 文件。

同時將數據源添加到事務中。

@Bean(name = "oneTransactionManager")
@Primary
public DataSourceTransactionManager testTransactionManager(@Qualifier("oneDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

接下來將上面創建的 SqlSessionFactory 注入,創建我們在 Mapper 中需要使用的 SqlSessionTemplate。

@Bean(name = "oneSqlSessionTemplate")
@Primary
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("oneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
    return new SqlSessionTemplate(sqlSessionFactory);
}

最後將上面創建的 SqlSessionTemplate 注入到對應的 Mapper 包路徑下,這樣這個包下面的 Mapper 都會使用第一個數據源來進行數據庫操作。

@Configuration
@MapperScan(basePackages = "com.neo.mapper.one", sqlSessionTemplateRef  = "oneSqlSessionTemplate")
public class OneDataSourceConfig {
 ...
}
  • basePackages 指明 Mapper 地址。
  • sqlSessionTemplateRef 指定 Mapper 路徑下注入的 sqlSessionTemplate。

第二個數據源配置

DataSource2Config 的配置和上面類似,方法上需要去掉 @Primary 註解,替換對應的數據源和 Mapper 路徑即可。下面是 DataSource2Config 完整示例:

@Configuration
@MapperScan(basePackages = "com.neo.mapper.two", sqlSessionTemplateRef  = "twoSqlSessionTemplate")
public class DataSource2Config {

    @Bean(name = "twoDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.two")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "twoSqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("twoDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/two/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "twoTransactionManager")
    public DataSourceTransactionManager testTransactionManager(@Qualifier("twoDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "twoSqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("twoSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

從上面的步驟我們可以總結出來,創建多數據源的過程就是:首先創建 DataSource,注入到 SqlSessionFactory 中,再創建事務,將 SqlSessionFactory 注入到創建的 SqlSessionTemplate 中,最後將 SqlSessionTemplate 注入到對應的 Mapper 包路徑下。其中需要指定分庫的 Mapper 包路徑。

注意,在多數據源的情況下,我們不需要在啓動類添加:@MapperScan("com.xxx.mapper") 的註解。

這樣 MyBatis 多數據源的配置就完成了,如果有更多的數據源請參考第二個數據源的配置即可。

測試

配置好多數據源之後,在項目中想使用哪個數據源就把對應數據源注入到類中使用即可。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private User1Mapper user1Mapper;
    @Autowired
    private User2Mapper user2Mapper;

    @Test
    public void testInsert() throws Exception {
        user1Mapper.insert(new User("aa111", "a123456", UserSexEnum.MAN));
        user1Mapper.insert(new User("bb111", "b123456", UserSexEnum.WOMAN));
        user2Mapper.insert(new User("cc222", "b123456", UserSexEnum.MAN));
    }
}

上面的測試類中注入了兩個不同的 Mapper,對應了不同的數據源。在第一個數據源中插入了兩條數據,第二個數據源中插入了一條信息,運行測試方法後查看數據庫1有兩條數據,數據庫2有一條數據,證明多數據源測試成功。

總結

這節課介紹了 ORM 框架 和 MyBatis 框架相關概念介紹,以用戶數據爲例演示了 MyBatis 的增、刪、改、查,以及分頁查詢、多數據源處理等常見場景。通過上面的示例可以發現 MyBatis 將執行 SQL 和代碼做了隔離,保證代碼處理和 SQL 的相對獨立,層級劃分比較清晰,MyBatis 對動態 SQL 支持非常友好,可以在 XML 文件中複用代碼高效編寫動態 SQL。

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