從源碼角度理解MyBatis字段映射

MyBatis在轉(zhuǎn)換查詢結(jié)果到需要的Java業(yè)務(wù)對(duì)象時(shí)做了三件事:

  1. 解決了數(shù)據(jù)庫(kù)列名到Java列名的映射。
  2. 解決了數(shù)據(jù)庫(kù)類型到Java類型的轉(zhuǎn)換工作莉钙。
  3. 在轉(zhuǎn)換過(guò)程中具備一定的容錯(cuò)能力陕壹。

其實(shí)核心就是:

  1. 數(shù)據(jù)庫(kù)中的列名怎么和對(duì)象中的字段對(duì)應(yīng)起來(lái)。
  2. 數(shù)據(jù)庫(kù)中的列的類型怎么轉(zhuǎn)換到合適的Java類型浸剩,不引起轉(zhuǎn)換失敗可帽。

我們先來(lái)看第一點(diǎn)娄涩,數(shù)據(jù)庫(kù)中的列名怎么和對(duì)象中的字段對(duì)應(yīng)起來(lái)。首先是PO(Persistant Object) CityPO里面有五個(gè)字段:

public class CityPO {
    Integer id;
    Long cityId;
    String cityName;
    String cityEnName;
    String cityPyName;
}

本次要查詢的數(shù)據(jù)庫(kù)中的列名如下所示:

mysql> mysql> desc SU_City;
+--------------+-------------+------+-----+-------------------+-----------------------------+
| Field        | Type        | Null | Key | Default           | Extra                       |
+--------------+-------------+------+-----+-------------------+-----------------------------+
| id           | int(11)     | NO   | PRI | NULL              | auto_increment              |
| city_id      | int(11)     | NO   | UNI | NULL              |                             |
| city_name    | varchar(20) | NO   |     |                   |                             |
| city_en_name | varchar(20) | NO   |     |                   |                             |
| city_py_name | varchar(50) | NO   |     |                   |                             |
| create_time  | datetime    | NO   |     | CURRENT_TIMESTAMP |                             |
| updatetime   | datetime    | NO   | MUL | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+--------------+-------------+------+-----+-------------------+-----------------------------+
7 rows in set (0.01 sec)

我們是按照駝峰式命名映跟,把數(shù)據(jù)庫(kù)中的列名對(duì)應(yīng)到了對(duì)象的字段名蓄拣。如下是MyBatis的接口類和映射文件。
接口類:

public interface CityMapper {
    CityPO selectCity(int id);
}

映射文件:

<?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">
<mapper namespace="mapper.CityMapper">
    <select id="selectCity" resultType="po.CityPO">
        select id,city_id,city_name,city_en_name from SU_City where id = #{id}
    </select>
</mapper>

在上面的映射文件中努隙,namespace指定了這個(gè)接口類的全限定類名球恤,緊隨其后的select代表是select語(yǔ)句,id是接口類中函數(shù)的名字荸镊,resultType代表了從這條語(yǔ)句中返回的期望類型的類的完全限定名或別名咽斧,在此例子中是我們的業(yè)務(wù)對(duì)象CityPO的類路徑。

主要有三種方案:

  1. 駝峰式命名開(kāi)關(guān)躬存,或者不開(kāi)——數(shù)據(jù)庫(kù)列和字段名全一致收厨。
  2. Select時(shí)指定AS。
  3. resultMap 最穩(wěn)健优构。

1. 駝峰命名開(kāi)關(guān)

因?yàn)镃ityPO的列名是完全根據(jù)數(shù)據(jù)庫(kù)列名駝峰式命名后得到的诵叁,因此MyBatis提供了一個(gè)配置項(xiàng)。開(kāi)啟開(kāi)配置項(xiàng)后钦椭,在匹配時(shí)拧额,能夠根據(jù)數(shù)據(jù)庫(kù)列名找到對(duì)應(yīng)對(duì)應(yīng)的駝峰式命名后的字段碑诉。

<settings>
    <!-- 開(kāi)啟駝峰,開(kāi)啟后侥锦,只要數(shù)據(jù)庫(kù)字段和對(duì)象屬性名字母相同进栽,無(wú)論中間加多少下劃線都可以識(shí)別 -->
    <setting name="mapUnderscoreToCamelCase" value="true" />
</settings>

從源碼角度解讀一下,MyBatis處理ResultSet的映射默認(rèn)都在DefaultResultSetHandler中完成恭垦。

處理行數(shù)據(jù)的源碼主要在下面的函數(shù)里進(jìn)行快毛,由于我們?cè)谟成湮募袥](méi)有定義額外的ResultMap,因此會(huì)直接進(jìn)入else分支的代碼番挺。

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

進(jìn)入handleRowValuesForSimpleResultMap中唠帝,主要處理函數(shù)如下,在這里完成了對(duì)象的生成及賦值玄柏。

Object rowValue = getRowValue(rsw, discriminatedResultMap);

在這里先創(chuàng)建了對(duì)象的實(shí)例襟衰,然后獲取了對(duì)象的元信息,為反射賦值做準(zhǔn)備粪摘。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = (foundValues || configuration.isReturnInstanceForEmptyRow()) ? rowValue : null;
    }
    return rowValue;
}

在applyAutomaticMappings完成了整個(gè)過(guò)程瀑晒,我們進(jìn)去探一探。

就是下面這個(gè)函數(shù)創(chuàng)建好了映射關(guān)系徘意,這個(gè)函數(shù)的下半部分是完成賦值的苔悦,映射的部分下次會(huì)詳細(xì)分析。

List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);

在這個(gè)方法里椎咧,上半部分是生成了數(shù)據(jù)庫(kù)的列名玖详,在這個(gè)函數(shù)中找到了對(duì)應(yīng)的字段名。

final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());

我們進(jìn)去看一看邑退,它傳進(jìn)了生成好的數(shù)據(jù)庫(kù)列名,傳進(jìn)了前面提到的是否根據(jù)駝峰式命名映射開(kāi)關(guān)的值劳澄。

事實(shí)證明地技,真的很簡(jiǎn)單,往下看秒拔,就是把下劃線都去了莫矗。

public String findProperty(String name, boolean useCamelCaseMapping) {
    if (useCamelCaseMapping) {
        name = name.replace("_", "");
    }
    return findProperty(name);
}

隱隱覺(jué)得是不是大小寫(xiě)不敏感啊,繼續(xù)往下看砂缩,這里返回找到的字段名作谚。

private StringBuilder buildProperty(String name, StringBuilder builder) {
    ......
    String propertyName = reflector.findPropertyName(name);
    if (propertyName != null) {
        builder.append(propertyName);
    }
    return builder;
}

好了,真相大白庵芭,就是大小寫(xiě)不敏感的妹懒。

public String findPropertyName(String name) {
    return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
}
大小寫(xiě)不敏感

2. Select .... AS

當(dāng)我們的數(shù)據(jù)庫(kù)列名和對(duì)象字段之間不是駝峰式命名的關(guān)系,可以在Select時(shí)使用AS双吆,使得列名和對(duì)象名匹配上眨唬。

映射文件中是本次會(huì)執(zhí)行的sql会前,我們會(huì)查出id,city_id匾竿,city_name瓦宜,city_en_name。 按照開(kāi)啟的駝峰式命名開(kāi)關(guān)岭妖,我們會(huì)對(duì)應(yīng)到對(duì)象的id临庇,cityId,cityName昵慌,cityEnName字段假夺。

<select id="selectCity" resultType="po.CityPO">
    select id,city_id,city_name,city_en_name from SU_City where id = #{id}
</select>

不過(guò)在這次,我們對(duì)PO做了小小的改動(dòng)废离,把cityEnName改成了cityEnglishName侄泽。

public class CityPO {
    Integer id;
    Long cityId;
    String cityName;
    String cityEnglishName;   // 由cityEnName改成了cityEnglishName
}

由于找不到匹配的列,cityEnlishName肯定沒(méi)法被反射賦值蜻韭,因此值為Null悼尾。

CityPO{id=2, cityId=2, cityName='北京', cityEnglishName='null'}

解決辦法:在Select字段的時(shí)候使用AS,下面是改動(dòng)后的映射文件肖方。

<select id="selectCity" resultType="po.CityPO">
        select id,
        city_id,
        city_name,
        city_en_name AS cityEnglishName
        from SU_City
        where id = #{id}
</select>

改動(dòng)后執(zhí)行得到的結(jié)果如下闺魏。

CityPO{id=2, cityId=2, cityName='北京', cityEnglishName='beijing'}

那么我們來(lái)看看它是如何生效的,主要的代碼在哪里俯画。上面我們第一個(gè)介紹的函數(shù)handleRowValues中傳入了參數(shù)rsw析桥,它是對(duì)ResultSet的一個(gè)包裝,在這個(gè)包裝里艰垂,完成了具體使用哪個(gè)名字作為數(shù)據(jù)庫(kù)的列名泡仗。

final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);

在這個(gè)構(gòu)造函數(shù)當(dāng)中,我們會(huì)獲取數(shù)據(jù)庫(kù)的列名猜憎,AS為什么可以生效娩怎,具體就在下面這段代碼。

super();
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.resultSet = rs;
final ResultSetMetaData metaData = rs.getMetaData();
final int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
    // 在這里
    columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
    jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
    classNames.add(metaData.getColumnClassName(i));
}

在添加列名時(shí)胰柑,會(huì)從配置中獲取是否使用類標(biāo)簽截亦,isUseColumnLabel,默認(rèn)為true柬讨。根據(jù)Javadoc崩瓤,這個(gè)ColumnLabel就是AS后的那個(gè)名字,如果沒(méi)有AS的話踩官,就是獲取的原生的字段名却桶。

/**
 * Gets the designated column's suggested title for use in printouts and
 * displays. The suggested title is usually specified by the SQL <code>AS</code>
 * clause.  If a SQL <code>AS</code> is not specified, the value returned from
 * <code>getColumnLabel</code> will be the same as the value returned by the
 * <code>getColumnName</code> method.
 *
 * @param column the first column is 1, the second is 2, ...
 * @return the suggested column title
 * @exception SQLException if a database access error occurs
 */
String getColumnLabel(int column) throws SQLException;

后面的過(guò)程就和上面方案一一模一樣了,不再贅述蔗牡。

3. ResultMap

resultMap 元素是 MyBatis 中最重要最強(qiáng)大的元素肾扰。它可以讓你從 90% 的 JDBC ResultSets 數(shù)據(jù)提取代碼中解放出來(lái)畴嘶,并在一些情形下允許你做一些 JDBC 不支持的事情。 實(shí)際上集晚,在對(duì)復(fù)雜語(yǔ)句進(jìn)行聯(lián)合映射的時(shí)候窗悯,它很可能可以代替數(shù)千行的同等功能的代碼。 ResultMap 的設(shè)計(jì)思想是偷拔,簡(jiǎn)單的語(yǔ)句不需要明確的結(jié)果映射蒋院,而復(fù)雜一點(diǎn)的語(yǔ)句只需要描述它們的關(guān)系就行了。

ResultMap是MyBatis中可以完成復(fù)雜語(yǔ)句映射的東西莲绰,但在我們的日常開(kāi)發(fā)中欺旧,我們往往是一個(gè)XML對(duì)應(yīng)JavaBeans 或 POJOs(Plain Old Java Objects,普通 Java 對(duì)象)蛤签,并沒(méi)有特別復(fù)雜的應(yīng)用辞友,下面也是基于日常的使用,看看簡(jiǎn)單的ResultMap在源碼層面是如何展現(xiàn)的震肮。

<resultMap id="cityMap" type="po.CityPO">
        <result column="id" property="id"/>
        <result column="city_id" property="cityId"/>
        <result column="city_name" property="cityName"/>
        <result column="city_en_name" property="cityEnglishName"/>
</resultMap>

<select id="selectCity" resultMap="cityMap">
        select id,
        city_id,
        city_name,
        city_en_name
        from SU_City
        where id = #{id}
</select>

在resultMap的子元素result對(duì)應(yīng)了result和對(duì)象字段之間的映射称龙,并通過(guò)id標(biāo)示,你在Select語(yǔ)句中指定需要使用的resultMap即可戳晌。

源碼層面的話鲫尊,依舊在DefaultResultSetHandler的handleResultSets中處理返回集合。

List<ResultMap> resultMaps = mappedStatement.getResultMaps();

在這次的ResultMap中沦偎,相比之前方案疫向,其屬性更加的豐富起來(lái)。將之前寫(xiě)的Result的信息保存在resultMappings豪嚎,idResultMappings等中搔驼,以備后續(xù)使用。

resultMappings

后續(xù)的函數(shù)走向和方案一二一致侈询,但在創(chuàng)建自動(dòng)映射的時(shí)候出現(xiàn)了不同舌涨。

private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
}

在這個(gè)函數(shù)中,會(huì)獲取沒(méi)有映射過(guò)的列名妄荔。

final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);

之后會(huì)根據(jù)resultMap查看是否有未映射的字段泼菌。

loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
    List<String> mappedColumnNames = new ArrayList<>();
    List<String> unmappedColumnNames = new ArrayList<>();
    final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
    // 這里沒(méi)有配置前綴谍肤,根據(jù)之前的圖啦租,定義了ResultMap后,會(huì)記錄這些已經(jīng)配置映射的字段荒揣。
    final Set<String> mappedColumns = prependPrefixes(resultMap.getMappedColumns(), upperColumnPrefix);
    for (String columnName : columnNames) {
        // 遍歷列名篷角,如果在已映射的配置中,那么就加入已經(jīng)映射的列名數(shù)據(jù)
        final String upperColumnName = columnName.toUpperCase(Locale.ENGLISH);
        if (mappedColumns.contains(upperColumnName)) {
            mappedColumnNames.add(upperColumnName);
        } else {
            unmappedColumnNames.add(columnName);
        }
    }
    // 生成未映射和已映射的Map
    mappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), mappedColumnNames);
    unMappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), unmappedColumnNames);
}

如果有沒(méi)配置在ResultMap中系任,且Select出來(lái)的恳蹲,那么之后也會(huì)按照之前方案一那樣虐块,繼續(xù)往下走,從對(duì)象中尋找映射關(guān)系嘉蕾。

由于沒(méi)有未映射的字段贺奠,使用自動(dòng)映射的結(jié)果是false。

foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;

之后繼續(xù)往下走错忱,使用applyPropertyMappings來(lái)創(chuàng)建對(duì)象儡率。其用到了propertyMappings,里面包含了字段名以清,列名儿普,字段的類型和對(duì)應(yīng)的處理器。

propertyMappings

遍歷整個(gè)Mappings掷倔。

Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);

函數(shù)里主要的就是獲取這個(gè)字段對(duì)應(yīng)的類型處理器眉孩,防止類型轉(zhuǎn)換失敗,這一部分下次會(huì)專門看一下勒葱。

final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);

TypeHandler就是一個(gè)接口浪汪,主要完成的工作就是從Result根據(jù)列名,獲取相應(yīng)類型的值错森,為下一步反射賦值做準(zhǔn)備吟宦。至于它是怎么決定為什么用這個(gè)類型的TypeHandler下次再看。

然后就是給對(duì)應(yīng)字段賦值涩维。

metaObject.setValue(property, value);

最后就完成了整個(gè)類的賦值殃姓。

賦值

總結(jié)

大致上,MyBatis完成映射主要是兩種方式:

  1. 只根據(jù)列名瓦阐,利用自動(dòng)映射蜗侈,根據(jù)反射類的信息,得到列名和字段之間的關(guān)系睡蟋,使用對(duì)應(yīng)的TypeHandler完成字段的賦值踏幻。
  2. 使用ResultMap預(yù)先定義好映射關(guān)系,最后也是根據(jù)TypeHandler和反射完成字段的賦值戳杀。

就簡(jiǎn)單的用法來(lái)說(shuō)该面,兩者都可以。在一次會(huì)話中信卡,Configuration中的ResultMap關(guān)系建立好隔缀,在每一次查詢的時(shí)候就不用再去重新建立了,直接用就行傍菇。而自動(dòng)映射的話猾瘸,執(zhí)行過(guò)一次后,也會(huì)在會(huì)話中建立自動(dòng)映射的緩存,所以沒(méi)什么差別牵触。但如果復(fù)雜的映射的話淮悼,就非ResultMap莫屬啦。具體可以參考MyBatis文檔關(guān)于映射的章節(jié)揽思,因?yàn)槟壳坝貌坏奖容^復(fù)雜的映射袜腥,不做深究了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钉汗,一起剝皮案震驚了整個(gè)濱河市瞧挤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌儡湾,老刑警劉巖特恬,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異徐钠,居然都是意外死亡癌刽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門尝丐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)显拜,“玉大人,你說(shuō)我怎么就攤上這事爹袁≡盾” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵失息,是天一觀的道長(zhǎng)譬淳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)盹兢,這世上最難降的妖魔是什么邻梆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮绎秒,結(jié)果婚禮上浦妄,老公的妹妹穿的比我還像新娘。我一直安慰自己见芹,他們只是感情好剂娄,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著玄呛,像睡著了一般阅懦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上把鉴,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天故黑,我揣著相機(jī)與錄音,去河邊找鬼庭砍。 笑死场晶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的怠缸。 我是一名探鬼主播诗轻,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼揭北!你這毒婦竟也來(lái)了扳炬?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤搔体,失蹤者是張志新(化名)和其女友劉穎恨樟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體疚俱,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劝术,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呆奕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片养晋。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖梁钾,靈堂內(nèi)的尸體忽然破棺而出绳泉,到底是詐尸還是另有隱情,我是刑警寧澤姆泻,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布零酪,位于F島的核電站,受9級(jí)特大地震影響拇勃,放射性物質(zhì)發(fā)生泄漏蛾娶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一潜秋、第九天 我趴在偏房一處隱蔽的房頂上張望蛔琅。 院中可真熱鬧,春花似錦峻呛、人聲如沸罗售。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寨躁。三九已至,卻和暖如春牙勘,著一層夾襖步出監(jiān)牢的瞬間职恳,已是汗流浹背所禀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留放钦,地道東北人色徘。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像操禀,于是被迫代替她去往敵國(guó)和親褂策。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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