• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • ADO.NET實現應用程序數據訪問層

    發表于:2007-05-25來源:作者:點擊數: 標簽:ADO.NET應用程序訪問實現數據
    [導讀] 實現數據訪問功能是大多數使用.NET Framework的 開發 人員的核心工作,并且由他們生成的數據訪問層是其應用程序必不可少的組成部分。本文概述了五個建議,希望您在使用Visual Studio .NET和.NET Framework生成數據訪問層時予以考慮。這些技巧包括通過
    [導讀]實現數據訪問功能是大多數使用.NET Framework的開發人員的核心工作,并且由他們生成的數據訪問層是其應用程序必不可少的組成部分。本文概述了五個建議,希望您在使用Visual Studio .NET和.NET Framework生成數據訪問層時予以考慮。這些技巧包括通過使用基類來利用面向對象的技術和.NET Framework基礎結構,通過遵循某些準則使類變得易于繼承,以及在就表示方法和外部接口進行決策之前仔細分析自己的需要。

      如果您要針對Microsoft .NET Framework來開發以數據為中心的應用程序,那么您終將需要創建數據訪問層(DAL)。您可能知道在.NET Framework中生成代碼帶來的一些好處。因為它同時支持實現和接口繼承,所以您的代碼可以具有更高的可重用性,尤其是可供您的組織中那些使用與Framework兼容的其他編程語言的開發人員重用。在本文中,我將介紹為了針對基于.NET Framework的應用程序而開發DAL的五個規則。在開始之前,我要說明的是,基于本文中討論的規則生成的任何DAL都將與Windows平臺上的開發人員所喜愛的傳統的多層或N層應用程序兼容。在該體系結構中,表示層由對協調數據訪問層工作的業務層進行調用的Web窗體、Windows窗體或XML Web Service代碼組成。該層包含多個數據訪問類。另外,在不需要進行業務處理協調的情況下,表示層可能會直接對DAL進行調用。該體系結構是傳統的模型-視圖-控制器(MVC)模式的變體,并且在許多方面由Visual Studio.NET及其公開的控件所采用。

      規則1:使用面向對象的功能

      最基礎的面向對象的任務是使用繼承的實現來創建抽象基類,該基類可以包含所有數據訪問類可以通過繼承使用的服務。如果這些服務足夠通用,則可以通過在整個組織中分發基類來對它們進行重用。例如,在最簡單的情況下,基類可以為派生類完成連接對象的創建,如圖1所示。

    Imports System.Data.SqlClient

    Namespace ACME.Data

    Public MustInherit Class DALBase : Implements IDisposable

     Private _connection As SqlConnection

     Protected Sub New(ByVal connect As String)
      _connection = New SqlConnection(connect)
     End Sub

     Protected ReadOnly Property Connection() As SqlConnection
     Get
      Return _connection
     End Get
    End Property

    Public Sub Dispose() Implements IDisposable.Dispose
     _connection.Dispose()
    End Sub

    End Class

    End Namespace
                     圖1 簡單的基類

      正如您在該圖中看到的那樣,DALBase類被標記為MustInherit(在C#中為abstract),以確保它用于繼承關系。該類隨后會包含一個在公共構造函數(它接受連接字符串作為參數)中實例化的私有SqlConnection對象。然后,受保護的Connection屬性允許派生類訪問該連接對象,而IDisposable接口中的Dispose方法則確保該連接對象得以被處理。即使是在下面這個簡化的示例中,您也可以從中注意到抽象基類的用處:

    Public Class WebData : Inherits DALBase
    Public Sub New()
     MyBase.New(ConfigurationSettings.AppSettings("ConnectString"))
    End Sub

    Public Function GetOrders() As DataSet
     Dim da As New SqlDataAdapter("usp_GetOrders", Me.Connection)
     da.SelectCommand.CommandType = CommandType.StoredProcedure
     Dim ds As New DataSet()
     da.Fill(ds)
     Return ds
    End Function
    End Class

      在該示例中,WebData類繼承自DALBase,因此它不需要考慮實例化SqlConnection對象的問題,而只需通過MyBase關鍵字(或C#中的base關鍵字)將連接字符串傳遞給基類。WebData類的GetOrders方法可以使用Me.Connection(在C#中為this.Connection)訪問受保護的屬性。盡管該示例相對簡單,但如果您看了規則2和規則3的話,就會發現該基類還可以提供其他服務。

      當DAL需要在COM+環境中運行時,抽象基類尤其有用。在這種情況下,因為允許組件使用COM+所需的代碼更為復雜,所以創建一個如圖2中所示的服務組件基類是有意義的。

    <ConstructionEnabled(True), _
    Transaction(TransactionOption.Supported), _
    EventTrackingEnabled(True)> _
    Public MustInherit Class DALServicedBase : Inherits ServicedComponent

    Private _connection As SqlConnection

     Protected Overrides Sub Construct(ByVal s As String)
      _connection = New SqlConnection(s)
     End Sub

     Protected ReadOnly Property Connection() As SqlConnection
     Get
      Return _connection
     End Get
    End Property

    End Class
                         圖2 服務組件基類

      在該代碼中,DALServicedBase類基本上包含了與圖1中相同的功能,但是它另外繼承了System.EnterpriseServices命名空間中的ServicedComponent,并且包含了一些屬性以指明該組件支持對象結構、事務和統計信息跟蹤。然后,該基類負責捕捉在組件服務管理器中配置的結構字符串,并再一次創建和公開SqlConnection對象。需要注意的是,當一個類從DALServicedBase繼承時,它還將繼承屬性的設置。換句話說,派生類也會將它的事務選項設置為Supported。如果該派生類想要重寫該行為,則它可以在類級別重新定義該屬性。此外,派生類還應該在適當位置對自身利用重載方法和共享方法。主要有兩種使用重載方法(具有多個簽名的單個方法)的情況。第一,當方法需要接收改變其類型的參數時,可以使用它們。在Framework中,這一類型的典型示例是System.Convert類的方法。例如,ToString方法包括18個接收一個參數的重載方法,每個方法都具有不同的類型。第二,重載方法可以用來公開參數數量不斷增加(但不一定是不同類型的參數)的簽名。這種類型的重載證明在DAL中非常有效,因為可以使用它來公開用于數據檢索和修改的備用簽名。例如,可以重載GetOrders方法以便一個簽名不接收任何參數并返回所有訂單,而另一個簽名則接收一個表明調用方只打算檢索特定客戶訂單的參數,如下面的代碼所示:

    Public Overloads Function GetOrders() As DataSet
    Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet

      在這種情況下,良好的實現技巧是將GetOrders方法的功能抽象到一個可以由每個重載簽名調用的私有或受保護的方法中。還可以使用共享方法(在C#中為static方法)來展開可供數據訪問類的所有實例訪問的字段、屬性和方法。盡管不能將共享成員與使用組件服務的類結合使用,但是對于可以在數據訪問類的共享構造函數中檢索然后被所有實例讀取的只讀數據來說,它們可能十分有用。在對讀/寫數據使用共享成員時要特別小心,這是因為多個執行線程可能會競爭使用對共享數據的訪問權。



      規則2:遵守設計準則

      在Visual Studio .NET隨附的聯機文檔中,有一個標題為“Design Guidelines for Class Library Developers”的主題,它不僅論述您應該遵循的重載成員、構造函數和事件的模式,而且還討論了類、屬性和方法的命名約定。您應該遵守命名約定的主要原因之一是.NET Framework提供的交叉語言繼承。如果您要在Visual Basic .NET中生成一個DAL基類,則您需要確保那些使用與.NET Framework兼容的其他語言的開發人員可以從它繼承,并能容易地理解它的工作方式。按照我概述過的準則去做,那么您的命名約定和結構將不會是特定于語言的了。舉個例子,您會在本文的代碼示例中注意到,Camel大小寫風格(首個單詞小寫,并夾雜大寫字母)用于方法的參數,Pascal大小寫風格(每個單詞都大寫)用于方法,而基類則具有Base后綴以表示它是一個抽象類。.NET Framework設計準則的必然結果是常規設計模式,就像Gang of Four撰寫的Design Patterns(Addison-Wesley, 1995)中所介紹的那些設計模式一樣。例如,.NET Framework使用了Observer模式的一個名為Event模式的變體(您在類中公開事件時應當遵循該模式)。

      規則3:利用基礎結構

      .NET Framework包含一些可以幫助處理與基礎結構相關的一般性任務(例如,檢測和異常處理)的類和結構,通過基類將這些概念與繼承相結合可能十分有用。例如,請考慮在System.Diagnostics命名空間中公開的跟蹤功能。除了Trace和Debug類,該命名空間還包括從Switch和TraceListener派生的類。Switch類—BooleanSwitch和TraceSwitch,可以通過編程方式以及通過應用程序的配置文件而被配置為打開和關閉。就TraceSwitch而言,可以公開多個級別的跟蹤。TraceListener類—TextWriterTraceListener和EventLogTraceListener將Trace和Debug方法的輸出分別定向到文本文件和事件日志。因此,您可以將跟蹤功能添加到基類中,以便派生類可以輕松地記錄消息。繼而,應用程序可以使用應用程序配置文件來控制是否啟用跟蹤。您可以通過包含一個BooleanSwitch類型的私有變量并在構造函數中將其實例化,以將該功能添加到圖1所示的DALBase類中:

    Public Sub New(ByVal connect As String)
     _connection = New SqlConnection(connect)
     _dalSwitch = New BooleanSwitch("DAL", "Data Aclearcase/" target="_blank" >ccess Code")
    End Sub

      BooleanSwitch的參數包括它的名稱和說明。您隨后可以添加一個受保護屬性,以便將開關打開和關閉,并添加另一個受保護屬性,以便使用Trace對象的WriteLineIf方法來格式化和寫入跟蹤消息:

    Protected Property TracingEnabled() As Boolean
     Get
      Return _dalSwitch.Enabled
     End Get
     Set(ByVal Value As Boolean)
      _dalSwitch.Enabled = Value
     End Set
    End Property

    Protected Sub WriteTrace(ByVal message As String)
     Trace.WriteLineIf(Me.TracingEnabled, Now & ": " & message)
    End Sub

      這樣,派生類無須自己了解開關類和偵聽器類,就可以在數據訪問類中發生重大事件時輕松地調用WriteTrace方法。

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
    <system.diagnostics>
    <switches>
    <add name="DAL" value="1" />
    </switches>
    <trace autoflush="true" indentsize="4">
    <listeners>
    <add name="myListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="DALLog.txt"/>
    </listeners>
    </trace>
    </system.diagnostics>
    </configuration>
                        圖3 跟蹤配置

      要創建偵聽器并將其打開,還可以使用應用程序配置文件。圖3顯示了一個簡單的配置文件,它打開剛剛顯示的DAL開關,并通過名為myListener的TextWriterTraceListener將它的輸出定向到DALLog.txt文件。當然,您還可以通過從TraceListener類派生并將偵聽器直接包含在DAL中,來通過編程方式創建自己的偵聽器。

    Public Class DALException : Inherits ApplicationException

     Public Sub New()
      MyBase.New()
     End Sub

     Public Sub New(ByVal message As String)
      MyBase.New(message)
     End Sub

     Public Sub New(ByVal message As String, ByVal innerException As Exception)
      MyBase.New(message, innerException)
     End Sub

     ' Add custom members here
     Public ConnectString As String
    End Class
                          圖4 自定義異常類

      毫無疑問,您應該利用的另一個基礎結構是結構化異常處理(SEH)。在最基本的級別,DAL可以公開它自己從System.ApplicationException繼承的Exception對象,并且還可以公開自定義成員。例如,圖4中所示的DALException對象可以用來包裝由數據訪問類中的代碼引發的異常。然后,基類可以公開一個受保護的方法以包裝異常、填充自定義成員并將其傳回調用方,如下所示:

    Protected Sub ThrowDALException(ByVal message As String, _
    ByVal innerException As Exception)

    Dim newMine As New DALException(message, innerException)

    newMine.ConnectString = Me.Connection.ConnectionString
    Me.WriteTrace(message & "{" & innerException.Message & "}")
    Throw newMine
    End Sub

      這樣,派生類可以輕松地調用受保護的方法,傳入所截獲的特定于數據的異常(通常為SqlException或OleDbException),并添加一個與特定數據域有關的消息?;愒贒ALException中包裝該異常,并將其傳回調用方。這使得調用方可以使用單個Catch語句輕松地捕獲來自DAL的所有異常。有關信息,請參閱MSDN上發布的 《Exception Management Application Block Overview》一文(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/emab-rm.asp)。該框架通過一組對象將異常的發布與應用程序日志耦合在一起。實際上,您可以通過從.NET Framework提供的BaseApplicationException類繼承自己的自定義異常類,來將它們插入到該框架中。


      規則4:仔細選擇外部接口

      當您設計數據訪問類的方法時,需要考慮它們將如何接收和返回數據。對于大多數開發人員而言,有三種主要選擇:直接使用ADO.NET對象、使用XML和使用自定義類。如果您要直接公開ADO.NET對象,則可以利用兩個編程模型中的一個。第一個模型包含DataSet和DataTable對象,它們對于斷開連接的數據訪問很有用。關于DataSet及其關聯的DataTable,已在許多文章中進行過介紹,但是在需要處理已經與基礎數據存儲區斷開連接的數據時,它們非常有用。換句話說,DataSet可以在應用層之間傳遞,即使這些層分布在不同的物理位置也可以進行傳遞,如在業務層和數據服務層被放在與表示服務不同的服務器群集上的情況下。

      另外,對于通過基于XML的Web Service返回數據的情況來說,DataSet對象也是理想的選擇,這是因為它們可以序列化,因而可以在SOAP響應消息中返回。這有別于使用實現IDataReader接口的類(例如,SqlDataReader和OleDbDataReader)訪問數據。這些數據讀取器用于以只進、只讀方式訪問數據。這兩者之間的巨大差異在于:DataSet和DataTable對象可以按值在應用程序域之間(因而也可以在相同或不同計算機上的進程之間)傳遞,而數據讀取器則可以到處傳遞,并且總是按引用傳遞。請參見圖5,其中Read和GetValues是在服務器進程中執行的,并且它們的返回值被復制到客戶端。


    圖5 遠程處理數據讀取器

      該圖著重顯示了數據讀取器如何存在于創建它的應用程序域中,以及對它的所有訪問如何在客戶端和服務器應用程序域之間產生往返行程。這意味著,只有當數據讀取器在調用方所在的同一應用程序域中執行時,數據訪問方法才應當返回這些數據讀取器。在使用數據讀取器時,還有其他兩個需要考慮的問題。

      首先,在從數據訪問類中的方法返回數據讀取器時,需要考慮與該數據讀取器相關聯的連接對象的生存期。默認情況下,在調用方循環訪問數據讀取器時,連接會保持繁忙。遺憾的是,當調用方完成工作時,連接將保持開啟狀態,因此不會返回到連接池(如果啟用連接池的話)。但是,您可以通過將CommandBehavior.CloseConnection枚舉值傳遞給命令對象的ExecuteReader方法,來指示數據讀取器在它的Close方法被調用時關閉連接。

      其次,為了將表示層從特定的Framework data provider(例如SqlClient或OleDb)分離,調用代碼應當使用IDataReader接口而不是具體類型(例如SqlDataReader)來引用返回值。這樣,如果應用程序從Oracle移植到SQL Server后端,并且數據訪問類中的方法的返回類型進行了更改,則表示層無須更改。如果您希望數據訪問類返回XML,則可以從System.Xml命名空間中的XmlDocument和XmlReader類(它們類似于DataSet和IDataReader)中進行選擇。換句話說,當數據要從它的源斷開連接時,您的方法應當返回XmlDocument(或XmlDataDocument),而XmlReader可以用來對XML數據進行流式訪問。

      最后,您還可以決定用公共屬性返回自定義類。這些類可以用Serialization屬性標記,以便能夠跨應用程序域進行復制。另外,如果您要從方法中返回多個對象,則可能需要強類型集合類。

    Imports System.Xml.Serialization

    <Serializable()> _
    Public Class Book : Implements IComparable

    <XmlAttributeAttribute()> Public ProductID As Integer
    Public ISBN As String
    Public Title As String
    Public Author As String
    Public UnitCost As Decimal
    Public Description As String
    Public PubDate As Date

    Public Function CompareTo(ByVal o As Object) As Integer _
    Implements IComparable.CompareTo

     Dim b As Book = CType(o, Book)
     Return Me.Title.CompareTo(b.Title)
    End Function

    End Class

    Public NotInheritable Class BookCollection : Inherits ArrayList

    Default Public Shadows Property Item(ByVal productId As Integer)
    As Book

    Get
     Return Me(IndexOf(productId))
    End Get
    Set(ByVal Value As Book)
     Me(IndexOf(productId)) = Value
    End Set
    End Property

    Public Overloads Function Contains(ByVal productId As Integer) As Boolean
     Return (-1 <> IndexOf(productId))
    End Function

    Public Overloads Function IndexOf(ByVal productId As Integer) As Integer
     Dim index As Integer = 0
     Dim item As Book

     For Each item In Me
      If item.ProductID = productId Then
       Return index
      End If
      index = index + 1
     Next
     Return -1
    End Function

    Public Overloads Sub RemoveAt(ByVal productId As Integer)
     RemoveAt(IndexOf(productId))
    End Sub

    Public Shadows Function Add(ByVal value As Book) As Integer
     Return MyBase.Add(value)
    End Function

    End Class
                   圖6 使用一個自定義類

      圖6包含了一個簡單的Book類及其關聯的集合類的示例。您會發現,Book類用Serializable進行了標記,以便跨應用程序域啟用“按值(by value)”語義。該類實現了IComparable接口,以便當它包含在集合類中的時候,它將在默認情況下按Title排序。BookCollection類派生自System.Collections命名空間中的ArrayList,并且遮蔽了Item屬性和Add方法,以便將集合限制為僅包含Book對象。通過使用自定義類,您可以獲得對數據表示方法的完全控制,通過強類型化和IntelliSense提高開發人員的工作效率,并且消除對ADO.NET的調用方依賴性。但是,由于.NET Framework不包含任何與對象相關的映射技術(除了本質上是派生的DataSet類的類型化DataSet對象以外),因此該方法需要更多的代碼。在這些情況下,您通常要在數據訪問類中創建一個數據讀取器,并且使用它來填充自定義類。


      規則5:抽象化.NET Framework data provider

      最后一個規則指定,為什么應該對在DAL內部使用的.NET Framework data provider抽象化,以及應該如何進行抽象。正如我已經提到的那樣,ADO.NET編程模型公開了獨特的.NET Framework data provider,包括SqlClient、OleDb和其他可從 MSDN 在線Web站點上獲得的data provider。盡管該設計能夠提高性能,并且使provider能夠公開特定于數據源的功能(例如SqlCommand對象的ExecuteXmlReader方法),但它會迫使開發人員決定針對哪個provider進行編碼。換句話說,開發人員通常選擇使用SqlClient或OleDb,然后直接針對各個命名空間中的類編寫代碼。

    public enum ProviderType : int {SqlClient = 0, OLEDB = 1}

    public class ProviderFactory {

     public ProviderFactory(ProviderType provider) {
      _pType = provider;
      _initClass();
     }

     public ProviderFactory() {
      _initClass();
     }

     private ProviderType _pType = ProviderType.SqlClient;
     private bool _pTypeSet = false;
     private Type[] _conType, _comType, _parmType, _daType;

     private void _initClass() {
      _conType = new Type[2];
      _comType = new Type[2];
      _parmType = new Type[2];
      _daType = new Type[2];

      // Initialize the types for the providers
      _conType[(int)ProviderType.SqlClient] = typeof(SqlConnection);
      _conType[(int)ProviderType.OLEDB] = typeof(OleDbConnection);
      _comType[(int)ProviderType.SqlClient] = typeof(SqlCommand);
      _comType[(int)ProviderType.OLEDB] = typeof(OleDbCommand);
      _parmType[(int)ProviderType.SqlClient] = typeof(SqlParameter);
      _parmType[(int)ProviderType.OLEDB] = typeof(OleDbParameter);
      _daType[(int)ProviderType.SqlClient] = typeof(SqlDataAdapter);
      _daType[(int)ProviderType.OLEDB] = typeof(OleDbDataAdapter);
     }

     public ProviderType Provider {
      get {
       return _pType;
      }
      set {
       if (_pTypeSet) {
        throw new ReadOnlyException("Provider already set to " + _pType.ToString());
       }
       else {
        _pType = value;
        _pTypeSet = true;
       }
      }
     }

     public IDataAdapter CreateDataAdapter(string commandText,IDbConnection connection) {
      IDataAdapter d;
      IDbDataAdapter da;

      d = (IDataAdapter)Activator.CreateInstance(_daType[(int)_pType], false);
      da = (IDbDataAdapter)d;
      da.SelectCommand = this.CreateCommand(commandText, connection);
      return d;
     }

     public IDataParameter CreateParameter(string paramName, DbType paramType) {
      IDataParameter p;
      p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType], false);
      p.ParameterName = paramName;
      p.DbType = paramType;
      return p;
     }

     public IDataParameter CreateParameter(string paramName, DbType paramType, Object value) {
      IDataParameter p;
      p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType], false);
      p.ParameterName = paramName;
      p.DbType = paramType;
      p.Value = value;
      return p;
     }

     public IDbConnection CreateConnection(string connect) {
      IDbConnection c;
      c = (IDbConnection)Activator.CreateInstance(_conType[(int)_pType], false);
      c.ConnectionString = connect;
      return c;
     }

     public IDbCommand CreateCommand(string cmdText, IDbConnection connection) {
      IDbCommand c;
      c = (IDbCommand)Activator.CreateInstance(_comType[(int)_pType], false);
      c.CommandText = cmdText;
      c.Connection = connection;
      return c;
     }
    }
                 圖7 Provider Factory

      如果您要更改.NET Framework data provider,則需要重新編寫數據訪問方法的代碼。為了避免這種情況,可以使用稱為“抽象工廠(Abstrace Factory)”的設計模式。使用該模式,可以生成一個簡單的類,該類將公開能夠基于標識傳入到構造函數的.NET Framework data provider的信息來創建主要的.NET Framework data provider對象(命令、連接、數據適配器和參數)的方法。圖7中的代碼顯示了該類的一個簡化的C#版本。為了使用該類,數據訪問類中的代碼需要針對.NET Framework data provider實現的各種接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)進行編程。例如,為了用來自參數化存儲過程的結果填充DataSet,您可以在數據訪問類的方法內部使用以下代碼:

    Dim _pf As New ProviderFactory(ProviderType.SqlClient)
    Dim cn As IDbConnection = _pf.CreateConnection(_connect)
    Dim da As IDataAdapter = _pf.CreateDataAdapter("usp_GetBook", cn)

    Dim db As IDbDataAdapter = CType(da, IDbDataAdapter)
    db.SelectCommand.CommandType = CommandType.StoredProcedure
    db.SelectCommand.Parameters.Add(_pf.CreateParameter("@productId", _
    DbType.Int32, id))

    Dim ds As New DataSet("Books")
    da.Fill(ds)

      通常,您需要在類級別聲明ProviderFactory變量,并且在數據訪問類的構造函數中將其實例化。另外,就像這里顯示的那樣,它的構造函數將用從配置文件中讀取的data provider填充,而不是硬編碼。如您想像的那樣,將ProviderFactory添加到DAL基類中會非常美妙,然后可以將它包含在程序集中并分發給其他開發人員。您還可以更深入一步,以封裝開發人員反復編寫的常見ADO.NET代碼。實際上,Microsoft已經發布了一個能夠為SQL Server執行該功能的數據訪問應用程序塊(參見《Data Access Application Block Overview》, http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/daab-rm.asp)。

      小結

      身處于Web Service時代之中,我們會構建越來越多的應用程序—從獨立的應用層來操縱數據。如果您遵循某些基本規則并最終將其作為一種習慣,那么編寫數據訪問代碼會變得更快、更容易并且代碼也會具有更高的可重用性,從而節省了與服務器之間的交互并使您在代碼中保持數據的獨立。

    原文轉自:http://www.kjueaiud.com

    評論列表(網友評論僅供網友表達個人看法,并不表明本站同意其觀點或證實其描述)
    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>