1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Mybatis查詢結(jié)果為空時,為什么返回值為NULL或空集合?

        共 22131字,需瀏覽 45分鐘

         ·

        2022-07-04 18:40


        文章來源:https://c1n.cn/6l7NH


        目錄
        • 背景

        • JDBC 中的 ResultSet 簡介

        • 簡單映射

        • 回歸最初的問題:查詢結(jié)果為空時的返回值

        • 結(jié)論


        背景


        一行數(shù)據(jù)記錄如何映射成一個 Java 對象,這種映射機(jī)制是 MyBatis 作為 ORM 框架的核心功能之一,也是我們這篇文章需要學(xué)習(xí)的內(nèi)容。


        開始前我們先看一個問題:

        你是否曾經(jīng)在學(xué)習(xí) Mybatis 的時候跟我有一樣的疑問,什么情況下返回 null,什么時候是空集合,為什么會是這種結(jié)果?那么你覺得上述這種回答能說服你嘛?


        我想應(yīng)該不能吧,除非親眼所見,否則真的很難確認(rèn)別人說的是對還是錯(畢竟網(wǎng)上的答案真的千奇百怪,啥都有,已經(jīng)不是第一次發(fā)現(xiàn)一些錯誤的說法被廣泛流傳了),那么這篇文章我們就簡單的分析一下。


        看完這篇你就知道查詢結(jié)果為空時候?yàn)槭裁醇蠒强占隙皇?NULL,而對象為什么會是 NULL 了。


        PS:對過程不感興趣的可以直接跳到最后看結(jié)論。


        JDBC 中的 ResultSet 簡介


        你如果有 JDBC 編程經(jīng)驗(yàn)的話,應(yīng)該知道在數(shù)據(jù)庫中執(zhí)行一條 Select 語句通常只能拿到一個 ResultSet,而結(jié)果集 ResultSet 是數(shù)據(jù)中查詢結(jié)果返回的一種對象,可以說結(jié)果集是一個存儲查詢結(jié)果的對象。


        但是結(jié)果集并不僅僅具有存儲的功能,他同時還具有操縱數(shù)據(jù)的功能,可能完成對數(shù)據(jù)的更新等,我們可以通過 next() 方法將指針移動到下一行記錄,然后通過 getXX() 方法來獲取值。
        while(rs.next()){
            // 獲取數(shù)據(jù)
            int id = rs.getInt(1);
            String name = rs.getString("name");

            System.out.println(id + "---" + name);
        }


        結(jié)果集處理入口 ResultSetHandler


        當(dāng) MyBatis 執(zhí)行完一條 select 語句,拿到 ResultSet 結(jié)果集之后,會將其交給關(guān)聯(lián)的 ResultSetHandler 進(jìn)行后續(xù)的映射處理。


        在 MyBatis 中只提供了一個 ResultSetHandler 接口實(shí)現(xiàn),即 DefaultResultSetHandler。


        下面我們就以 DefaultResultSetHandler 為中心,介紹 MyBatis 中 ResultSet 映射的核心流程。


        它的結(jié)構(gòu)如下:
        public interface ResultSetHandler {

            // 將ResultSet映射成Java對象
            <E> List<E> handleResultSets(Statement stmt) throws SQLException;

            // 將ResultSet映射成游標(biāo)對象
            <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

            // 處理存儲過程的輸出參數(shù)
            void handleOutputParameters(CallableStatement cs) throws SQLException;

        }


        | handleResultSets

        DefaultResultSetHandler 實(shí)現(xiàn)的 handleResultSets() 方法就支持多個 ResultSet 的處理,里面所調(diào)用的 handleResultSet() 方法就是負(fù)責(zé)處理單個 ResultSet。


        通過 while 循環(huán)來實(shí)現(xiàn)多個 ResultSet 的處理:
        public List<Object> handleResultSets(Statement stmt) throws SQLException {
            // 用于記錄每個ResultSet映射出來的Java對象
            final List<Object> multipleResults = new ArrayList<>();
            int resultSetCount = 0;
            // 從Statement中獲取第一個ResultSet,其中對不同的數(shù)據(jù)庫有兼容處理邏輯,
            // 這里拿到的ResultSet會被封裝成ResultSetWrapper對象返回
            ResultSetWrapper rsw = getFirstResultSet(stmt);
            // 獲取這條SQL語句關(guān)聯(lián)的全部ResultMap規(guī)則。如果一條SQL語句能夠產(chǎn)生多個ResultSet,
            // 那么在編寫Mapper.xml映射文件的時候,我們可以在SQL標(biāo)簽的resultMap屬性中配置多個
            // <resultMap>標(biāo)簽的id,它們之間通過","分隔,實(shí)現(xiàn)對多個結(jié)果集的映射
            List<ResultMap> resultMaps = mappedStatement.getResultMaps();
            int resultMapCount = resultMaps.size();
            validateResultMapsCount(rsw, resultMapCount);
            while (rsw != null && resultMapCount > resultSetCount) { // 遍歷ResultMap集合
                ResultMap resultMap = resultMaps.get(resultSetCount);
                // 根據(jù)ResultMap中定義的映射規(guī)則處理ResultSet,并將映射得到的Java對象添加到
                // multipleResults集合中保存
                handleResultSet(rsw, resultMap, multipleResults, null);
                // 獲取下一個ResultSet
                rsw = getNextResultSet(stmt);
                // 清理nestedResultObjects集合,這個集合是用來存儲中間數(shù)據(jù)的
                cleanUpAfterHandlingResultSet();
                resultSetCount++; // 遞增ResultSet編號
            }
            // 下面這段邏輯是根據(jù)ResultSet的名稱處理嵌套映射,你可以暫時不關(guān)注這段代碼,
            // 嵌套映射會在后面詳細(xì)介紹
            ... 
            // 返回全部映射得到的Java對象
            return collapseSingleResultList(multipleResults);
        }

        private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
            try {
                if (parentMapping != null) {
                    handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
                } else {
                    if (resultHandler == null) {
                        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                        // 將該ResultSet結(jié)果集處理完后的List對象放入multipleResults中,這樣就可以支持返回多個結(jié)果集了
                        multipleResults.add(defaultResultHandler.getResultList());
                    } else {
                        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
                    }
                }
            } finally {
                // issue #228 (close resultsets)
                closeResultSet(rsw.getResultSet());
            }
        }


        這里獲取到的 ResultSet 對象,會被包裝成 ResultSetWrapper 對象,而 ResultSetWrapper 主要用于封裝 ResultSet 的一些元數(shù)據(jù),其中記錄了 ResultSet 中每列的名稱、對應(yīng)的 Java 類型、JdbcType 類型以及每列對應(yīng)的 TypeHandler。


        | DefaultResultHandler 和 DefaultResultContext

        在開始詳細(xì)介紹映射流程中的每一步之前,我們先來看一下貫穿整個映射過程的兩個輔助對象 DefaultResultHandler 和 DefaultResultContext。


        在 DefaultResultSetHandler 中維護(hù)了一個 resultHandler 字段(ResultHandler 接口類型),它默認(rèn)情況下為空。


        比如 DefaultSqlSession#selectList() 中傳遞的值就是 ResultHandler NO_RESULT_HANDLER = null;


        它有兩個實(shí)現(xiàn)類:

        • DefaultResultHandler 實(shí)現(xiàn)的底層使用 ArrayList<Object> 存儲單個結(jié)果集映射得到的 Java 對象列表。

        • DefaultMapResultHandler 實(shí)現(xiàn)的底層使用 Map<K, V> 存儲映射得到的 Java 對象,其中 Key 是從結(jié)果對象中獲取的指定屬性的值,Value 就是映射得到的 Java 對象。


        DefaultResultContext 對象,它的生命周期與一個 ResultSet 相同,每從 ResultSet 映射得到一個 Java 對象都會暫存到 DefaultResultContext 中的 resultObject 字段,等待后續(xù)使用。


        同時 DefaultResultContext 還可以計算從一個 ResultSet 映射出來的對象個數(shù)(依靠 resultCount 字段統(tǒng)計)。


        | 多結(jié)果集返回

        數(shù)據(jù)庫支持同時返回多個 ResultSet 的場景,例如在存儲過程中執(zhí)行多條 Select 語句。


        MyBatis 作為一個通用的持久化框架,不僅要支持常用的基礎(chǔ)功能,還要對其他使用場景進(jìn)行全面的支持。


        而支持多結(jié)果集返回的邏輯就在 collapseSingleResultList 方法中:
        private List<Object> collapseSingleResultList(List<Object> multipleResults) {
            // 如果只有一個結(jié)果集就返回一個,否則直接通過List列表返回多個結(jié)果集
            return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
        }


        multipleResults 里有多少個 List 列表取決于 handleResultSet() 方法里的 resultHandler == null 的判斷。


        默認(rèn)情況下沒有設(shè)置 resultHandler 的話,那每處理一個 ResultSet 就會添加結(jié)果到 multipleResults 中, 此時 multipleResults.size() == 1 必然是不等于 1 的。


        注:感興趣的可以自行查看 resultHandler 什么時候會不為空。


        簡單映射


        DefaultResultSetHandler 是如何處理單個結(jié)果集的,這部分邏輯的入口是 handleResultSet() 方法,其中會根據(jù)第四個參數(shù),也就是 parentMapping,判斷當(dāng)前要處理的 ResultSet 是嵌套映射,還是外層映射。


        無論是處理外層映射還是嵌套映射,都會依賴 handleRowValues() 方法完成結(jié)果集的處理。


        通過方法名也可以看出,handleRowValues() 方法是處理多行記錄的,也就是一個結(jié)果集。


        handleRowValuesForNestedResultMap() 方法處理包含嵌套映射的 ResultMap,是否為嵌套查詢結(jié)果集,看 <resultMap> 聲明時,是否包含 association、collection、case 關(guān)鍵字。


        handleRowValuesForSimpleResultMap() 方法處理不包含嵌套映射的簡單 ResultMap。
        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);
            }
        }

        private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
            throws SQLException 
        {
            DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
            ResultSet resultSet = rsw.getResultSet();
            // 跳過多余的記錄
            skipRows(resultSet, rowBounds);
            // 檢測是否還有需要映射的數(shù)據(jù)
            while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
                // 處理映射中用到的 Discriminator,決定此次映射實(shí)際使用的 ResultMap。
                ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
                Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
                storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
            }
        }


        該方法的核心步驟可總結(jié)為如下:

        • 執(zhí)行 skipRows() 方法跳過多余的記錄,定位到指定的行。

        • 通過 shouldProcessMoreRows() 方法,檢測是否還有需要映射的數(shù)據(jù)記錄。

        • 如果存在需要映射的記錄,則先通過 resolveDiscriminatedResultMap() 方法處理映射中用到的 Discriminator,決定此次映射實(shí)際使用的 ResultMap。

        • 通過 getRowValue() 方法對 ResultSet 中的一行記錄進(jìn)行映射,映射規(guī)則使用的就是步驟 3 中確定的 ResultMap。

        • 執(zhí)行 storeObject() 方法記錄步驟 4 中返回的、映射好的 Java 對象。


        | ResultSet 的預(yù)處理

        我們可以通過 RowBounds 指定 offset、limit 參數(shù)實(shí)現(xiàn)分頁的效果。


        這里的 skipRows() 方法就會根據(jù) RowBounds 移動 ResultSet 的指針到指定的數(shù)據(jù)行,這樣后續(xù)的映射操作就可以從這一行開始。


        通過上述分析我們可以看出,通過 RowBounds 實(shí)現(xiàn)的分頁功能實(shí)際上還是會將全部數(shù)據(jù)加載到 ResultSet 中,而不是只加載指定范圍的數(shù)據(jù)所以我們可以認(rèn)為 RowBounds 實(shí)現(xiàn)的是一種“假分頁”。


        這種“假分頁”在數(shù)據(jù)量大的時候,性能就會很差,在處理大數(shù)據(jù)量分頁時,建議通過 SQL 語句 where 條件 + limit 的方式實(shí)現(xiàn)分頁。


        | 確定 ResultMap

        在完成 ResultSet 的預(yù)處理之后,接下來會通過 resolveDiscriminatedResultMap() 方法處理標(biāo)簽,確定此次映射操作最終使用的 ResultMap 對象。
        public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
            // 用于維護(hù)處理過的ResultMap唯一標(biāo)識
            Set<String> pastDiscriminators = new HashSet<>();
            // 獲取ResultMap中的Discriminator對象,這是通過<resultMap>標(biāo)簽中的<discriminator>標(biāo)簽解析得到的
            Discriminator discriminator = resultMap.getDiscriminator();
            while (discriminator != null) {
                // 獲取當(dāng)前待映射的記錄中Discriminator要檢測的列的值
                final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
                // 根據(jù)上述列值確定要使用的ResultMap的唯一標(biāo)識
                final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
                if (configuration.hasResultMap(discriminatedMapId)) {
                    // 從全局配置對象Configuration中獲取ResultMap對象
                    resultMap = configuration.getResultMap(discriminatedMapId);
                    // 記錄當(dāng)前Discriminator對象
                    Discriminator lastDiscriminator = discriminator;
                    // 獲取ResultMap對象中的Discriminator
                    discriminator = resultMap.getDiscriminator();
                    // 檢測Discriminator是否出現(xiàn)了環(huán)形引用
                    if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
                        break;
                    }
                } else {
                    break;
                }
            }
            // 返回最終要使用的ResultMap
            return resultMap;
        }


        至于 ResultMap 對象是怎么創(chuàng)建的,感興趣的可以自行從 XMLMapperBuilder#resultMapElements() 方法去了解一下,這里不再贅述。


        | 創(chuàng)建映射結(jié)果對象

        確定了當(dāng)前記錄使用哪個 ResultMap 進(jìn)行映射之后,要做的就是按照 ResultMap 規(guī)則進(jìn)行各個列的映射,得到最終的 Java 對象,這部分邏輯是在 getRowValue() 方法完成的。


        其核心步驟如下:

        • 首先根據(jù) ResultMap 的 type 屬性值創(chuàng)建映射的結(jié)果對象。

        • 然后根據(jù) ResultMap 的配置以及全局信息,決定是否自動映射 ResultMap 中未明確映射的列。

        • 接著根據(jù) ResultMap 映射規(guī)則,將 ResultSet 中的列值與結(jié)果對象中的屬性值進(jìn)行映射。

        • 最后返回映射的結(jié)果對象,如果沒有映射任何屬性,則需要根據(jù)全局配置決定如何返回這個結(jié)果值,這里不同場景和配置,可能返回完整的結(jié)果對象、空結(jié)果對象或是 null。


        這個可以關(guān)注 mybatis 配置中的 returnInstanceForEmptyRow 屬性,它默認(rèn)為 false。


        當(dāng)返回行的所有列都是空時,MyBatis 默認(rèn)返回 null。當(dāng)開啟這個設(shè)置時,MyBatis會返回一個空實(shí)例。


        請注意,它也適用于嵌套的結(jié)果集(如集合或關(guān)聯(lián))。(新增于 3.4.2)
        private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
            final ResultLoaderMap lazyLoader = new ResultLoaderMap();
            // 根據(jù)ResultMap的type屬性值創(chuàng)建映射的結(jié)果對象
            Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
            if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
                final MetaObject metaObject = configuration.newMetaObject(rowValue);
                boolean foundValues = this.useConstructorMappings;
                // 根據(jù)ResultMap的配置以及全局信息,決定是否自動映射ResultMap中未明確映射的列
                if (shouldApplyAutomaticMappings(resultMap, false)) {
                    foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
                }
                // 根據(jù)ResultMap映射規(guī)則,將ResultSet中的列值與結(jié)果對象中的屬性值進(jìn)行映射
                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
                // 如果沒有映射任何屬性,需要根據(jù)全局配置決定如何返回這個結(jié)果值,
                // 這里不同場景和配置,可能返回完整的結(jié)果對象、空結(jié)果對象或是null
                foundValues = lazyLoader.size() > 0 || foundValues;
                rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
            }
            return rowValue;
        }


        | 自動映射

        創(chuàng)建完結(jié)果對象之后,下面就可以開始映射各個字段了。在簡單映射流程中,會先通過 shouldApplyAutomaticMappings() 方法檢測是否開啟了自動映射。


        主要檢測以下兩個地方:

        • 檢測當(dāng)前使用的 ResultMap 是否配置了 autoMapping 屬性,如果是,則直接根據(jù)該 autoMapping 屬性的值決定是否開啟自動映射功能。

        • 檢測 mybatis-config.xml 的 <settings> 標(biāo)簽中配置的 autoMappingBehavior 值,決定是否開啟自動映射功能。NONE 表示關(guān)閉自動映射;PARTIAL 只會自動映射沒有定義嵌套結(jié)果映射的字段;FULL 會自動映射任何復(fù)雜的結(jié)果集(無論是否嵌套)。


        | 正常映射

        完成自動映射之后,MyBatis 會執(zhí)行 applyPropertyMappings() 方法處理 ResultMap 中明確要映射的列。


        | 存儲對象

        通過上述 5 個步驟,我們已經(jīng)完成簡單映射的處理,得到了一個完整的結(jié)果對象。


        接下來,我們就要通過 storeObject() 方法把這個結(jié)果對象保存到合適的位置。
        private void storeObject(...) throws SQLException {
            if (parentMapping != null) {
                // 嵌套查詢或嵌套映射的場景,此時需要將結(jié)果對象保存到外層對象對應(yīng)的屬性中
                linkToParents(rs, parentMapping, rowValue);
            } else {
                // 普通映射(沒有嵌套映射)或是嵌套映射中的外層映射的場景,此時需要將結(jié)果對象保存到ResultHandler中
                callResultHandler(resultHandler, resultContext, rowValue);
            }
        }


        這里處理的簡單映射,如果是一個嵌套映射中的子映射,那么我們就需要將結(jié)果對象保存到外層對象的屬性中。


        如果是一個普通映射或是外層映射的結(jié)果對象,那么我們就需要將結(jié)果對象保存到 ResultHandler 中。


        回歸最初的問題:查詢結(jié)果為空時的返回值


        | 返回結(jié)果為單行數(shù)據(jù)

        可以從 ResultSetHandler的handleResultSets 方法開始分析。


        multipleResults 用于記錄每個 ResultSet 映射出來的 Java 對象,注意這里是每個 ResultSet,也就說可以有多個結(jié)果集。


        我們可以看到 DefaultSqlSession#selectOne() 方法,我們先說結(jié)論:因?yàn)橹挥幸粋€ ResultSet 結(jié)果集,那么返回值為 null。


        步驟如下:


        handleResultSet() 方法的 handleRowValuesForSimpleResultMap 會判斷 ResultSet.next,此時為 false,直接跳過(忘記了的,返回去看簡單映射章節(jié))
            // 檢測是否還有需要映射的數(shù)據(jù)
            while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next())


        然后 multipleResults.add(defaultResultHandler.getResultList());中獲得的 list 就是默認(rèn)創(chuàng)建的空集合。
        public class DefaultResultHandler implements ResultHandler<Object{

          // 默認(rèn)是空集合
          private final List<Object> list;

          public DefaultResultHandler() {
            list = new ArrayList<>();
          }

          @SuppressWarnings("unchecked")
          public DefaultResultHandler(ObjectFactory objectFactory) {
            list = objectFactory.create(List.class);
          }

          @Override
          public void handleResult(ResultContext<? extends Object> context) {
            list.add(context.getResultObject());
          }

          public List<Object> getResultList() {
            return list;
          }

        }


        接下來 selectOne 拿到的就是空 list,此時 list.size() == 1和list.size() > 1 均為 false,所以它的返回值為 NULL。
        public <T> selectOne(String statement, Object parameter) {
            // Popular vote was to return null on 0 results and throw exception on too many.
            List<T> list = this.selectList(statement, parameter);
            if (list.size() == 1) {
                return list.get(0);
            } else if (list.size() > 1) {
                throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
            } else {
                return null;
            }
          }

        | 返回結(jié)果為多行數(shù)據(jù)

        那么我們看到 DefaultSqlSession#selectList() 方法,先說結(jié)論:返回值為空集合而不是 NULL。


        前面都同理,感興趣的可以自己順著 executor.query 一路往下看,會發(fā)現(xiàn)最后就是調(diào)用的 resultSetHandler.handleResultSets() 方法。


        只不過 selectList 是直接把 executor.query 從 defaultResultHandler.getResultList() 返回的空集合沒有做處理,直接返回。
        public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
            try {
                MappedStatement ms = configuration.getMappedStatement(statement);
                return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
            } catch (Exception e) {
                throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
            } finally {
                ErrorContext.instance().reset();
            }
        }


        結(jié)論


        看到這,我們在反過來看上面截圖里的答案,什么返回值是 Java 集合會先初始化??而且如果是 Map 作為返回值的話,那直接是返回的 NULL 好吧,簡直是錯的離譜!


        如果返回值是 Java 集合類型,如 List、Map,會先初始化(new 一個集合對象),再把結(jié)果添加進(jìn)去;如果返回值是普通對象,查詢不到時,返回值是 null。


        其實(shí)不管你是查單行記錄還是多行記錄,對于 Mybatis 來說都會放到 DefaultResultHandler 中去,而 DefaultResultHandler 又是用 List 存儲結(jié)果。


        所以不管是集合類型還是普通對象,Mybatis 都會先初始化一個 List 存儲結(jié)果,然后返回值為普通對象且查為空的時候,selectOne 會判斷然后直接返回 NULL 值。


        而返回值為集合對象且查為空時,selectList 會把這個存儲結(jié)果的 List 對象直接返回,此時這個 List 就是個空集合。

        瀏覽 50
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            黑大粗长欧美成人免费 | 操逼操逼操逼视频 | 抱着娇妻让粗大玩3p在线观看 | 韩国三级久久电影 | 亚洲成人综合网站 | 97国产在线 | 国外精品二三区 | 美女扒开尿道让男人桶 | 操女人逼逼视频 | bytv跳转接口点击进入网页 |