Mybatis學(xué)習(xí)筆記

官方文檔

一、 JDBC回顧

JDBC的使用過程:

register the JDBC driver 加載驅(qū)動(dòng)
open a connection 打開連接
execute using statment, including binding params 執(zhí)行statment并綁定參數(shù)
extract data from result set (only select) 從結(jié)果集中獲取數(shù)據(jù)
close database resources 關(guān)閉連接

JDBC具有的問題:

代碼與sql語句耦合
動(dòng)態(tài)組裝sql語句處理繁瑣
數(shù)據(jù)對(duì)象與結(jié)果集的映射與代碼耦合
需要關(guān)心數(shù)據(jù)庫資源的釋放

二讯私、 MyBatis的簡介

MyBatis是一個(gè)支持普通 SQL查詢辆影,存儲(chǔ)過程和高級(jí)映射的優(yōu)秀持久層框架颖侄;MyBatis 消除了幾乎所有的JDBC代碼和參數(shù)的手工設(shè)置以及結(jié)果集的檢索航邢。MyBatis 使用簡單的 XML或注解用于配置和原始映射躏升,將接口和 Java 的POJOs(Plain Old Java Objects酝静,普通的 Java對(duì)象)映射成數(shù)據(jù)庫中的記錄节榜。

依賴
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
</dependency>
術(shù)語解釋
  • SqlSessionFactoryBuilder

  • SqlSessionFactory

  • SqlSession
    (1)每個(gè)基于 MyBatis 的應(yīng)用都是以一個(gè) SqlSessionFactory 的實(shí)例為中心的。SqlSessionFactory 的實(shí)例可以通過 SqlSessionFactoryBuilder 獲得别智。而 SqlSessionFactoryBuilder 則可以從 XML 配置文件或一個(gè)預(yù)先定制的 Configuration 的實(shí)例構(gòu)建出 SqlSessionFactory 的實(shí)例宗苍。
    (2)SqlSessionFactory可以在xml配置文件中構(gòu)建,也可以使用代碼構(gòu)建薄榛。
    (3)三者的作用域(Scope)和生命周期:不同作用域和生命周期類是至關(guān)重要的讳窟,因?yàn)殄e(cuò)誤的使用會(huì)導(dǎo)致非常嚴(yán)重的并發(fā)問題:
    SqlSessionFactoryBuilder:這個(gè)類可以被實(shí)例化、使用和丟棄敞恋,一旦創(chuàng)建了 SqlSessionFactory丽啡,就不再需要它了。因此 SqlSessionFactoryBuilder 實(shí)例的最佳作用域是方法作用域(也就是局部方法變量)硬猫。你可以重用 SqlSessionFactoryBuilder 來創(chuàng)建多個(gè) SqlSessionFactory 實(shí)例补箍,但是最好還是不要讓其一直存在以保證所有的 XML 解析資源開放給更重要的事情。
    SqlSessionFactory:SqlSessionFactory 一旦被創(chuàng)建就應(yīng)該在應(yīng)用的運(yùn)行期間一直存在啸蜜,沒有任何理由對(duì)它進(jìn)行清除或重建坑雅。使用 SqlSessionFactory 的最佳實(shí)踐是在應(yīng)用運(yùn)行期間不要重復(fù)創(chuàng)建多次,多次重建 SqlSessionFactory 被視為一種代碼“壞味道(bad smell)”衬横。因此 SqlSessionFactory 的最佳作用域是應(yīng)用作用域霞丧。有很多方法可以做到,最簡單的就是使用單例模式或者靜態(tài)單例模式冕香。
    SqlSession:每個(gè)線程都應(yīng)該有它自己的 SqlSession 實(shí)例蛹尝。SqlSession 的實(shí)例不是線程安全的,因此是不能被共享的悉尾,所以它的最佳的作用域是請(qǐng)求或方法作用域突那。絕對(duì)不能將 SqlSession 實(shí)例的引用放在一個(gè)類的靜態(tài)域,甚至一個(gè)類的實(shí)例變量也不行构眯。也絕不能將 SqlSession 實(shí)例的引用放在任何類型的管理作用域中愕难,比如 Serlvet 架構(gòu)中的 HttpSession。如果你現(xiàn)在正在使用一種 Web 框架惫霸,要考慮 SqlSession 放在一個(gè)和 HTTP 請(qǐng)求對(duì)象相似的作用域中猫缭。換句話說,每次收到的 HTTP 請(qǐng)求壹店,就可以打開一個(gè) SqlSession猜丹,返回一個(gè)響應(yīng),就關(guān)閉它硅卢。這個(gè)關(guān)閉操作是很重要的射窒,你應(yīng)該把這個(gè)關(guān)閉操作放到 finally 塊中以確保每次都能執(zhí)行關(guān)閉藏杖。

  • mybatis 配置文件(xml,全局設(shè)置脉顿、數(shù)據(jù)庫連接信息等)

      <?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="development">
              <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <property name="driver" value="${driver}"/>
                      <property name="url" value="${url}"/>
                      <property name="username" value="${username}"/>
                      <property name="password" value="${password}"/>
                  </dataSource>
              </environment>
          </environments>
          <mappers>
              <mapper resource="org/mybatis/example/BlogMapper.xml"/>
          </mappers>
      </configuration>
    
  • sql mapper文件(存放需要執(zhí)行的sql語句)

三蝌麸、 XML配置文件

Mybatis使用xml配置文件主要有兩個(gè)地方:Mybatis配置和Mapper映射配置。具體可以參考官方文檔艾疟,說得非常詳細(xì)来吩。

properties
配置屬性

settings
這是 MyBatis 中極為重要的調(diào)整設(shè)置,它們會(huì)改變 MyBatis 的運(yùn)行時(shí)行為蔽莱,比如是否使用緩存弟疆、是否延遲加載、是否允許單一語句返回多結(jié)果集碾褂、是否允許 JDBC 支持自動(dòng)生成主鍵等;一個(gè)例子:

    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
        <setting name="useColumnLabel" value="true"/>
        <setting name="useGeneratedKeys" value="false"/>
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <setting name="defaultStatementTimeout" value="25"/>
        <setting name="defaultFetchSize" value="100"/>
        <setting name="safeRowBoundsEnabled" value="false"/>
        <setting name="mapUnderscoreToCamelCase" value="false"/>
        <setting name="localCacheScope" value="SESSION"/>
        <setting name="jdbcTypeForNull" value="OTHER"/>
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
    </settings>

typeAliases
類型別名是為 Java 類型設(shè)置一個(gè)短的名字历葛。它只和 XML 配置有關(guān)正塌,存在的意義僅在于用來減少類完全限定名的冗余。

typeHandlers
用來指定所使用的類型處理器恤溶。無論是 MyBatis 在預(yù)處理語句(PreparedStatement)中設(shè)置一個(gè)參數(shù)時(shí)乓诽,還是從結(jié)果集中取出一個(gè)值時(shí), 都會(huì)用類型處理器將獲取的值以合適的方式轉(zhuǎn)換成 Java 類型咒程。

objectFactory(對(duì)象工廠)
MyBatis 每次創(chuàng)建結(jié)果對(duì)象的新實(shí)例時(shí)鸠天,它都會(huì)使用一個(gè)對(duì)象工廠(ObjectFactory)實(shí)例來完成。 默認(rèn)的對(duì)象工廠需要做的僅僅是實(shí)例化目標(biāo)類帐姻,要么通過默認(rèn)構(gòu)造方法稠集,要么在參數(shù)映射存在的時(shí)候通過參數(shù)構(gòu)造方法來實(shí)例化。 如果想覆蓋對(duì)象工廠的默認(rèn)行為饥瓷,則可以通過創(chuàng)建自己的對(duì)象工廠來實(shí)現(xiàn)剥纷。

plugins(插件)
MyBatis 允許你在已映射語句執(zhí)行過程中的某一點(diǎn)進(jìn)行攔截調(diào)用,默認(rèn)情況下呢铆,MyBatis 允許使用插件來攔截的方法調(diào)用晦鞋。

environments(配置環(huán)境)
MyBatis 可以配置成適應(yīng)多種環(huán)境,這種機(jī)制有助于將 SQL 映射應(yīng)用于多種數(shù)據(jù)庫之中棺克, 現(xiàn)實(shí)情況下有多種理由需要這么做悠垛。例如,開發(fā)娜谊、測試和生產(chǎn)環(huán)境需要有不同的配置确买;或者共享相同 Schema 的多個(gè)生產(chǎn)數(shù)據(jù)庫, 想使用相同的 SQL 映射纱皆。許多類似的用例拇惋。
不過要記字苜恕:盡管可以配置多個(gè)環(huán)境,每個(gè) SqlSessionFactory 實(shí)例只能選擇其一撑帖。

mappers(映射器)
既然 MyBatis 的行為已經(jīng)由上述元素配置完了蓉坎,我們現(xiàn)在就要定義 SQL 映射語句了。但是首先我們需要告訴 MyBatis 到哪里去找到這些語句胡嘿。 Java 在自動(dòng)查找這方面沒有提供一個(gè)很好的方法蛉艾,所以最佳的方式是告訴 MyBatis 到哪里去找映射文件。你可以使用相對(duì)于類路徑的資源引用衷敌, 或完全限定資源定位符(包括 file:/// 的 URL)勿侯,或類名和包名等

四、XML映射文件

MyBatis 的真正強(qiáng)大在于它的映射語句缴罗,也是它的魔力所在助琐。由于它的異常強(qiáng)大,映射器的 XML 文件就顯得相對(duì)簡單面氓。如果拿它跟具有相同功能的 JDBC 代碼進(jìn)行對(duì)比兵钮,你會(huì)立即發(fā)現(xiàn)省掉了將近 95% 的代碼。MyBatis 就是針對(duì) SQL 構(gòu)建的舌界,并且比普通的方法做的更好掘譬。

select
select 元素有很多屬性允許你配置,來決定每條語句的作用細(xì)節(jié)呻拌。

<select
        id="selectPerson" ->> 在命名空間中唯一的標(biāo)識(shí)符葱轩,可以被用來引用這條語句
        parameterType="int" ->> 將會(huì)傳入這條語句的參數(shù)類的完全限定名或別名。這個(gè)屬性是可選的藐握,因?yàn)?MyBatis 可以通過 TypeHandler 推斷出具體傳入語句的參數(shù)靴拱,默認(rèn)值為 unset。
        resultType="hashmap" ->> 從這條語句中返回的期望類型的類的完全限定名或別名猾普。注意如果是集合情形缭嫡,那應(yīng)該是集合可以包含的類型,而不能是集合本身抬闷。使用 resultType 或 resultMap妇蛀,但不能同時(shí)使用。
        resultMap="personResultMap" ->> 外部 resultMap 的命名引用笤成。結(jié)果集的映射是 MyBatis 最強(qiáng)大的特性评架,對(duì)其有一個(gè)很好的理解的話,許多復(fù)雜映射的情形都能迎刃而解炕泳。使用 resultMap 或 resultType纵诞,但不能同時(shí)使用。
        flushCache="false" ->> 將其設(shè)置為 true培遵,任何時(shí)候只要語句被調(diào)用浙芙,都會(huì)導(dǎo)致本地緩存和二級(jí)緩存都會(huì)被清空登刺,默認(rèn)值:false。
        useCache="true" ->> 將其設(shè)置為 true嗡呼,將會(huì)導(dǎo)致本條語句的結(jié)果被二級(jí)緩存纸俭,默認(rèn)值:對(duì) select 元素為 true
        timeout="10000" ->> 這個(gè)設(shè)置是在拋出異常之前,驅(qū)動(dòng)程序等待數(shù)據(jù)庫返回請(qǐng)求結(jié)果的秒數(shù)南窗。默認(rèn)值為 unset(依賴驅(qū)動(dòng))揍很。
        fetchSize="256" ->> 這是嘗試影響驅(qū)動(dòng)程序每次批量返回的結(jié)果行數(shù)和這個(gè)設(shè)置值相等。默認(rèn)值為 unset(依賴驅(qū)動(dòng))万伤。
        statementType="PREPARED" ->> STATEMENT窒悔,PREPARED 或 CALLABLE 的一個(gè)。這會(huì)讓 MyBatis 分別使用 Statement敌买,PreparedStatement 或 CallableStatement简珠,默認(rèn)值:PREPARED。
        resultSetType="FORWARD_ONLY" ->> FORWARD_ONLY虹钮,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個(gè)聋庵,默認(rèn)值為 unset (依賴驅(qū)動(dòng))。
        >

insert, update 和 delete
數(shù)據(jù)變更語句 insert芜抒,update 和 delete 的實(shí)現(xiàn)非常接近:

<insert
    id="insertAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    keyProperty=""
    keyColumn=""
    useGeneratedKeys=""
    timeout="20"></insert>

<update
    id="updateAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    timeout="20"></update>

<delete
    id="deleteAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    timeout="20"></delete>

如前所述珍策,插入語句的配置規(guī)則更加豐富托启,在插入語句里面有一些額外的屬性和子元素用來處理主鍵的生成宅倒,而且有多種生成方式。

  • 如果你的數(shù)據(jù)庫支持自動(dòng)生成主鍵的字段(比如 MySQL 和 SQL Server)屯耸,那么你可以設(shè)置 useGeneratedKeys=”true”拐迁,然后再把 keyProperty 設(shè)置到目標(biāo)屬性上就OK了。例如:

    <insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
      insert into Author (username,password,email,bio)
      values (#{username},#{password},#{email},#{bio})
    </insert>
    

這樣就可以將自動(dòng)生成的主鍵綁定到Author對(duì)象的id屬性上了疗绣。

  • 如果你的數(shù)據(jù)庫還支持多行插入, 你也可以傳入一個(gè)Authors數(shù)組或集合线召,并返回自動(dòng)生成的主鍵。

    <insert id="insertAuthor" useGeneratedKeys="true"  keyProperty="id">
      insert into Author (username, password, email, bio) values
      <foreach item="item" collection="list" separator=",">
        (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
      </foreach>
    </insert>
    

sql
這個(gè)元素可以被用來定義可重用的 SQL 代碼段多矮,可以包含在其他語句中缓淹。它可以被靜態(tài)地(在加載參數(shù)) 參數(shù)化. 不同的屬性值通過包含的實(shí)例變化. 比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

這個(gè) SQL 片段可以被包含在其他語句中,例如:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

參數(shù)(Parameters)
參數(shù)是 MyBatis 非常強(qiáng)大的功能塔逃;

  • 例子1:

    <select id="selectUsers" resultType="User">
      select id, username, password
      from users
      where id = #{id}
    </select>
    

上面的這個(gè)示例說明了一個(gè)非常簡單的命名參數(shù)映射讯壶。參數(shù)類型被設(shè)置為 int,這樣這個(gè)參數(shù)就可以被設(shè)置成任何內(nèi)容湾盗。

  • 例子2:

    <insert id="insertUser" parameterType="User">
      insert into users (id, username, password)
      values (#{id}, #{username}, #{password})
    </insert>
    

如果 User 類型的參數(shù)對(duì)象傳遞到了語句中伏蚊,id、username 和 password 屬性將會(huì)被查找格粪,然后將它們的值傳入預(yù)處理語句的參數(shù)中躏吊。這點(diǎn)對(duì)于向語句中傳參是比較好的而且又簡單氛改,不過參數(shù)映射的功能遠(yuǎn)不止于此。

  • 參數(shù)的屬性

像 MyBatis 的其他部分一樣比伏,參數(shù)也可以指定一個(gè)特殊的數(shù)據(jù)類型胜卤。

  例子1 : #{property,javaType=int,jdbcType=NUMERIC}
  例子2 : #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
  例子3 :#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

  javaType 通常可以從參數(shù)對(duì)象中來去確定凳怨,前提是只要對(duì)象不是一個(gè) HashMap瑰艘。那么 javaType 應(yīng)該被確定來保證使用正確類型處理器。盡管看起來配置變得越來越繁瑣肤舞,但實(shí)際上是很少去設(shè)置它們紫新。
  numericScale 對(duì)于數(shù)值類型,還有一個(gè)小數(shù)保留位數(shù)的設(shè)置李剖,來確定小數(shù)點(diǎn)后保留的位數(shù)芒率。
  mode 屬性允許你指定 IN,OUT 或 INOUT 參數(shù)篙顺。如果參數(shù)為 OUT 或 INOUT偶芍,參數(shù)對(duì)象屬性的真實(shí)值將會(huì)被改變,就像你在獲取輸出參數(shù)時(shí)所期望的那樣德玫。如果 mode 為 OUT(或 INOUT)匪蟀,而且 jdbcType 為 CURSOR(也就是 Oracle 的 REFCURSOR),你必須指定一個(gè) resultMap 來映射結(jié)果集到參數(shù)類型宰僧。要注意這里的 javaType 屬性是可選的材彪,如果左邊的空白是 jdbcType 的 CURSOR 類型,它會(huì)自動(dòng)地被設(shè)置為結(jié)果集琴儿。
  • 字符串替換

默認(rèn)情況下,使用#{}格式的語法會(huì)導(dǎo)致 MyBatis 創(chuàng)建預(yù)處理語句屬性并安全地設(shè)置值(比如?)段化。這樣做更安全,更迅速造成,通常也是首選做法显熏,不過有時(shí)你只是想直接在 SQL 語句中插入一個(gè)不改變的字符串。比如晒屎,像 ORDER BY喘蟆,你可以這樣來使用:

  ORDER BY ${columnName}
  這里 MyBatis 不會(huì)修改或轉(zhuǎn)義字符串。

注意:以這種方式接受從用戶輸出的內(nèi)容并提供給語句中不變的字符串是不安全的鼓鲁,會(huì)導(dǎo)致潛在的 SQL 注入攻擊蕴轨,因此要么不允許用戶輸入這些字段,要么自行轉(zhuǎn)義并檢驗(yàn)坐桩。

Result Maps
resultMap 元素是 MyBatis 中最重要最強(qiáng)大的元素尺棋。它就是讓你遠(yuǎn)離 90%的需要從結(jié)果集中取出數(shù)據(jù)的 JDBC 代碼的那個(gè)東西, 而且在一些情形下允許你做一些 JDBC 不支持的事情。 事實(shí)上, 編寫相似于對(duì)復(fù)雜語句聯(lián)合映射這些等同的代碼, 也許可以跨過上千行的代碼。 ResultMap 的設(shè)計(jì)就是簡單語句不需要明確的結(jié)果映射,而很多復(fù)雜語句確實(shí)需要描述它們的關(guān)系膘螟。

resultMap用來映射查詢結(jié)果成福,不論是否在標(biāo)簽屬性中顯示的指定,MyBatis 會(huì)在幕后自動(dòng)創(chuàng)建一個(gè) ResultMap,基于屬性名來映射列到 JavaBean 的屬性上荆残。

我將其概括為3種形式:

  • (1)指定resultType為map奴艾,將所有列被自動(dòng)映射到 HashMap 的鍵上:
    <select id="selectUsers" resultType="map">
    select id, username, hashedPassword
    from some_table
    where id = #{id}
    </select>
    這在很多情況下是有用的,但是 HashMap 不能很好描述一個(gè)領(lǐng)域模型,所以我們還有以下幾種映射方式内斯。

  • (2)指定resultType為JavaBean或 POJO蕴潦,查詢結(jié)果自動(dòng)映射到j(luò)ava類屬性上,映射規(guī)則是基于屬性名來映射列到 JavaBean 的屬性上
    <select id="selectUsers" resultType="com.someapp.model.User">
    select id, username, hashedPassword
    from some_table
    where id = #{id}
    </select>
    也許會(huì)受限于映射規(guī)則俘闯,不過如果列名沒有精確匹配,你可以在列名上使用 select 字句的別名(AS潭苞,一個(gè)基本的SQL特性)來匹配標(biāo)簽:
    <select id="selectUsers" resultType="User">
    select
    user_id as "id",
    user_name as "userName",
    hashed_password as "hashedPassword"
    from some_table
    where id = #{id}
    </select>

  • (3)使用外部的 resultMap,這也是解決列名不匹配的另外一種方式
    1. 定義外部resultMap與實(shí)體之間的映射關(guān)系
    <resultMap id="userResultMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="user_name"/>
    <result property="password" column="hashed_password"/>
    </resultMap>

    2.引用它的語句使用 resultMap 屬性就行了(注意我們?nèi)サ袅?resultType 屬性)真朗。比如:
    <select id="selectUsers" resultMap="userResultMap">
      select user_id, user_name, hashed_password
      from some_table
      where id = #{id}
    </select>
    

—— “如果世界總是這么簡單就好了”

高級(jí)結(jié)果映射

MyBatis的構(gòu)想:數(shù)據(jù)庫不用永遠(yuǎn)是你想要的或需要它們是什么樣的此疹。而我們 最喜歡的數(shù)據(jù)庫最好是第三范式或 BCNF 模式,但它們有時(shí)不是。如果可能有一個(gè)單獨(dú)的 數(shù)據(jù)庫映射,所有應(yīng)用程序都可以使用它,這是非常好的,但有時(shí)也不是遮婶。結(jié)果映射就是 MyBatis 提供處理這個(gè)問題的答案蝗碎。

具體地說,當(dāng)我們查詢的結(jié)果非常復(fù)雜旗扑,比如說使用了連接查詢蹦骑,那么查詢到的結(jié)果該如何映射?

【例子】:比如有下面這樣一個(gè)查詢需求臀防,需要查詢一篇博客和它的作者信息眠菇、評(píng)論列表以及每條評(píng)論的回復(fù)列表,下面是具體的查詢語句:

<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
    select
    B.id as blog_id,
    B.title as blog_title,
    B.author_id as blog_author_id,
    A.id as author_id,
    A.username as author_username,
    A.password as author_password,
    A.email as author_email,
    A.bio as author_bio,
    A.favourite_section as author_favourite_section,
    P.id as post_id,
    P.blog_id as post_blog_id,
    P.author_id as post_author_id,
    P.created_on as post_created_on,
    P.section as post_section,
    P.subject as post_subject,
    P.draft as draft,
    P.body as post_body,
    C.id as comment_id,
    C.post_id as comment_post_id,
    C.name as comment_name,
    C.comment as comment_text,
    T.id as tag_id,
    T.name as tag_name
    from Blog B
    left outer join Author A on B.author_id = A.id
    left outer join Post P on B.id = P.blog_id
    left outer join Comment C on P.id = C.post_id
    left outer join Post_Tag PT on PT.post_id = P.id
    left outer join Tag T on PT.tag_id = T.id
    where B.id = #{id}
</select>

你可能想把它映射到一個(gè)智能的對(duì)象模型,包含一個(gè)作者寫的博客,有很多的博文,每 篇博文有零條或多條的評(píng)論和標(biāo)簽清钥,就像這樣:

class Blog
    - id
    - title
    - class Author
        - id
        - username
        - password
        - email
        - bio
        - favouriteSection
    - List class Post
        - id
        - subject
        - class Author
            - ...
        - List Comment
            - id
        - List class Tag
            - id
    - draft(是否是草稿)

下面是一個(gè)完整的復(fù)雜結(jié)果映射例子 (假設(shè)Author, Blog, Post, Comment和Tag都是類型的別名):

<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
        <idArg column="blog_id" javaType="int"/>
    </constructor>
    <result property="title" column="blog_title"/>
    <association property="author" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="username" column="author_username"/>
        <result property="password" column="author_password"/>
        <result property="email" column="author_email"/>
        <result property="bio" column="author_bio"/>
        <result property="favouriteSection" column="author_favourite_section"/>
    </association>
    <collection property="posts" ofType="Post">
        <id property="id" column="post_id"/>
        <result property="subject" column="post_subject"/>
        <association property="author" javaType="Author"/>
        <collection property="comments" ofType="Comment">
            <id property="id" column="comment_id"/>
        </collection>
        <collection property="tags" ofType="Tag" >
            <id property="id" column="tag_id"/>
        </collection>
        <discriminator javaType="int" column="draft">
            <case value="1" resultType="DraftPost"/>
        </discriminator>
    </collection>
</resultMap>

這就是resultMap的強(qiáng)大作用琼锋!將我們的復(fù)雜查詢映射到智能實(shí)體對(duì)象放闺。
下面是 resultMap 元素的概念視圖:

上述標(biāo)簽中可以具有的屬性:

  • jdbcType:支持的 JDBC 類型
  • constructor:構(gòu)造方法
  • association:關(guān)聯(lián)
    • 關(guān)聯(lián)的嵌套查詢
    • 關(guān)聯(lián)的嵌套結(jié)果
  • collection:集合
    • 集合的嵌套查詢
    • 集合的嵌套結(jié)果
  • discriminator:鑒別器

五祟昭、動(dòng)態(tài)SQL

動(dòng)態(tài)SQL

MyBatis 的強(qiáng)大特性之一便是它的動(dòng)態(tài) SQL功能。如果你有使用 JDBC 或其他類似框架的經(jīng)驗(yàn)怖侦,你就能體會(huì)到根據(jù)不同條件拼接 SQL 語句有多么痛苦篡悟。拼接的時(shí)候要確保不能忘了必要的空格,還要注意省掉列名列表最后的逗號(hào)匾寝。利用動(dòng)態(tài) SQL 這一特性可以徹底擺脫這種痛苦搬葬。

通常使用動(dòng)態(tài) SQL 不可能是獨(dú)立的一部分,MyBatis 當(dāng)然使用一種強(qiáng)大的動(dòng)態(tài) SQL 語言來改進(jìn)這種情形,這種語言可以被用在任意的 SQL 映射語句中

動(dòng)態(tài) SQL 元素和使用 JSTL 或其他類似基于 XML 的文本處理器相似艳悔。在 MyBatis 之前的版本中,有很多的元素需要來了解急凰。MyBatis 3 大大提升了它們,現(xiàn)在用不到原先一半的元素就可以了。MyBatis 采用功能強(qiáng)大的基于 OGNL 的表達(dá)式來消除其他元素猜年。

  • if
  • choose, when, otherwise
  • trim, where, set
  • foreach

六抡锈、基于注解方式的使用

注解方式又將SQL和Java代碼耦合疾忍,不推薦使用。但需要掌握一些基本的注解床三,以更好的配合基于Mapper文件的使用一罩。

@Param

注解對(duì)象:方法參數(shù)
作用:如果你的映射器的方法需要多個(gè)參數(shù), 這個(gè)注解可以被應(yīng)用于映射器的方法 參數(shù)來給每個(gè)參數(shù)一個(gè)名字。否則,多 參數(shù)將會(huì)以它們的順序位置來被命名 (不包括任何 RowBounds 參數(shù)) 比如撇簿。 #{param1} , #{param2} 等 , 這 是 默 認(rèn) 的 聂渊。 使 用 @Param(“person”),參數(shù)應(yīng)該被命名為 #{person}。

七四瘫、 集成Spring

依賴
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
</dependency>

將Mybatis與Spring集成汉嗽,可以利用Spring的依賴注入對(duì)Mybatis進(jìn)行托管, SQLSessionFactory找蜜、SqlSession的管理都可以被Spring管理诊胞。

0、引入properties
<context:property-placeholder location="jdbc.properties" ignore-unresolvable="true"/>
1锹杈、配置數(shù)據(jù)源
我們這里使用druid
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${url}"/>
    <property name="username" value="${user}"/>
    <property name="password" value="${password}"/>
    <property name="filters" value="stat"/>
    <property name="maxActive" value="20"/>
    <property name="initialSize" value="1"/>
    <property name="maxWait" value="60000"/>
    <property name="minIdle" value="1"/>
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <property name="validationQuery" value="SELECT 'x'"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>
    <property name="poolPreparedStatements" value="true"/>
    <property name="maxOpenPreparedStatements" value="20"/>
</bean>
2撵孤、配置SqlSessionFactoryBean
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

configLocation:mybatis配置文件路徑
mapperLocations:sql mapper文件路徑

3、配置MapperScannerConfigurer

配置要掃描的DAO接口(與xml mapper對(duì)應(yīng))的包位置竭望,這個(gè)包下面的接口將會(huì)被掃描邪码,我們在Service中可以通過自動(dòng)裝配的方法獲取并使用

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.qunar.frc.demo3.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

sqlSessionFactoryBeanName:指定SqlSessionFactoryBean

4咬清、將數(shù)據(jù)源DataSource的事務(wù)托管給Spring
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
# 開啟注解事務(wù)管理
<tx:annotation-driven transaction-manager="transactionManager"/>
5闭专、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>
    <settings>
        <!-- Globally enables or disables any caches configured in any mapper under this configuration -->
        <setting name="cacheEnabled" value="false"/>
        <!-- Sets the number of seconds the driver will wait for a response from the database -->
        <setting name="defaultStatementTimeout" value="3000"/>
        <!-- Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- Allows JDBC support for generated keys. A compatible driver is required.
        This setting forces generated keys to be used if set to true,
         as some drivers deny compatibility but still work -->
        <setting name="useGeneratedKeys" value="true"/>
    </settings>

    <!-- 為這個(gè)包下面的實(shí)體類起別名 -->
    <typeAliases>
        <package name="com.xxx.frc.demo3.model"/>
    </typeAliases>

    <!-- 自定義的類型處理器 -->
    <typeHandlers>
        <typeHandler handler="com.xxx.frc.demo3.typehandler.MoneyTypeHandler" javaType="xxx.api.pojo.Money"/>
        <typeHandler handler="com.xxx.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler" javaType="com.xxx.frc.demo3.enums.LevelEnum"/>
    </typeHandlers>

    <!-- 事務(wù)攔截 -->
    <plugins>
        <plugin interceptor="com.xxx.base.meerkat.orm.mybatis.support.ResultSetHandlerInterceptor"/>
        <plugin interceptor="com.xxx.base.meerkat.orm.mybatis.support.StatementHandlerInterceptor"/>
        <plugin interceptor="com.xxx.frc.demo3.plugin.SlowQueryTimePlugin">
            <property name="slowMillisecond" value="1"/>
        </plugin>
    </plugins>

</configuration>
6、mapper配置文件

八旧烧、 typeHandler

無論是 MyBatis 在預(yù)處理語句(PreparedStatement)中設(shè)置一個(gè)參數(shù)時(shí)影钉,還是從結(jié)果集中取出一個(gè)值時(shí),都會(huì)用類型處理器將獲取的值以合適的方式轉(zhuǎn)換成 Java 類型掘剪。Mybatis默認(rèn)為我們實(shí)現(xiàn)了許多TypeHandler, 當(dāng)我們沒有配置指定TypeHandler時(shí)平委,Mybatis會(huì)根據(jù)參數(shù)或者返回結(jié)果的不同,默認(rèn)為我們選擇合適的TypeHandler處理夺谁。

那么廉赔,Mybatis為我們實(shí)現(xiàn)了哪些TypeHandler呢? 我們?cè)趺醋远x實(shí)現(xiàn)一個(gè)TypeHandler ?

從源碼看TypeHandler的實(shí)現(xiàn)和管理

org.apache.ibatis.type.TypeHandlerRegistry是TypeHandler的注冊(cè)管理類,在這里注冊(cè)了所有Mybatis提供的默認(rèn)類型處理器:

TypeHandlerRegistry()方法中注冊(cè)默認(rèn)提供的類型處理器

由源碼可以看到匾鸥, mybatis默認(rèn)實(shí)現(xiàn)了很多TypeHandler蜡塌,繼承自一個(gè)抽象類:BaseTypeHandler,自定義typeHandler需要實(shí)現(xiàn)4個(gè)抽象方法:

自定義TypeHandler——以CodeEnumTypeHandler為例

在這里我參考Qunar對(duì)枚舉的類型處理封裝的類型處理器CodeEnumTypeHandler勿负,用于處理枚舉類型馏艾。在很多場景下,數(shù)據(jù)庫中需要枚舉變量作為字段值,比如性別(1-男琅摩,2-女)等厚者。

為了適用所有使用Enum的場景,強(qiáng)制規(guī)定:使用CodeEnumTypeHandler的Enum必須具有兩個(gè)方法:

code和靜態(tài)的codeOf方法:
public int code() {
  return this.getId();
  }

public static xxxEnum codeOf(int id) {
  return map.get(id);
}

code()方法:為一個(gè)Enum變量標(biāo)識(shí)了編號(hào)迫吐,通過code()獲取該Enum的編號(hào)库菲,當(dāng)然這個(gè)編號(hào)的意義必須與數(shù)據(jù)庫中的意義相同,這用codeOf方法獲得的枚舉才有意義
codeOf()方法:通過編號(hào)獲取Enum

在CodeEnumTypeHandler中志膀,通過反射獲取到Enum的這兩個(gè)方法熙宇,在繼承自BaseTypeHandler的四個(gè)方法中invoke調(diào)用,從而實(shí)現(xiàn)從Integer ->> Enum的轉(zhuǎn)換溉浙。

最后烫止,要在mybatis-config.xml中注冊(cè)typeHandler:

<typeHandlers>
    <typeHandler handler="com.xxx.xxx.CodeEnumTypeHandler" javaType="com.demo3.enums.LevelEnum"/>
</typeHandlers>

這樣,凡是LevelEnum類型戳稽,都將使用CodeEnumTypeHandler來處理馆蠕。


根據(jù)上面的思路,我這里自己實(shí)現(xiàn)一個(gè)枚舉類型的類型處理器惊奇,并且實(shí)現(xiàn)一個(gè)表示性別的枚舉類:

/**
 * version    date      author
 * ──────────────────────────────────
 * 1.0       17-3-22   wanlong.ma
 * Description: 性別枚舉類
 * Others:
 * Function List:
 * History:
 */
public enum  SexEnum {
    MALE(1,"男"),
    FEMALE(2,"女");

    private int id;         // 標(biāo)識(shí)號(hào)
    private String type;    // 類型

    SexEnum(int id, String type) {
        this.id = id;
        this.type = type;
    }

    public int getId() {
        return id;
    }

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

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    /////////定義下面的屬性和方法用于類型處理//////////

    private static final Map<Integer, SexEnum> map = Maps.newHashMap();
    static {
        for(SexEnum sexEnum : values()) {
            map.put(sexEnum.getId(), sexEnum);
        }
    }

    public int code(){
        return this.getId();
    }

    public static SexEnum codeOf(Integer id) {
        return map.get(id);
    }

}

/**
 * version    date      author
 * ──────────────────────────────────
 * 1.0       17-3-22   wanlong.ma
 * Description: 參考o(jì)m.qunar.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler寫的Enum通用類型處理器
 *              使用該處理器的Enum必須自己實(shí)現(xiàn)一個(gè)code方法和靜態(tài)的codeOf方法
 * Others:
 * Function List:
 * History:
 */
public class CustomEnumTypeHandler extends BaseTypeHandler<Enum<?>> {
    private Method code;
    private Method codeOf;

    public CustomEnumTypeHandler(Class<Enum<?>> enumType) {
        String className = enumType.getName();
        String simpleName = enumType.getSimpleName();

        try {
            code = enumType.getDeclaredMethod("code");
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Method " + className + "#code():int required.'");
        }

        try {
            codeOf = enumType.getDeclaredMethod("codeOf", int.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
        }

        if (!Modifier.isStatic(codeOf.getModifiers())) {
            throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
        }
    }

    /**
     * 調(diào)用enum的code方法互躬,返回枚舉的標(biāo)識(shí)
     * @param object
     * @return
     */
    private Integer code(Enum object){
        try {
            return (Integer) code.invoke(object);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    /**
     * 根據(jù)枚舉標(biāo)識(shí)號(hào)獲取枚舉值
     * 注意codeOf方法在枚舉類型中是一個(gè)靜態(tài)方法,所以此處是靜態(tài)調(diào)用
     * @param value
     * @return
     */
    private Enum codeOf(int value){
        try {
            return (Enum) codeOf.invoke(null, value);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Enum<?> parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, code(parameter));
    }

    @Override
    public Enum<?> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return codeOf(rs.getInt(columnName));
    }

    @Override
    public Enum<?> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return codeOf(rs.getInt(columnIndex));
    }

    @Override
    public Enum<?> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return codeOf(cs.getInt(columnIndex));
    }
}

配置文件中注冊(cè):

<typeHandlers>
    <typeHandler handler="com.xxx.db.handler.CustomEnumTypeHandler" javaType="com.xxx.db.enums.SexEnum"/>
</typeHandlers>

走起走起~

九颂郎、Plugins

十吼渡、RowBounds與分頁

參考:
MyBatis中的RowBounds
mybatis的兩種分頁方式:RowBounds和PageHelper

Mybatis如何分頁查詢?Mysql中可以使用limit語句乓序,但limit并不是標(biāo)準(zhǔn)SQL中的寺酪,如果是其它的數(shù)據(jù)庫,則需要使用其它語句替劈。MyBatis提供了RowBounds類寄雀,用于實(shí)現(xiàn)分頁查詢。通過設(shè)置RowBounds中的兩個(gè)變量來設(shè)置分頁起始行和頁面大性上住:offset和limit盒犹。

使用方法
  • RowBounds:在mapper.java中的方法中傳入RowBounds對(duì)象。

    RowBounds rowBounds = new RowBounds(offset, page.getPageSize()); // offset起始行湿故,limit是當(dāng)前頁顯示多少條數(shù)據(jù)
    public List<ProdProduct> findRecords(HashMap<String,Object> map,RowBounds rowBounds);
    
  • mappep.xml里面正常配置阿趁,不用對(duì)rowBounds任何操作膜蛔。mybatis的攔截器自動(dòng)操作rowBounds進(jìn)行分頁坛猪。

從源碼分析RowBounds的實(shí)現(xiàn)原理
  • RowBounds類


    RowBounds類
  • DefaultResultSetHandler類中通過RowBounds實(shí)現(xiàn)的分頁

handleRowValuesForSimpleResultMap方法
shouldProcessMoreRows方法

RowBounds在處理分頁時(shí),只是簡單的把offset之前的數(shù)據(jù)都skip掉皂股,超過limit之后的數(shù)據(jù)不取出墅茉,上圖中的代碼取自MyBatis中的DefaultResultSetHandler類。跳過offset之前的數(shù)據(jù)是由方法skipRows處理,判斷數(shù)據(jù)是否超過了limit則是由shouldProcessMoreRows方法進(jìn)行判斷就斤。說簡單點(diǎn)悍募,就是先把數(shù)據(jù)全部查詢到ResultSet,然后從ResultSet中取出offset和limit之間的數(shù)據(jù)洋机,這就實(shí)現(xiàn)了分頁查詢坠宴。

參考另外一種分頁方法:Mybatis 數(shù)據(jù)庫物理分頁插件 PageHelper

十一、關(guān)于DataSource

十二绷旗、Spring+Mybatis通用dao層喜鼓、service層的實(shí)現(xiàn)原則

spring+mybatis通用dao層、service層的一些個(gè)人理解與實(shí)現(xiàn)

十三衔肢、其他

sql中使用轉(zhuǎn)義字符

在mapper ***.xml中的sql語句中庄岖,不能直接用大于號(hào)、小于號(hào)要用轉(zhuǎn)義字符:

MyBatis中Like語句使用方式

mysql數(shù)據(jù)庫:

SELECT  
*  
FROM  
user  
WHERE  
name like CONCAT('%',#{name},'%')  
如果select查詢?yōu)榭战侵瑁琈ybatis會(huì)返回什么
/**
 * 驗(yàn)證結(jié)果:返回null
 */
@Test
public void testSelectIfNotExist(){
    EmployeeModel employeeModel = employeeService.queryEmployeeByStaffId(999);
    logger.info("Is employeeModel null? ->> {}", employeeModel == null);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隅忿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子邦尊,更是在濱河造成了極大的恐慌背桐,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝉揍,死亡現(xiàn)場離奇詭異牢撼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)疑苫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門熏版,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捍掺,你說我怎么就攤上這事撼短。” “怎么了挺勿?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵曲横,是天一觀的道長。 經(jīng)常有香客問我不瓶,道長禾嫉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任蚊丐,我火速辦了婚禮熙参,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘麦备。我一直安慰自己孽椰,他們只是感情好昭娩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著黍匾,像睡著了一般栏渺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锐涯,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天磕诊,我揣著相機(jī)與錄音,去河邊找鬼纹腌。 笑死秀仲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶笼。 我是一名探鬼主播神僵,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼覆劈!你這毒婦竟也來了保礼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤责语,失蹤者是張志新(化名)和其女友劉穎炮障,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坤候,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胁赢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了白筹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片智末。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖徒河,靈堂內(nèi)的尸體忽然破棺而出系馆,到底是詐尸還是另有隱情,我是刑警寧澤顽照,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布由蘑,位于F島的核電站,受9級(jí)特大地震影響代兵,放射性物質(zhì)發(fā)生泄漏尼酿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一植影、第九天 我趴在偏房一處隱蔽的房頂上張望裳擎。 院中可真熱鬧,春花似錦何乎、人聲如沸句惯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抢野。三九已至,卻和暖如春各墨,著一層夾襖步出監(jiān)牢的瞬間指孤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工贬堵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恃轩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓黎做,卻偏偏與公主長得像叉跛,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蒸殿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容