基於SQL詞法分析的多種數據庫自動分頁方案

基於SQL詞法分析的多種數據庫自動分頁方案

(原文見 http://www.pwmis.cn/bbs/dispbbs.asp?boardID=8&ID=595 )

一、背景:
1,巨大的數據處理量;
2,快速分頁;
3,動態查詢(列表顯示的項目);
4,適應多種數據庫;
5,適應多表查詢;
6,適應無主鍵或主鍵無序的情況;

二、算法:
一般快速分頁都是採用基於有序主鍵採用二分查找排序的算法,可以應對大數據量的分頁要求。
但是實際運用中往往需要對某些非主鍵字段排序分頁,或者有些查詢沒有主鍵,或者主鍵是無序的。
本文采用兩種分法來處理這幾類情況:
(注意:下面的算法中均以降序爲準,1,2 方法考慮在SQL SERVER 及ACCESS 中的實現方法)
1,有主鍵,並且主鍵是有序的:
SELECT TOP  @@PageSize * FROM
    (SELECT TOP @@PageSize * FROM 
      (SELECT TOP  @@Page_Size_Number [@@FieldList]
        FROM (@@DataSourceTable) st
          @@Where
          ORDER BY @@PrimaryKey DESC
      ) t1 ORDER BY @@PrimaryKey ASC
    ) t2 ORDER BY @@PrimaryKey DESC

說明:在查詢中,使用主鍵排序比採用其它字段排序擁有極高的效率。
[@@DataSourceTable] 可能是一個單表的查詢,例如:

SELECT * FROM USERINFO WHERE USERCLASS='1' ORDER BY USERNAME DESC

也可能是一個複雜的多表查詢,例如:

SELECT A.ArticleID, A.ClassID, A.Title, A.CreateTime, A.Hints, A.Writer ,A.PaperDate,A.FromOffice,C.ClassName AS ClassName
FROM TB_OA_Article A
INNER JOIN  TB_OA_Class C ON A.ClassID = C.ClassID 

或者是一個視圖,例如:

SELECT * FROM VIEW1

[@@Where] 表示要多數據源的篩選條件。
注:把數據篩選條件放到這一層次可以避免 [@@DataSourceTable] 中有複雜的多表查詢的情況引起“字段不明確”的問題。

***以下如果沒有特殊說明,出現的相同單詞均以此處爲準。***


2,有主鍵,同時主鍵可能是無序的;或者沒有主鍵:

  SELECT TOP  @@PageSize * FROM
    (SELECT TOP @@PageSize * FROM 
      (SELECT TOP  @@Page_Size_Number [@@FieldList]
        FROM (@@DataSourceTable) st
          @@Where
          ORDER BY @@OrderField DESC
      ) t1 ORDER BY @@OrderField ASC
    ) t2 ORDER BY @@OrderField DESC

說明:
該方式可以適應 各種分頁及排序的要求。
[@@OrderField] 可以不是主鍵,可以有多個,但是必須分別指明排序方式 [desc]/[asc]

3,Oracle 分頁算法:
select [@@FieldList] from (
   select [@@FieldList],rownum rn from
    (SELECT [@@FieldList] FROM ( @@DataSourceTable ) ST0 @@Where order by @@OrderField DESC ) st
    where rownum <= @@Page_Size_Number
)
where rn >= (@@Page_Size_Number-@@PageSize)
說明:
在Oracle中,由於虛擬列 rownum 實在排序之後輸出的,所以必須採用嵌套的查詢來獲取制定範圍內的數據。


三、實現方案:
根據上面的算法,可以發現它們構造的SQL語句及其相似,算法1其實就是算法2的特例,在實現本文的分頁方案的時候,
只需要判斷是否提供了主鍵即可。
另外,爲了儘可能提高效率,在查詢第一頁和最後一頁的時候,需要分別處理(ORACLE 不需要下面的處理):
1,查詢第一頁:
SELECT TOP @@PageSize [@@FieldList]
  FROM   (@@DataSourceTable) st
    @@Where
    ORDER BY @@OrderField DESC
說明:對於第一頁,直接採用Top 方式,即可以擁有很高的效率。

2,查詢最後一頁:
SELECT * FROM
  (SELECT Top @@LeftSize [@@FieldList]
    FROM (@@DataSourceTable) st
       @@Where
       ORDER BY @@OrderField ASC
  ) T1 ORDER BY @@OrderField DESC

說明:對於最後一頁,只需要第一次取數據的時候倒序,第二次正序即可。注意這裏,@@LeftSize 的大小,
可能小於 @@PageSize,需要計算它的大小,這樣,在處理所有的分頁的時候,涉及到一個計算最後一頁大小的問題。
假設 符合條件的所有記錄數量爲 AllCount,每一頁的大小爲 PageSize,當前頁碼爲 PageNumber,
那麼計算最後一頁大小的方法如下:

 if(PageSize*PageNumber>AllCount) //最後的頁 @@LeftSize
    LeftSize=AllCount-PageSize*(PageNumber-1);
 else
    LeftSize=PageSize;

取得符合條件的記錄數的查詢:

SELECT COUNT(*) FROM (@@DataSourceTable) st
    @@Where
說明:統計記錄數不再需要 Order by 子句。如果有主鍵,也可以採用下面的語句提高效率:

SELECT COUNT(@PrimaryKey) FROM (@@DataSourceTable) st
    @@Where
-----------------------------------------------------------------

好了,上面已經分析了各種情況下處理分頁及排序的最佳算法,現在涉及到真正實現上面算法的時候了,
根據上面的算法,我們可以正確地寫出適應不同情況的分頁排序SQL 語句,但是做一次分頁排序需要寫
4條SQL語句,在一個項目中這樣的SQL語句將會多得嚇人(我們目前的一個方案就是採用每次查詢分頁手
寫四條SQL的方式),有沒有更好的辦法來解決這個問題呢?

[解決方案]
******************採用 SQL 詞法分析 ************************

一提到“詞法分析” 有點像編譯語言的詞法分析了,其實在每一個SQL語句中,不管查詢有多麼複雜,
都可以分爲以下4部分,而且是順序鐵定的4部分:

SELECT ... FROM ... [WHERE ...] [ORDER BY ...]

其中,Where 子句和 Order by 子句都是可選的,而 Select 子句中的某一個字段可以是一個子查詢,
From 子句 可以是一個或者多個表,甚至是一個( ) 中的子查詢。但是根據 SQL 語句中的這幾部分
特別的語法順序,我們自動分離出每一個子句是不成問題的,這裏只要注意 帶 ( ) 的子查詢就是了。
所以,在我們需要對一次查詢作分頁以及排序的時候,我們只需要寫出上面的標準SQL 語句即可,然後
程序可以取得我們要篩選的條件,待排序的字段及方式,自動地完成符合特定數據庫的分頁查詢語句。
 
在用戶給出的 一條查詢 SQL 中,我們可以簡單的對其進行字符串處理,就可以把各個子句提取出來,
然後根據上面的算法,構造出與具體數據庫無關分頁查詢語句。

**********************特別說明**********************************************
在最終實現的時候,用戶給的SQL 語句,其實就是上面的 “@@DataSourceTable”,之所以要這麼處理,
就是因爲用戶的這個SQL 語句,可能是一個簡單的單表查詢,可以能是一個複雜的多表連接查詢,甚至
是一個存儲過程(如果是存儲過程的話,那麼需要手工指明需要返回那些字段以及待排序的字段)
*****************************************************************************

ok,下面我們來進行第一步,“ SQL 詞法分析”
(2006.5.29 ,未完待續)

 

我們先來看幾個典型的查詢語句:

1,簡單的單表查詢:
SELECT UserID,UserName,UserClass FROM USERINFO WHERE USERCLASS='1' ORDER BY USERNAME DESC
分析:對該表進行簡單的單詞查找,很容易取得Select ,From ,Where ,Order by 子句。
經過轉換,將構造如下語句:
SELECT TOP  @@PageSize * FROM
    (SELECT TOP @@PageSize * FROM 
      (SELECT TOP  @@Page_Size_Number UserID,UserName,UserClass
        FROM USERINFO
          WHERE USERCLASS='1'
          ORDER BY USERNAME DESC
      ) t1 ORDER BY USERNAME ASC
    ) t2 ORDER BY USERNAME DESC

注意:爲了以後有可能將該查詢結果綁定到數據表格,然後通過模糊查找表格中顯示的文字內容(不限列),
有必要加上 @@Where 替換參數(指在運行時用文本方式替換重新生成一條查詢語句),上面的查詢將變成:

SELECT TOP  @@PageSize * FROM
    (SELECT TOP @@PageSize * FROM 
      (SELECT TOP  @@Page_Size_Number * FROM
        (SELECT UserID,UserName,UserClass
          FROM USERINFO
          WHERE USERCLASS='1'
        ) ST
        @@Where ORDER BY USERNAME DESC
      ) t1 ORDER BY USERNAME ASC
    ) t2 ORDER BY USERNAME DESC

注意:@@Where ORDER BY USERNAME DESC 中的 Order by 子句,放在最內的一層原是查詢語句的外面了,
爲什麼要這樣做呢?在SQL SERVER 中,“除非指定了Top謂詞,否則在嵌套的查詢語句中不允許使用
Order by 子句”!所以對原始SQL 語句中的排序條件的處理就很重要了,對於單表的查詢還比較容易處理,
但是對於複雜的查詢就比較困難了,接着看下面的情況。

2,複雜的多表連接查詢:
select
B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
      M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
(select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
   FROM   TB_Flow_Bussiness B
       Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
WHERE  M.State='01'
ORDER BY PlanCreateDate DESC

注意:在子查詢
select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc
中,使用了order by 謂詞,在主查詢中,也使用了order by 謂詞,所以不能夠簡單的查找
SELECT,FROM,WHERE,ORDER BY 謂詞的位置來確定相應的子句內容。
實際上,我們關心的只是最外層的 ORDER BY 子句,通過確定的查詢謂詞的順序,我們很容易找到該
 ORDER BY 子句。經過轉換,將生成如下的語句:

SELECT Top 10 * from
 (select B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
      M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
      (select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
   FROM   TB_Flow_Bussiness B
       Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
   WHERE  M.State='01' 
) ST
where bussinessid like '%2006%' and CreatorName='XIAO'
ORDER BY PlanCreateDate DESC

如果最外層的Order by 子句中的 排序字段包含 “數據表.字段名” 這樣的限定標記,應該替換隻有字段名
的標記,例如上面如果還以 M.MPID 排序,那麼在外層的排序語句中,應該使用 MPID 的形式。

3,從另外的查詢結果集中進行的查詢:
指的是如下的查詢方式:
SELECT [@@FieldList]
    FROM (@@DataSourceTable) st
       Where @@WhereStr
         ORDER BY @@OrderField ASC

其中,@@DataSourceTable 是另外一個查詢結果集,整個查詢的實例可能如下:

SELECT * from
 (select B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
      M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
      (select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
   FROM   TB_Flow_Bussiness B
       Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
   WHERE  M.State='01' 
) ST
where bussinessid like '%2006%' and CreatorName='XIAO'
ORDER BY PlanCreateDate DESC

這樣我們不管另外的查詢結果集有多麼複雜,都可以歸結到上面的公式中

4,從“存儲過程結果集”中進行的查詢:
在SQL2000以上,支持一種特殊的存儲過程--用戶自定義函數,它可以表的形式返回一個查詢結果集,
這樣我們就能 從“存儲過程結果集”中進行的查詢了,請看下面的例子:

--從函數中選擇結果集示例:
CREATE FUNCTION FUN_TEST1
(@STATE VARCHAR(10))
RETURNS TABLE --返回表類型
AS
RETURN
(SELECT B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
      M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State
   FROM   TB_Flow_Bussiness B
       Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
   WHERE  M.State=@STATE
)

GO
--從函數中選擇結果集
SELECT BussinessID,AddressNo,PlanCreateDate
  FROM DBO.FUN_TEST1 ('01')
    ORDER BY MPID
GO

看到了嗎?我們的From 子句中,可以包含一個用戶自定義函數,這樣我們就可以把很複雜的查詢寫在
SQLSERVER的用戶自定義函數中,在實際的使用的時候就很簡單了:)
由於從“存儲過程結果集”中進行的查詢SQL語句比較簡單,分析處理比較容易,用它構建分頁就很容易了.
(既然查詢都寫到了用戶函數中了,幹嗎不直接在存儲過程中分頁?我們這裏只是提供這麼一種可能.)

總結:
不管對於各種不同的查詢,只要我們能夠正確地提取Select ,From ,Where ,Order by 子句,我們就能夠
構建出本文所述的分頁排序語句來,從而將目前複雜的分頁問題減輕到最低程度.

到現在爲止,我們可以進行實戰了,"編程分析SQL語句"!
(2006.5.30 ,未完待續)
* 函數 MakeSQLStringByPage 在此基礎上實現了更爲複雜的分頁處理,這裏的複雜時說查詢
   * 包含大量的子查詢或者連接查詢,因此評價查詢複雜與否採用下面的標準:
   *
   * 只包含一個 SELECT 謂詞;
   * 沒有 INNER JOIN,RIGHT JOIN,LEFT JOIN 等表連接謂詞;
   * 謂詞 FROM 後只能有一個表名;
   *
   * 否則,視爲該查詢爲一個複雜查詢,採用複雜查詢分頁方案;
  

** 約束:
使用該分頁方法要求 SQL語句本身必須滿足下列條件:
1,最外層的查詢不能含有 TOP 謂詞;
2,最外層查詢必須含有 ORDER BY 語句;
3,不能含有下列替換參數(區分大小寫):@@PageSize,@@Page_Size_Number,@@LeftSize,@@Where
4,SQL必須符合 SQL-92 以上標準,且 最外層ORDER BY 語句之後不能有其他語句,
Group by 等放在Order by 之前;
**
   */

/// <summary>
 /// SQL SERVER 分頁處理,自動識別標準SQL語句並生成適合分頁的SQL語句
 /// </summary>
 public class SQLPage
 {
  public SQLPage()
  {
   //
   // TODO: 在此處添加構造函數邏輯
   //
  }

  /// <summary>
  /// 生成SQL分頁語句,記錄總數爲0表示生成統計語句
  /// </summary>
  /// <param name="strSQLInfo">原始SQL語句</param>
  /// <param name="strWhere">在分頁前要替換的字符串,用於分頁前的篩選</param>
  /// <param name="PageSize">頁大小</param>
  /// <param name="PageNumber">頁碼</param>
  /// <param name="AllCount">記錄總數</param>
  /// <returns>生成SQL分頁語句</returns>
  public static string MakeSQLStringByPage(string strSQLInfo,string strWhere,int PageSize,int PageNumber,int AllCount)
  {
   string strSQLType=string.Empty ;
   if(AllCount!=0)
   {
    if(PageNumber==1) //首頁
    {
     strSQLType="First";
    }
    else if(PageSize*PageNumber>AllCount) //最後的頁 @@LeftSize
    {
     PageSize=AllCount-PageSize*(PageNumber-1);
     strSQLType="Last";
    }
    else //中間頁
    {
     strSQLType="Mid";
    }
   }
   else //特殊處理
   {
    strSQLType="Count";
   }
   string SQL=MakeSQLStringByPage( strSQLInfo, strSQLType);
   //執行分頁參數替換
   SQL=SQL.Replace ("@@PageSize",PageSize.ToString ())
    .Replace ("@@Page_Size_Number",Convert.ToString (PageSize * PageNumber))
    .Replace ("@@LeftSize",PageSize.ToString ())
    .Replace ("@@Where",strWhere);
   return SQL;

  }

  private static string MakeSQLStringByPage(string strSQLInfo,string strSQLType)
  {
   #region SQL 複雜度分析
   //SQL 複雜度分析 開始
   bool SqlFlag=true;//簡單SQL標記
   string TestSQL=strSQLInfo.ToUpper ();
   int n=TestSQL.IndexOf ("SELECT ",0);
   n=TestSQL.IndexOf ("SELECT ",n+7);
   if(n==-1)
   {
    //可能是簡單的查詢,再次處理
    n=TestSQL.IndexOf (" JOIN ",n+7);
    if(n!=-1) SqlFlag=false;
    else
    {
     //判斷From 謂詞情況
     n=TestSQL.IndexOf("FROM ",9);
     if(n==-1) return "";
     //計算 WHERE 謂詞的位置
     int m=TestSQL.IndexOf ("WHERE ",n+5);
     // 如果沒有WHERE 謂詞
     if(m==-1) m=TestSQL.IndexOf ("ORDER BY ",n+5);
     //如果沒有ORDER BY 謂詞,那麼無法排序,退出;
     if(m==-1) return "";
     string strTableName=TestSQL.Substring (n,m-n);
     //表名中有 , 號表示是多表查詢
     if(strTableName.IndexOf (",")!=-1)
      SqlFlag=false;
    }
   }
   else
   {
    //有子查詢;
    SqlFlag=false;
   }
   //SQL 複雜度分析 結束
   #endregion

   #region 排序語法分析
   //排序語法分析 開始
   int iOrderAt=strSQLInfo.ToLower ().LastIndexOf ("order by ");
   //如果沒有ORDER BY 謂詞,那麼無法排序分頁,退出;
   if(iOrderAt==-1) return "";
   string strOrder=strSQLInfo.Substring (iOrderAt+9);
   strSQLInfo=strSQLInfo.Substring(0,iOrderAt);
   string[] strArrOrder=strOrder.Split (new char []{','});
   for(int i=0;i<strArrOrder.Length ;i++)
   {
    string[] strArrTemp=(strArrOrder[i].Trim ()+" ").Split (new char[]{' '});
    //壓縮多餘空格
    for(int j=1;j<strArrTemp.Length ;j++)
    {
     if(strArrTemp[j].Trim ()=="")
     {
      continue;
     }
     else
     {
      strArrTemp[1]=strArrTemp[j];
      if(j >1 ) strArrTemp[j]="";
      break;
     }
    }
    //判斷字段的排序類型
    switch(strArrTemp[1].Trim ().ToUpper ())
    {
     case "DESC":
      strArrTemp[1]="ASC";
      break;
     case "ASC":
      strArrTemp[1]="DESC";
      break;
     default:
      //未指定排序類型,默認爲降序
      strArrTemp[1]="DESC";
      break;
    }
    //消除排序字段對象限定符
    if(strArrTemp[0].IndexOf (".")!=-1)
     strArrTemp[0]=strArrTemp[0].Substring (strArrTemp[0].IndexOf (".")+1);
    strArrOrder[i]=string.Join (" ",strArrTemp);

   }
   //生成反向排序語句
   string strNewOrder=string.Join (",",strArrOrder).Trim ();
   strOrder=strNewOrder.Replace ("ASC","ASC0").Replace ("DESC","ASC").Replace ("ASC0","DESC");
   //排序語法分析結束
   #endregion

   #region 構造分頁查詢
   string SQL=string.Empty ;
   if(!SqlFlag)
   {
    //複雜查詢處理
    switch(strSQLType.ToUpper ())
    {
     case "FIRST":
      SQL="Select Top @@PageSize * FROM (/n" +strSQLInfo+
       "/n) T0 @@Where ORDER BY "+strOrder;
      break;
     case "MID":
      SQL=@"SELECT Top @@PageSize * FROM
                (SELECT Top @@PageSize * FROM
               (
                             SELECT Top @@Page_Size_Number * FROM (";
      SQL+="/n"+strSQLInfo+" ) P_T0 @@Where ORDER BY "+strOrder+"/n";
      SQL+=@") P_T1
   ORDER BY "+ strNewOrder +") P_T2 /n"+
       "ORDER BY "+strOrder;
      break;
     case "LAST":
      SQL=@"SELECT * FROM ( 
                       Select Top @@LeftSize * FROM ("+"/n/r"+strSQLInfo+"/r";
      SQL+=" ) P_T0 @@Where ORDER BY "+ strNewOrder+"/n/r"+
       " ) P_T1 ORDER BY "+strOrder;
      break;
     case "COUNT":
      SQL="Select COUNT(*) FROM ( " +strSQLInfo+" ) P_Count @@Where";
      break;
     default:
      SQL=strSQLInfo+strOrder;//還原
      break;
    }

   }
   else
   {
    //簡單查詢處理
    switch(strSQLType.ToUpper ())
    {
     case "FIRST":
      SQL=strSQLInfo.ToUpper().Replace ("SELECT ","SELECT TOP @@PageSize ");
      SQL+="  @@Where ORDER BY "+strOrder;
      break;
     case "MID":
      string strRep=@"SELECT Top @@PageSize * FROM
                (SELECT Top @@PageSize * FROM
               (
                             SELECT Top @@Page_Size_Number  ";
      SQL=strSQLInfo.ToUpper().Replace ("SELECT ",strRep);
      SQL+="  @@Where ORDER BY "+strOrder;
      SQL+=" /r/n) P_T0 ORDER BY "+ strNewOrder+"/n/r"+
       " ) P_T1 ORDER BY "+strOrder;
      break;
     case "LAST":
      string strRep2=@"SELECT * FROM ( 
                       Select Top @@LeftSize ";
      SQL=strSQLInfo.ToUpper().Replace ("SELECT ",strRep2);
      SQL+=" @@Where ORDER BY "+ strNewOrder+"/n/r"+
       " ) P_T1 ORDER BY "+strOrder;
      break;
     case "COUNT":
      SQL="Select COUNT(*) FROM ( " +strSQLInfo+" ) P_Count @@Where";
      break;
     default:
      SQL=strSQLInfo+strOrder;//還原
      break;
    }
   }
   return SQL;
   #endregion
  }
 }Oracle分頁算法

Oracle :
基本的分頁原理利用Oracle內指的 rownum 僞列,它是一個遞增序列,但是它在Order by 之前生成,通常
採用下面的分頁語句:
select * from
 (select rownum r_n,temptable.* from 
   ( @@SourceSQL ) temptable
 ) temptable2 where r_n between @@RecStart  and @@RecEnd
其中:
@@SourceSQL :當前任意複雜的SQL語句
@@RecStart:記錄開始的點,等於 ((tCurPage -1) * tPageSize +1)
@@RecEnd  :記錄結束的點,等於 (tCurPage * tPageSize)


  /// <summary>
  /// Oracle 分頁SQL語句生成器
  /// </summary>
  /// <param name="strSQLInfo">原始SQL語句</param>
  /// <param name="strWhere">在分頁前要替換的字符串,用於分頁前的篩選</param>
  /// <param name="PageSize">頁大小</param>
  /// <param name="PageNumber">頁碼</param>
  /// <param name="AllCount">記錄總數</param>
  /// <returns>生成SQL分頁語句</returns>
  private static string MakePageSQLStringByOracle(string strSQLInfo,string strWhere,int PageSize,int PageNumber,int AllCount)
  {
   if(AllCount==0)
   {
    //生成統計語句 
    return "select count(*) from ("+strSQLInfo+") ";
   }
   //分頁摸板語句
   string SqlTemplate=@"SELECT * FROM
 (SELECT rownum r_n,temptable.* FROM 
   ( @@SourceSQL ) temptable @@Where
 ) temptable2 WHERE r_n BETWEEN @@RecStart  AND  @@RecEnd";

   int iRecStart= (PageNumber -1) * PageSize +1 ;
   int iRecEnd  = PageNumber * PageSize ;
   //執行參數替換
   string SQL=SqlTemplate.Replace ("@@SourceSQL",strSQLInfo)
    .Replace ("@@Where",strWhere)
    .Replace ("@@RecStart",iRecStart.ToString ())
    .Replace ("@@RecEnd",iRecEnd.ToString ());
   return strSQLInfo;
  }

發佈了36 篇原創文章 · 獲贊 12 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章