1. 程式人生 > 實用技巧 >[Re] MyBatis-1

[Re] MyBatis-1

簡述

  • 是一個持久層框架,MyBatis 可以使用簡單的 XML 或註解用於配置和原始對映,將介面和 POJO 對映成資料庫中的記錄。MyBatis 底層就是對原生 JDBC 的一個簡單封裝。
  • 之前學過的 JDBC → DBUtils(QueryRunner) → JdbcTemplate,這些都是"工具"。
    • 工具:一些功能的簡單封裝;
    • 框架:某個領域的整體解決方法(快取、異常處理、部分欄位對映 ...)
  • 為什麼要用 MyBatis?

HelloWorld

基礎環境搭建

  1. 建立 Java 工程
  2. 建立測試庫、測試表
  3. 封裝表資料用的 JavaBean 和 操作資料庫的 dao 介面

導包

log4j-1.2.17.jar
mybatis-3.4.1.jar
mysql-connector-java-5.1.37-bin.jar

建議匯入日誌包,如此,在 mybatis 關鍵環節就會有日誌列印。log4j.jar 還依賴類路徑下的一個 log4j.xml 的配置檔案。

配置檔案

全域性配置檔案

指導 MyBatis 如何正確執行。

<?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>
    <environments default="mysql">
        <environment id="mysql">
        <transactionManager type="JDBC"/>
        <!-- 配置連線池 -->
        <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///test"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        </dataSource>
        </environment>
    </environments>
    <!-- 註冊自己編寫的SQL對映檔案到全域性配置檔案中 -->
    <mappers>
        <!-- resource: 表示從類路徑下找資源 -->
        <mapper resource="EmployeeDao.xml"/>
    </mappers>
</configuration>

SQL 對映檔案

編寫介面中的每一個方法都如何向 DB 傳送 SQL 語句。這個配置檔案就相當於是介面的實現類。

<?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(名稱空間):寫介面的全類名,相當於告訴 MyBatis
這個配置檔案是實現哪個介面的 → [介面和XML繫結]
-->
<mapper namespace="cn.edu.nuist.dao.EmployeeDao">
    <!--
    public Employee getEmpByEid(Integer eid);
    ···········································
    select: 用來定義一個查詢操作
    id: 唯一識別符號,用來引用這條語句,需要和介面的方法名一致。相當於這個配置是對方法的實現
    resultType: 指定方法執行後的返回值型別(查詢操作必須指定)
    標籤體:SQL 語句
    -->
    <select id="getEmpByEid" resultType="cn.edu.nuist.bean.Employee">
        SELECT * FROM emp WHERE eid = #{eid}
    </select>

    <!-- public int updateEmp(Employee emp); -->
    <update id="updateEmp">
        UPDATE emp SET ename=#{ename}, email=#{email}, gender=#{gender} WHERE eid=#{eid}
    </update>

    <!-- public int deleteEmp(Integer eid); -->
    <delete id="deleteEmp">
        DELETE FROM emp WHERE eid=#{eid}
    </delete>

    <!-- public int insertEmp(Employee emp); -->
    <insert id="insertEmp">
        INSERT INTO emp(ename, gender, email) VALUES(#{ename}, #{gender}, #{email})
    </insert>
</mapper>

測試

public class HelloWorld {
    SqlSessionFactory sqlSessionFactory;

    @Before
    public void initSqlSessionFactory() {
        try {
            // 根據全域性配置檔案建立一個 SqlSessionFactory
            // SqlSessionFactory 是 SqlSession 的工廠,負責建立 SqlSession 物件
            sqlSessionFactory = new SqlSessionFactoryBuilder()
                    .build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws IOException {
        // 1. 得到 SqlSession 物件:SQL 會話,代表和 DB 的一次會話 // 類比 getConnection()
        // SqlSession openSession(boolean autoCommit);
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        // 2. 得到 dao 介面的實現
        EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);
        // 3. 測試CRUD (介面式程式設計)
        // 3.1 查詢
        Employee emp = employeeDao.getEmpByEid(1);
        // System.out.println("查詢:" + emp);
        // 3.2 更新
        emp.setEmail("[email protected]");
        employeeDao.updateEmp(emp);
        // sqlSession.commit(); // 不提交更新操作沒效果
        // 4. 關閉 SqlSession
        sqlSession.close();
    }
}
  • 若獲取 SqlSession 時用的是無參構造器並且在進行增刪改操作後沒有手動 sqlSession.commit()
  • openSession(true) 或者雖使用無參構造器但在操作完成後手動 sqlSession.commit()

@Test
public void test2() {
    SqlSession sqlSession = sqlSessionFactory.openSession(true);
    EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);
    // org.apache.ibatis.binding.MapperProxy@4566e5bd
    System.out.println(employeeDao);
    // class com.sun.proxy.$Proxy5
    System.out.println(employeeDao.getClass());
}
  • SqlSession 相當於 Connection,負責和 DB 進行互動
  • 和 Connection 一樣,都是非執行緒安全的。故和 DB 的每一次會話,就應該建立一個新的 SqlSession。
  • 由列印結果知,獲取到的是介面的動態代理物件,用該代理物件進行操作 DB。
  • 介面式程式設計
    • [原生] Dao<I> ← 實現 ← DaoImpl
    • [MyBatis] Mapper<I> ← 繫結 ← XxxMapper.xml

全域性配置檔案

文件的頂層結構如下:

properties

引入外部配置檔案

dbconfig.properties

jdbc.user=root
jdbc.password=root
jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.driverClass=com.mysql.jdbc.Driver

mybatis-config.xml

<!-- resource: 類路徑下;url: 網路|磁碟路徑下 -->
<properties resource="dbconfig.properties"></properties>
<environments default="mysql">
    <environment id="mysql">
        <transactionManager type="JDBC"/>
        <!-- 配置連線池 -->
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driverClass}"/>
            <property name="url" value="${jdbc.jdbcUrl}"/>
            <property name="username" value="${jdbc.user}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

如果屬性在不只一個地方進行了配置,那麼 MyBatis 將按照下面的順序來載入:

  1. 在 properties 元素體內指定的屬性首先被讀取。
  2. 然後根據 properties 元素中的 resource 屬性讀取類路徑下屬性檔案或根據 url 屬性指定的路徑讀取屬性檔案,並覆蓋已讀取的同名屬性。
  3. 最後讀取作為方法引數傳遞的屬性,並覆蓋已讀取的同名屬性。

因此,通過方法引數傳遞的屬性具有最高優先順序,resource/url 屬性中指定的配置檔案次之,最低優先順序的是 properties 屬性中指定的屬性。

settings

會改變 MyBatis 的執行時行為

以 mapUnderscoreToCamelCase(是否開啟自動駝峰命名規則對映,即從資料庫列名 A_COLUMN 到 Java 屬性名 aColumn 的類似對映)為例:

<settings>
    <!-- 預設 false -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

typeAliases

類型別名是為 Java 型別設定一個短的名字,可以方便我們引用某個類。

  • 通過 <typeAlias> 為一個 JavaBean 起別名;不寫 alias 屬性,則預設別名為簡單類名(不區分大小寫)。
    <typeAliases>
        <typeAlias type="cn.edu.nuist.bean.Department"/>
        <typeAlias type="cn.edu.nuist.bean.Employee" alias="emp"/>
    </typeAliases>
    
  • 通過 <package> 可以批量為這個包下的每一個類建立一個預設的別名,就是簡單類名(不區分大小寫)。
    <typeAliases>
        <package name="cn.edu.nuist.bean"/>
    </typeAliases>
    
  • 若想指定某類的別名,可在該類上加一個註解:@Alias("別名")

值得注意的是,MyBatis已經為許多常見的 Java 型別內建了相應的類型別名。它們都是大小寫不敏感的,我們在起別名的時候千萬不要佔用已有的別名。

typeHandlers

內建型別處理器

無論是 MyBatis 在預處理語句(PreparedStatement) 中設定一個引數時,還是從結果集中取出一個值時, 都會用型別處理器將獲取的值以合適的方式轉換成 Java 型別。


日期型別的處理:

  • 日期和時間的處理,JDK 1.8 以前一直是個頭疼的問題。我們通常使用 JSR310 規範領導者 Stephen Colebourne 建立的 Joda-Time 來操作。JDK 1.8 已經實現全部的 JSR310 規範了。
  • 日期時間處理上,我們可以使用 MyBatis 基於 JSR310(Date and Time API) 編寫的各種日期時間型別處理器。
  • MyBatis3.4 以前的版本需要我們手動註冊這些處理器,以後的版本都是自動註冊的。

自定義型別處理器

可以重寫型別處理器或建立你自己的型別處理器來處理不支援的或非標準的型別。

  1. 實現 org.apache.ibatis.type.TypeHandler<I> 或繼承 org.apache.ibatis.type.BaseTypeHandler
  2. 選擇性地將它對映到一個 JDBC 型別
  3. 在 mybatis 全域性配置檔案中註冊
    <typeHandlers>
        <typeHandler handler=""/>
    </typeHandlers>
    

plugins

外掛是 MyBatis 提供的一個非常強大的機制,我們可以通過外掛來修改 MyBatis 的一些核心行為。外掛通過動態代理機制,可以介入四大物件的任何一個方法的執行。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

environments

實際開發中我們使用 Spring 管理資料來源,並進行事務控制的配置來覆蓋下述配置。

  • MyBatis 可以配置多種環境,比如開發、測試和生產環境需要有不同的配置。
  • 每種環境使用一個 <environment> 進行配置並指定唯一識別符號 id。
  • 可以通過 <environments> 中的 default 屬性指定一個環境的識別符號來快速的切換環境。
  • 示例
    <environments default="mysql">
        <!--
        id 是當前環境的唯一標識
        每一個環境都需要一個事務管理器和一個數據源
         -->
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driverClass}"/>
                <property name="url" value="${jdbc.jdbcUrl}"/>
                <property name="username" value="${jdbc.user}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    
  • transactionManager —— type: JDBC | MANAGED | 自定義
    • JDBC:使用了 JDBC 的提交和回滾設定,依賴於從資料來源得到的連線來管理事務範圍
    • MANAGED:不提交或回滾一個連線、讓容器來管理事務的整個生命週期(比如 JEE 應用伺服器的上下文)
    • 自定義:實現 TransactionFactory<I>,type=全類名/別名
  • dataSource —— type: UNPOOLED | POOLED | JNDI | 自定義
    • UNPOOLED:不使用連線池
    • POOLED:使用連線池
    • JNDI: 在 EJB 或應用伺服器這類容器中查詢指定的資料來源
    • 自定義:實現 DataSourceFactory<I>,定義資料來源的獲取方式

databaseIdProvider

MyBatis 可以根據不同的資料庫廠商執行不同的語句。

<databaseIdProvider type="DB_VENDOR">
    <!-- name: 資料庫廠商標識 -->
    <!-- value: 為標識起一個別名,方便 SQL 語句標籤使用 databaseId 屬性引用 -->
    <property name="MySQL" value="mysql"/>
    <property name="SQL Server" value="sqlServer"/>
    <property name="Oracle" value="orcl"/>
</databaseIdProvider>
  • typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
  • 會通過 DatabaseMetaData#getDatabaseProductName() 返回的字串進行設定。由於通常情況下這個字串都非常長而且相同產品的不同版本會返回不同的值,所以最好通過設定屬性別名來使其變短
  • 使用 MyBatis 提供的 VendorDatabaseIdProvider 解析資料庫廠商標識,也可以通過實現 DatabaseIdProvider<I> 來自定義

MyBatis匹配規則如下:

  1. 如果沒有配置 databaseIdProvider 標籤,那麼 databaseId=null
  2. 如果配置了 databaseIdProvider 標籤,使用標籤配置的 name 去匹配資料庫資訊,匹配上設定 databaseId=配置指定的值,否則依舊為 null
  3. 如果 databaseId 不為 null,他只會找到配置 databaseId 的 sql 語句
  4. MyBatis 會載入不帶 databaseId 屬性和帶有匹配當前資料庫 databaseId 屬性的所有語句。如果同時找到帶有 databaseId 和不帶databaseId 的相同語句,則後者會被捨棄(精準匹配)。

mapper 對映

逐個註冊

<mappers>
    <mapper resource="EmployeeDao.xml"/>
    <mapper class="file:///D:/EmployeeDao.xml"/>
    <mapper class="cn.edu.nuist.dao.EmployeeDao"/>
</mappers>
  • url:從磁碟|網路路徑下引用 xml
  • resource:從類路徑下引用 xml
  • class:引用介面;直接使用會拋異常
    • 【有 SQL 對映檔案】需要將對映檔案放入介面所在包下(在同一目錄下),且二者名字相同
    • 【無 SQL 對映檔案】將 @Select / @Update / @Delete / @Insert 直接寫在介面對應的方法上,註解的 value 屬性值為對應的 SQL 語句

批量註冊

要求 SQL 對映檔名必須和介面名相同並且在同一目錄下。

<mapper>
    <package name="cn.edu.nuist.dao"/>
</mappers>

SQL 對映檔案

對映檔案指導著 MyBatis 如何進行資料庫增刪改查。

  • cache – 名稱空間的二級快取配置
  • cache-ref – 其他名稱空間快取配置的引用
  • resultMap – 自定義結果集對映
  • parameterMap – 已廢棄!老式風格的引數對映
  • sql –抽取可重用語句塊
  • insert – 對映插入語句
  • update – 對映更新語句
  • delete – 對映刪除語句
  • select – 對映查詢語句

增刪改標籤

MyBatis 允許介面的增刪改方法直接定義 int、long、boolean 及其包裝類型別的返回值。

  • useGeneratedKeys 和 keyProperty 配合使用可以實現獲取插入資料後的自增主鍵 eid。
    <!--
    在支援主鍵自增的資料庫:
        useGeneratedKeys="true" 使用自增主鍵獲取主鍵值策略
            底層呼叫了原生JDBC獲取自增主鍵的方法:ResultSet getGeneratedKeys()
        keyProperty="eid" 將剛才自增的主鍵值封裝給 JavaBean 的哪個屬性
    -->
    <insert id="insertEmp" useGeneratedKeys="true" keyProperty="eid">
        INSERT INTO emp(ename, gender, email) VALUES(#{ename}, #{gender}, #{email})
    </insert>
    
  • 對於不支援自增型主鍵的資料庫(例如 Oracle),則可以使用 selectKey 子元素:selectKey 元素將會首先執行,eid 會被設定,然後插入語句才會被呼叫。
    <!-- 不支援主鍵自增的資料庫 -->
    <insert id="insertEmp2" databaseId="orcl">
        <!-- 提前查詢自增主鍵列的最大值+1 -->
        <!--
        order="BEFORE" 當前標籤體中的 SQL 在 insert-SQL 之前執行
        keyProperty 指出查出的主鍵值賦值給 JavaBean 的哪個屬性
        resultType 查詢結果的資料型別
        -->
        <selectKey order="BEFORE" resultType="int" keyProperty="eid">
            SELECT EMP_SEQ.nextval FROM dual;
        </selectKey>
        INSERT INTO emp(eid, ename, gender, email)
                VALUES(#{eid}, #{ename}, #{gender}, #{email})
    </insert>
    ·····································································
    <insert id="insertEmp3" databaseId="orcl">
        <!--
        order="AFTER" 當前標籤體中的 SQL 在 insert-SQL 之後執行
        先執行插入SQL → 再拿出當前的主鍵值賦給 JavaBean 的指定屬性
        可能會有問題;還是用 BEFORE 吧
        -->
        <selectKey order="AFTER" resultType="int" keyProperty="eid">
            SELECT EMP_SEQ.curravl FROM dual;
        </selectKey>
        INSERT INTO emp(eid, ename, gender, email)
                VALUES(EMP_SEQ.nextval, #{ename}, #{gender}, #{email})
    </insert>
    

查詢標籤

引數傳遞

  • 單個引數,可以接受基本型別,物件型別,集合型別的值。這種情況 MyBatis 可直接使用這個引數,不需要經過任何處理。
    • 基本型別:#{隨便寫}
    • POJO:{POJO的屬性名}
    • 集合型別/陣列 → (特殊處理) → 封裝進 Map 中,key 為型別名小寫,如 collection、list、array
  • 多個引數,以 Employee getEmpByEidAndEname(eid, ename) 為例
    • #{引數名} 方式無效,直接丟擲異常 ↓;得使用 #{0}, #{param1}
      BindingException: Parameter 'eid' not found.
      Available parameters are [0, 1, param1, param2]
      
    • 原因:只要傳入多個引數,MyBatis 會重新包裝成一個 Map 傳入,而封裝時使用的 key 就是引數索引和引數個數
    • 【命名引數】除了按照指定 key 獲取引數外,還可以告訴 Mybatis 在封裝引數 map 的時候使用我們自己指定的 key:在介面方法的形參上加 @Param("指定的key")
  • 引數為 map:既然都已經將引數封裝好了,那就直接 #{key} 即可

使用場景:

  1. 多個引數正好是業務邏輯的模型資料,直接傳入 POJO
  2. 多個引數不是業務邏輯的模型資料,沒有對應的 POJO,不經常使用,為了方便,傳入 Map
  3. 多個引數不是業務模型中的資料,但是經常要使用,比如分頁,推薦編寫一個 TO(Transfer Object) 資料傳輸物件。

ParamNameResolver

private static final String GENERIC_NAME_PREFIX = "param";

private final SortedMap<Integer, String> names;

private boolean hasParamAnnotation;

public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        if (isSpecialParameter(paramTypes[paramIndex])) {
            // skip special parameters
            continue;
        }
        String name = null;
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            // 獲取每個標註了@Param註解的引數,將註解的value值賦給name變數
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }
        if (name == null) {
            // @Param was not specified.
            // 全域性配置: useActualParamName(JDK 8),name=引數名
            if (config.isUseActualParamName()) {
                name = getActualParamName(method, paramIndex);
            }
            if (name == null) {
                // use the parameter index as the name ("0", "1", ...)
                // gcode issue #71
                // name中儲存當前引數的索引
                name = String.valueOf(map.size());
            }
        }
        // 儲存進 map
        map.put(paramIndex, name);
    }
    // 繼而儲存到 names 中
    names = Collections.unmodifiableSortedMap(map);
}

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // 沒有引數,方法直接返回
    if (args == null || paramCount == 0) {
        return null;
    // 引數列表上沒有@param註解 && 引數只有一個
    } else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()]; // 單個引數直接返回
    } else {
        // 遍歷構造器初始化好的 names{0=0, 1=1}
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // names 的 value 作為 key,args[names 的 key] 作為 value
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            // #{param1}, #{param2} ... 能拿到的原因 ↓
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

思考:

method(Integer id, @Param("e") Employee emp)
    > id → #{param1}
    > lastName → #{param2.lastName} | #{e.lastName}
method(@Param("eid")Integer eid, @Param("ename")String ename, Employee emp)
    > Integer eid → #{id} | #{param1}
    > String ename → #{param2}
    > Employee emp(email) → #{param3.email}
method(List<Integer> ids)
    > 第一個 id 的值 → #{list[0]}

#{} 和 ${} 取值

  • #{key}:獲取引數的值,預編譯到 SQL 中。安全。
  • ${key}:獲取引數的值,拼接到 SQL 中。有 SQL 注入問題

SQL 語句不是什麼位置都支援預編譯的。比如 FROM 的表名、ORDER BY 的欄位,,這些地方都不行。這時候,如果要向這些位置傳遞引數,就必須要用 ${} 的方式:ORDER BY ${paramName}; FROM ${tableName}


#{...} 更豐富的用法:引數位置支援的屬性

javaType、jdbcType、mode、numericScale、resultMap、typeHandler、jdbcTypeName、expression

但實際上通常被設定的只有:可能為空的列名指定 jdbcType

  • 【情景】在資料為 null 的時候,有些 DB 可能不識別 Mybatis 對 null 的預設處理。比如:Oracle,會拋 TypeException: Error setting null for parameter #3 with JdbcType OTHER.
  • 檢視官方文件 (解決辦法1:修改全域性配置檔案)
  • 檢視列舉 JdbcType (解決辦法2:修改 SQL 語句)

select 標籤

  • test1

    <!-- public List<Employee> getAllEmps() -->
    <!-- 方法返回值如果是集合型別,值應該是集合元素的型別! -->
    <select id="getAllEmps" resultType="cn.edu.nuist.bean.Employee">
        SELECT * FROM emp
    </select>
    
    查詢多條記錄→ resultType="Employee" → 查詢結果封裝到一個 List 裡
    
  • test2

    <!-- public Map<String, Object> getEmpByEidRetMap(Integer id) -->
    <select id="getEmpByEidRetMap" resultType="map">
        SELECT * FROM emp WHERE eid=#{eid}
    </select>
    
    查詢單條記錄 → resultType="map" → 封裝到一個 Map 裡,欄位名為 key,欄位值為 value
    
  • test3

    <!--
        @MapKey("eid")
        public Map<Integer, Employee> getAllEmpsRetMap()
    -->
    <select id="getAllEmpsRetMap" resultType="map">
        SELECT * FROM emp
    </select>
    
    查詢多條記錄 → resultType="map" → 查詢結果封裝到一個 Map 裡 (依照 @MapKey
    註解可知,結果所封裝的Map中的 key 為 eid 欄位的值,但 value 也是一個 [Map:
    每條記錄的欄位名為 key,欄位值為 value],value 的封裝同 test2
    --------------
    插一句:這個查詢結果的實際型別是 Map<Integer, HashMap<String, Object>>,
    可是方法宣告返回值是 Map<Integer, Employee> 鴨?這是怎麼賦上值的?都說無反
    射無框架,所以底層是用反射繞過泛型檢查賦上的嗎...
    --------------
    所以,resultMap 要寫元素型別 → resultType="Employee"
    

resultMap

自定義對映規則

預設 MyBatis 自動封裝結果集時是按照列名和屬性名一一對應(忽略大小寫) 的規則;現有一種情況,列名和屬性名不一樣,也不允許使用別名,這要怎麼對應呢?→ 使用 resultMap(自定義結果集),即自己定義表列名和物件屬性名的對映規則。

  • 簡單屬性:對映一個單獨列的值到簡單資料型別(字串/整型/雙精度浮點數/日期等) 的屬性或欄位。
    <!-- public Employee getEmpByEid(Integer eid); -->
    <select id="getEmpByEid" resultMap="empMap">
        SELECT * FROM emp WHERE eid = #{baishizhu}
    </select>
    
    <resultMap type="cn.edu.nuist.bean.Employee" id="empMap">
        <!-- 主鍵列對映規則(用 result 也能映射出來;但用 id 的話,底層會有優化) -->
        <id column="emp_id" property="eid"/>
        <!-- 普通欄位列對映規則 -->
        <result column="emp_email" property="email"/>
        <result column="emp_gender" property="gender"/>
        <result column="emp_ename" property="ename"/>
    </resultMap>
    
    <!-- public Employee getEmpWithDeptByEid(Integer eid); -->
    <select id="getEmpWithDeptByEid" resultMap="empWithDeptMap">
        SELECT emp.*, dept.dname FROM emp LEFT JOIN dept
        ON emp.did = dept.did WHERE eid=#{eid}
    </select>
    
    <resultMap type="cn.edu.nuist.bean.Employee" id="empWithDeptMap">
        <!-- 主鍵列對映規則 -->
        <id column="eid" property="eid"/>
        <!-- 普通欄位列對映規則 -->
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <result column="ename" property="ename"/>
        <!-- 複雜物件對映:使用級聯屬性方式封裝聯合查詢的結果 -->
        <result column="did" property="dept.did"/>
        <result column="dname" property="dept.dname"/>
    </resultMap>
    
  • 複雜物件對映,POJO 中的屬性可能會是一個物件,除了以級聯屬性的方式封裝物件外,還可以使用 association 標籤定義物件的封裝規則。
    <resultMap type="cn.edu.nuist.bean.Employee" id="empWithDeptMap">
        <id column="eid" property="eid"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <result column="ename" property="ename"/>
        <!-- association 可以指定 property 關聯的 javaType 的封裝規則 -->
        <association property="dept" javaType="cn.edu.nuist.bean.Department">
            <id column="did" property="did"/>
            <result column="dname" property="dname"/>
        </association>
    </resultMap>
    
  • 複雜物件對映,POJO 中的屬性可能會是一個集合。
    <!-- public Department getDeptByDid(Integer did) -->
    <select id="getDeptByDid" resultMap="deptWithEmps">
        SELECT emp.*, dept.dname FROM dept LEFT JOIN emp
        ON dept.did = emp.did WHERE dept.did = #{did}
    </select>
    
    <resultMap type="cn.edu.nuist.bean.Department" id="deptWithEmps">
        <id column="did" property="did"/>
        <result column="dname" property="dname"/>
        <!--
        collection 定義關聯的集合型別屬性其元素的封裝規則
            ofType: collection 中元素的型別
            property: 集合型別屬性名
        -->
        <collection property="empList" ofType="cn.edu.nuist.bean.Employee">
            <id column="eid" property="eid"/>
            <result column="email" property="email"/>
            <result column="gender" property="gender"/>
            <result column="ename" property="ename"/>
        </collection>
    </resultMap>
    

分步查詢

EmployeeDao.xml

<!-- public Employee getEmpByEid(Integer eid); -->
<select id="getEmpByEid" resultMap="empMap">
    SELECT * FROM emp WHERE eid = #{baishizhu}
</select>

<resultMap type="cn.edu.nuist.bean.Employee" id="empMap">
    <!-- 主鍵列對映規則 -->
    <id column="eid" property="eid"/>
    <!-- 普通欄位列對映規則 -->
    <result column="email" property="email"/>
    <result column="gender" property="gender"/>
    <result column="ename" property="ename"/>
    <!-- >>> 分步查詢 <<<
        select: 分步查詢SQL的id
        column: 分步查詢SQL需要的引數(得是上一步的查詢結果中的某個欄位名)
        property: 分步查詢的結果所要儲存到的 resultMap-type 物件的哪個屬性中
    -->
    <association column="did" property="dept"
        select="cn.edu.nuist.dao.DepartmentDao.getDeptInfoByDid">
    </association>
</resultMap>

DepartmentDao.xml

<!-- public Department getDeptInfoByDid(Integer did); -->
<select id="getDeptInfoByDid" resultType="cn.edu.nuist.bean.Department">
    SELECT * FROM dept WHERE did = #{did}
</select>

分步查詢 SQL 需要多個引數,該怎麼傳呢?
將多列的值封裝成 map 傳遞,即:column="{key1=column1, key2=column2, ...}"。key 是分步查詢 SQL 語句 #{...} 中的值,column 就是我們上一步查詢結果的某個欄位名。

延遲載入

相關全域性設定:

  • lazyLoadingEnabled:延遲載入的全域性開關。當開啟時,所有關聯物件都會延遲載入。 特定關聯關係中可通過設定 fetchType="eager | lazy" 屬性來覆蓋該項的開關狀態。預設值:false
  • aggressiveLazyLoading: 當啟用時,對任意延遲屬性的呼叫會使帶有延遲載入屬性的物件完整載入;反之,每種屬性將會按需載入。預設值:true

discriminator

鑑別器。MyBatis 可以使用 discriminator 判斷某個值,然後根據某列的值改變封裝行為。

要求:如果查出來的 Employee為女性,其 Department 資訊也要查出來;如果是男性,不查部門資訊,且 email 屬性值設為ename

<resultMap type="cn.edu.nuist.bean.Employee" id="empMap">
    <!-- 主鍵列對映規則 -->
    <id column="eid" property="eid"/>
    <!-- 普通欄位列對映規則 -->
    <result column="email" property="email"/>
    <result column="gender" property="gender"/>
    <result column="ename" property="ename"/>
    <!--
        column: 指定判定的列名
        javaType: 列值對應的Java型別
    -->
    <discriminator javaType="integer" column="gender">
        <!-- resultType/resultMap 不能少! -->
        <case value="0" resultType="cn.edu.nuist.bean.Employee">
            <association column="did" property="dept"
                select="cn.edu.nuist.dao.DepartmentDao.getDeptInfoByDid">
            </association>
        </case>
        <case value="1" resultType="cn.edu.nuist.bean.Employee">
            <result column="ename" property="email"/>
        </case>
    </discriminator>
</resultMap>