.net數據庫連接詳解


ADO.NET與抽水的故事

ADO.NET是微軟新一代.NET數據庫的訪問架構,ADO是ActiveX Data Objects的縮寫。ADO.NET是數據庫應用程序和數據源之間溝通的橋梁,主要提供一個面向對象的數據訪問架構,用來開發數據庫應用程序。

5.1.1  ADO.NET的定義

ADO.NET是為.NET框架而創建的,它提供對 Microsoft SQL Server、Oracle等數據源及通過OLE DB和XML公開的數據源的一致訪問。應用程序可以使用ADO.NET來連接到這些數據源,並檢索、操作和更新數據。ADO.NET是對Microsoft ActiveX Data Objects(ADO)的一個跨時代的改進,是一種全新的數據訪問方法,是一項新技術、新設計。它提供了平台互用性和可伸縮的數據訪問。由於傳送的數據都是XML格式的,因此任何能夠讀取XML格式的應用程序都可以進行數據處理。事實上,接受數據的組件不一定要是ADO .NET組件,它可以是一個基於Microsoft Visual Studio的解決方案,也可以是任何運行在其他平台上的應用程序。

簡單地講,ADO.NET是用於和數據源打交道的.NET技術,是一組向 .NET 程序員公開數據訪問服務的類。ADO.NET提供了一個統一的編程模式和一組公用的類來進行任何類型的數據訪問,而不管你用何種語言來開發代碼。

ADO.NET有兩個重要組成部分:.NET數據提供程序(.NET Data Provider)和數據集(DataSet)。

1..NET數據提供程序(.NET Data Provider)

.NET數據提供程序是一個類集,用於連接到數據庫、執行命令和檢索結果。可以直接處理檢索到的結果,或將其放入DataSet對象中。它可以被認為是數據庫與應用程序的一個接口件或中間件。由於現在使用的數據源有多種(SQL Server、OLEDB 、ODBC 、Oracle),在編寫應用程序的時候就要針對不同的數據源編寫不同的接口代碼,這很麻煩,效率也不高,針對這一問題Data Provider向上(應用程序)提供了統一的編程模型,向下(數據源)提供了多種數據源的接口,這樣一來就可以使應用程序不需關心什么數據源,即對數據源進行了屏蔽,其好處是無論什么樣的數據源,對於應用程序來說都只需提供一種編程模式即可。

針對不同的數據源,目前在.NET平台中包含如下.NET數據提供程序。

l         SQL Server .NET數據提供程序:提供專門針對SQL Server 7.0版或更高版本的數據訪問。使用 System.Data.SqlClient 命名空間。

l         OLEDB .NET數據提供程序:適合於使用 OLE DB 公開的數據源。使用 System.Data.OleDb 命名空間。

l         ODBC .NET數據提供程序:針對一些老式的數據源的訪問。

l         Oracle .NET數據提供程序:針對Oracle數據庫的訪問(需要Oracle Client的支持)。

l         MySQL .NET數據提供程序:針對MySQL的數據訪問(需要MySQL Connector/Net 的支持)。

l         SQLite .NET數據提供程序(非官方,由sqlite.phxsoftware.com提供)。

l         PostgreSQL .NET數據提供程序(非官方,由pgfoundry.org/projects/npgsql提供)。

備注:

OLE DB 是一種技術標准,目的是提供一種統一的數據訪問接口,這里所說的“數據”,除了標准的關系型數據庫中的數據之外,還包括郵件數據、Web 上的文本或圖形、目錄服務(Directory Services),以及主機系統中的IMS 和VSAM 數據。OLE DB 標准的核心內容就是要求對以上這些各種各樣的數據存儲提供一種相同的訪問接口,使得數據的使用者(應用程序)可以使用同樣的方法訪問各種格式的數據,而不用考慮數據的具體存儲地點、格式或類型。

使用OLE DB的方法可以通過新建一個后綴是udl的文本文件進行編輯,如圖5-1所示。

 

圖5-1  OLE DB設置

雖然OLE DB .NET數據提供程序可以訪問任何數據源且比較通用,但專門針對特定數據庫類型設計的.NET數據提供程序(如SQL Server .NET數據提供程序、Oracle .NET數據提供程序)具有相應的性能優化設計,相比之下OLE DB .NET數據提供程序的訪問速度更慢。所以,建議能盡量用特定的數據提供程序的就盡量用數據提供程序。

.NET數據提供程序有以下幾個核心對象。

l         Connection 對象:用於連接數據源。

l         Command 對象:對數據源執行命令。

l         DataReader 對象:在只讀和只寫的連接模式下從數據源讀取數據。

l         DataAdapter 對象:從數據源讀取數據並使用所讀取的數據填充數據集對象。

所有.NET數據提供程序都實現了這幾個對象各自的版本,附加了它們各自的前綴。

2.數據集(DataSet)

DataSet專門為獨立於任何數據源的數據訪問而設計。因此,它可以用於多種不同的數據源,用於XML數據,或用於管理應用程序本地的數據。DataSet包含一個或多個DataTable對象的集合,這些對象由數據行和數據列,以及有關DataTable對象中數據的主鍵、外鍵、約束和關系信息組成。

我們可以看一下ADO.NET的多數據庫連通性的特點,如圖5-2所示。

 

圖5-2  ADO.NET的多數據庫連通性

多個類型數據庫,上層共享單一DataSet對象,實現單應用程序下多數據庫底層支持。

本章不再使用大量篇幅來闡述ADO.NET的原理細節,而從實際項目應用的角度言簡意賅地說明ADO.NET的使用方法和常見問題。

5.1.2  趣味理解ADO.NET對象模型

為了更好地理解ADO.NET的架構模型的各個組成部分,我們可以對ADO.NET中的相關對象進行圖示理解,如圖5-3所示的是ADO.NET中數據庫對象的關系圖。

 

圖5-3  ADO.NET對象模型

我們可以用趣味形象化的方式理解ADO.NET對象模型的各個部分,如圖5-4所示,可以看出這些對象所處的地位和對象間的邏輯關系。

 

圖5-4  ADO.NET趣味理解圖

 

趣味理解

對比ADO.NET的數據庫對象的關系圖,我們可以用對比的方法來形象地理解每個對象的作用,如圖5-4所示。

l         數據庫好比水源,存儲了大量的數據。

l         Connection好比伸入水中的進水籠頭,保持與水的接觸,只有它與水進行了“連接”,其他對象才可以抽到水。

l         Command則像抽水機,為抽水提供動力和執行方法,通過“水龍頭”,然后把水返給上面的“水管”。

l         DataAdapter、DataReader就像輸水管,擔任着水的傳輸任務,並起着橋梁的作用。二者是有不同的,后面章節中將詳細介紹。

l         DataSet則是一個大水庫,把抽上來的水按一定關系的池子進行存放。即使撤掉“抽水裝置”(斷開連接,離線狀態),也可以保持“水”的存在。這也正是ADO.NET的核心。

l         DataTable則像水庫中的每個獨立的水池子,分別存放不同種類的水。一個大水庫由一個或多個這樣的水池子組成。

5.1.3  進水籠頭——建立Connection

Connection表示與數據源之間的連接。可根據Connection對象的各種不同屬性來指定數據源的類型、位置及其他屬性,可用它來與數據庫建立連接或斷開連接。其他對象如DataAdapter和Command對象通過它與數據庫通信。根據.NET Framework 數據提供程序的不同,也有幾種不同的Connection,如針對SQL Server的SqlConnection、針對Oracle的OracleConnection、針對MySQL的MySqlConnection、針對OLEDB的OleDbConnection等。(本節代碼示例位置:光盤\code\ch05\01)

1.用SqlConnection連接SQL Server

(1)加入命名空間:

using System.Data.SqlClient;

(2)連接數據庫:

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;password=";

SqlConnection myConnection = new SqlConnection(conString);

myConnection.Open();

2.用OracleConnection連接Oracle

首先添加OracleClient的引用,如圖5-5所示。

 

圖5-5  添加OracleClient的引用

(1)加入命名空間:

using System.Data.OracleClient;

(2)連接數據庫:

string conString = "Data Source=codematic;User ID=codeuser;Password=user123";

OracleConnection myConnection = new OracleConnection(conString);

myConnection.Open();

3.用MySqlConnection連接MySQL

在.NET中連接MySQL數據庫有兩種方法:MySQL Connector/ODBC 和 MySQL Connector/NET,ODBC連接器是符合ODBC標准的交互平台,是.NET訪問MySQL數據庫的最好的選擇。

首先,我們下載安裝MySql-connector-net-5.1.5.Data.msi這個組件。如果是默認安裝,則可以在C:\Program Files\MySQL\MySQL Connector Net 5.1.5\Binaries\.NET 2.0(這里安裝的是MySQL Connector/Net 5.1.5,老的1.1版本是:C:\Program Files\MySQL\MySQL Connector Net 1.0.4\bin\.NET 1.1\)中找到MySql.Data.dll,將該文件復制到項目的bin目錄下。

然后在項目引用中添加MySql.Data.dll的引用,如圖5-6所示。

 

圖5-6  添加MySql.Data.dll的引用

實現代碼如下。

(1)加入命名空間:

using MySql.Data.MySqlClient;

(2)連接數據庫:

string conString = "server=127.0.0.1;database=mysql;user id=root;password=123";

MySqlConnection myConnection = new MySqlConnection(conString);

myConnection.Open();

4.用OleDbConnection連接各種數據源

由於數據源不同,相應的連接字符串也會不同。

(1)加入命名空間:

using System.Data.OleDb;

(2)連接SQL Server:

string conString = " Provider=SQLOLEDB.1;Persist Security Info=False;

        User ID=sa;Database=Codematic;Data Source=COMPUTER";

OleDbConnection myConnection = new OleDbConnection(conString);

myConnection.Open();

(3)連接Access(可通過建立.udl文件來獲得字符串):

string conString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source

=C:\\Database1.mdb;Persist Security Info=False";

(4)連接Oracle(也可通過OracleConnection連接):

string conString = "Provider=MSDAORA.1;User ID=user; Password=123;

Data Source=db;Persist Security Info=False";

從以上幾個對象實例對比來看,幾個.NET數據提供程序組件模型的基本編程模式相同,只是組件對象的前綴有所區別,正是這種統一編程模型,讓我們在做不同類型數據庫開發時,變得非常簡單。

5.1.4  抽水機——Command

Command對象封裝了與用戶想要完成的動作相關的數據庫命令。在一般情況下這些命令就是SQL語句。

1.構造SqlCommand

(1)初始化SqlCommand類的新實例。

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString);

           

SqlCommand myCommand = new SqlCommand();

myCommand.Connection = myConnection;

myCommand.CommandText = "update P_Product set Name='電腦1' where Id=52";

myConnection.Open();

int rows = myCommand.ExecuteNonQuery();

myConnection.Close();

(2)初始化具有查詢文本的 SqlCommand 類的新實例。

string conString  = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString );

string strSql = "update P_Product set Name='電腦2' where Id=52";

SqlCommand myCommand = new SqlCommand(strSql);

myCommand.Connection = myConnection;          

myConnection.Open();

int rows = myCommand.ExecuteNonQuery();

myConnection.Close();

(3)初始化具有查詢文本和 SqlConnection 的SqlCommand類實例。

string conString  = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString );

string strSql = "update P_Product set Name='電腦3' where Id=52";

SqlCommand myCommand = new SqlCommand(strSql, myConnection);

myConnection.Open();

int rows = myCommand.ExecuteNonQuery();

myConnection.Close();

(4)初始化具有查詢文本、SqlConnection 和 Transaction 的 SqlCommand 類實例。

string conString  = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString );

string strSql = "update P_Product set Name='電腦4' where Id=52";

string strSql2 = "update P_Product set Name='數碼4' where Id=53";

myConnection.Open();

SqlTransaction myTrans = myConnection.BeginTransaction();

SqlCommand myCommand = new SqlCommand(strSql, myConnection, myTrans);

try

{

    int rows = myCommand.ExecuteNonQuery();

    myCommand.CommandText = strSql2;

    rows = myCommand.ExecuteNonQuery();

    myTrans.Commit();

    myConnection.Close();

}

catch

{

    myTrans.Rollback();               

}

2.建立SqlCommand與SqlConnection的關聯

myCommand.Connection = myConnection;

//或者

SqlCommand myCommand = myConnection.CreateCommand;

3.設置SqlCommand的查詢文本

myCommand.CommandText = "SELECT * FROM P_Product ";

//或者第2種構造:

SqlCommand myCommand = new SqlCommand(strSql);

4.執行命令

SqlCommand方法如表5-1所示。

表5-1  SqlCommand方法

方    法

說    明

ExecuteReader

返回一行或多行。多用於SELECT查詢數據

ExecuteNonQuery

對Connection 執行 SQL 語句,並返回受影響的行數(int),多用於INSERT、UPDATE、DELETE、CREATE等操作

ExecuteScalar

返回單個值。返回結果集中第一行的第一列。忽略額外的列或行

ExecuteXmlReader

將 CommandText 發送到 Connection 並生成一個 XmlReader 對象

例如:

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString);

SqlCommand cmd = myConnection.CreateCommand();

cmd.CommandText = "SELECT * FROM P_Product";

myConnection.Open();

SqlDataReader dr = cmd.ExecuteReader();

//SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

while (dr.Read())//循環讀取數據

{

    Response.Write(dr.GetInt32(0).ToString() + ", " + dr.GetString(1) +

             "<br>");

}

dr.Close();

myConnection.Close();

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

   password=";

SqlConnection myConnection = new SqlConnection(conString);

string strSql = "select count(*) from P_Product";

SqlCommand myCommand = new SqlCommand(strSql, myConnection);

myConnection.Open();

int count = (int)myCommand.ExecuteScalar();

myConnection.Close();

Response.Write(count);

很多數據庫支持將多條命令合並或批處理成一條單一命令執行。例如,SQL Server使你可以用分號“;”分隔命令。將多條命令合並成單一命令,能減少到服務器的行程數,並提高應用程序的性能。例如,可以將所有預定的刪除在應用程序中本地存儲起來,然后再發出一條批處理命令調用,從數據源刪除它們。

雖然這樣做確實能提高性能,但是當對DataSet中的數據更新進行管理時,可能會增加應用程序的復雜性。要保持簡單,需要在DataSet中為每個DataTable創建一個DataAdapter。

 

注  意

使用SqlCommand執行存儲過程的快速提示:如果調用存儲過程,則需將SqlCommand的CommandType屬性指定為StoredProcedure。這樣在將該命令顯式標識為存儲過程時,就不需要在執行之前分析命令了。

5.1.5  輸水管——DataAdapter

DataAdapter提供連接DataSet對象和數據源的橋梁。DataAdapter使用Command對象在數據源中執行SQL命令,以便將數據加載到DataSet中,並使DataSet中數據的更改與數據源保持一致。

 

趣味理解

DataAdapter就像一根輸水管,通過發動機,把水從水源輸送到水庫里進行保存。

1.創建SqlDataAdapter

(1)初始化SqlDataAdapter類的新實例。

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString);

SqlCommand cmd = myConnection.CreateCommand();

cmd.CommandText = "SELECT * FROM P_Product";

DataSet ds = new DataSet();

myConnection.Open();

SqlDataAdapter adapter = new SqlDataAdapter();

adapter.SelectCommand = cmd;

adapter.Fill(ds, "ds");

myConnection.Close();

(2)使用指定的 SqlCommand 初始化 SqlDataAdapter 類的新實例。

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

        password=";

SqlConnection myConnection = new SqlConnection(conString);

DataSet ds = new DataSet();

SqlCommand cmd = myConnection.CreateCommand();

cmd.CommandText = "SELECT * FROM P_Product";

myConnection.Open();

SqlDataAdapter adapter = new SqlDataAdapter(cmd);

adapter.Fill(ds, "ds");

myConnection.Close();

(3)使用selectcommand字符串 和 SqlConnection對象初始化SqlDataAdapter 類的新實例。

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

password=";

string strSQL = "SELECT * FROM P_Product";

SqlConnection myConnection = new SqlConnection(conString);

DataSet ds = new DataSet();

myConnection.Open();

SqlDataAdapter adapter = new SqlDataAdapter(strSQL, myConnection);

adapter.Fill(ds, "ds");

myConnection.Close();

(4)使用selectcommand字符串和一個連接字符串初始化SqlDataAdapter類的新實例。

string conString = "data source=127.0.0.1;Database=codematic;user id=sa;

password=";

string strSQL = "SELECT * FROM P_Product";           

DataSet ds = new DataSet();           

SqlDataAdapter adapter = new SqlDataAdapter(strSQL, conString);

adapter.Fill(ds, "ds");

2.DataAdapter和SqlConnection、SqlCommand建立關聯

方式1:DataAdapter在構造參數時建立。

方式2:通過SelectCommand屬性建立。

SqlDataAdapter adapter = new SqlDataAdapter();

adapter.SelectCommand = new SqlCommand(strSQL, myConnection);

5.1.6  輸水管——DataReader

通過執行ExecuteReader方法可以返回一個DataReader 對象。DataReader以只進、只讀方式返回數據,從而提高應用程序的性能。這樣可以節省 DataSet 所使用的內存,並省去創建 DataSet 並填充其內容所需的處理。

 

趣味理解

DataReader 也是一種水管,和DataAdapter不同的是,DataReader不把水輸送到水庫里面,而是單向地直接把水送到需要水的用戶那里或田地里,所以要比在水庫中轉一下更快。

(1)遍歷DataReader結果集。

SqlDataReader dr = cmd.ExecuteReader();           

while (dr.Read())

{

    Response.Write(dr.GetInt32(0).ToString()+ ", "+ dr.GetString(1) + "<br>");

}

dr.Close();

(2)使用序數索引器。

SqlDataReader dr = cmd.ExecuteReader();

while (dr.Read())

{

    Response.Write(dr[0].ToString() + ", " + dr[1].ToString() + "<br>");

}

dr.Close();

(3)使用列名索引器。

SqlDataReader dr = cmd.ExecuteReader();

while (dr.Read())

{

    Response.Write(dr["ProductId"].ToString()+", "+dr["Name"].ToString());

}

dr.Close();

(4)使用類型訪問器。

public char GetChar(int i); 獲取指定列的單個字符串形式的值

public DateTime GetDateTime(int i); 獲取指定列的 DateTime 對象形式的值

public short GetInt16(int i); 獲取指定列的 16 位有符號整數形式的值

public string GetString(int i); 獲取指定列的字符串形式的值

(5)得到DataReader的列信息。

dr.FieldCount     獲取當前行中的列數

dr.GetFieldType(序號)   獲取是對象的數據類型的 Type

dr.GetDataTypeName(序號)  獲取源數據類型的名稱

dr.GetName(序號)     獲取指定列的名稱

dr.GetOrdinal(序號)   在給定列名稱的情況下獲取列序號

(6)得到數據表的信息。

DataTable dt=dr.GetSchemaTable();

(7)操作多個結果集。

SqlDataReader dr = cmd.ExecuteReader();    

do

{

    while (dr.Read())

    {

        Response.Write(dr.GetInt32(0).ToString()+", "+dr.GetString(1));

    }

}

while(myReader.NextResult());//使數據讀取器前進到下一個結果集

dr.Close();

下面是一些使用DataReader獲得最佳性能的技巧。

l         在使用帶參數的Command前,必須關閉DataReader。

l         完成讀數據之后一定要關閉DataReader。如果使用Connection只返回DataReader,那么關閉DataReader之后立刻關閉它。另外一個顯式關閉Connection的方法是將CommandBehavior.CloseConnection傳遞給ExecuteReader方法,以確保關閉DataReader時相應的連接也被關閉。如果從一個方法返回DataReader,而且不能控制DataReader的相關連接的關閉,則這樣做特別有用。

l         不能在層之間遠程訪問DataReader。DataReader是為已連接好的數據訪問而設計的。

l         當訪問列數據時,使用類型化訪問器,例如GetString、GetInt32等。這使你不用將GetValue返回的Object強制轉換成特定類型。

l         一個單一連接每次只能打開一個DataReader。如果想在相同的數據存儲區上同時打開兩個DataReader,則必須顯式創建兩個連接,每個DataReader一個。這是ADO.NET為池化連接的使用提供更多控制的一種方法。

l         在默認情況下,DataReader每次Read時都要將整行加載到內存。這允許在當前行內隨機訪問列。如果不需要這種隨機訪問,為了提高性能,則將CommandBehavior.SequentialAccess傳遞給ExecuteReader調用。這將DataReader的默認行為更改為僅在請求時將數據加載到內存。注意,CommandBehavior. SequentialAccess要求順序訪問返回的列。也就是說,一旦讀過返回的列,就不能再讀它的值了。

l         如果已經讀取了來自DataReader的數據,但仍然有大量掛起的未讀結果,則在關閉DataReader之前先要取消Command。因為取消Command可使服務器放棄這些結果,從而釋放服務器的資源。

5.1.7  隨用隨關,釋放資源

對於C#程序員來說,確保始終關閉Connection和DataReader對象的一個方便的方法就是使用using語句。using語句在離開自己的作用范圍時,會自動調用被“使用”的對象的Dispose。例如:

string connectionString = "data source=127.0.0.1;Database=codematic;

user id=sa;password=";

using (SqlConnection myConnection = new SqlConnection(connectionString))

{

    SqlCommand cmd = myConnection.CreateCommand();

    cmd.CommandText = "SELECT * FROM P_Product";

    myConnection.Open();

    using (SqlDataReader dr = cmd.ExecuteReader())

    {

        while (dr.Read())

        {

            Response.Write(dr.GetInt32(0).ToString()+","+dr.GetString(1)+"<br>");

        }

}

}

5.1.8  水庫管理——DataSet

DataSet是ADO.NET中最核心的成員之一,是各種基於.NET平台程序語言(如VB.NET、C#.NET、C++.NET)的數據庫應用程序開發最常接觸的類,這是因為DataSet在ADO.NET實現從數據庫中抽取數據的作用。數據抽取后,DataSet就是數據的存放地,它是各種數據源(SQL Server 、OLE DB等)的數據在計算機內存的緩存,所以有時說DataSet可以看成是一個數據容器(又稱數據集)。在客戶端通過對DataSet的數據集讀取、更新等操作,從而實現對數據源的同等操作。

DataSet的最大優點是離線(斷開)和連接。DataSet既可以以離線方式,也可以以實時連接方式來操作數據庫中的數據。這樣的好處是大大減少了服務器端數據庫的連接線程,從而大大地減少了服務器端的運行壓力。所以,在數據量不大的情況下,使用DataSet是最好的選擇。

DataSet的基本工作過程:應用程序一般並不直接對數據庫進行操作(直接在程序中調用存儲過程等除外),而是先完成和數據庫的連接,接着通過數據適配器(DataAdapter)把數據庫中的數據填入DataSet對象,然后客戶端再通過讀取DataSet來獲得需要的數據,同樣,在更新數據庫中的數據時,也是首先更新DataSet,然后再通過DataSet和數據適配器將更新的數據同步地解釋入數據庫中。

下面列出了DataSet的一些常用操作。

1.創建DataSet 對象

初始化DataSet類的新實例。

public DataSet();

用給定名稱初始化DataSet類的新實例。

public DataSet(string);

2.用DataAdapter填充DataSet

DataSet ds = new DataSet();

adapter.Fill(ds);

adapter.Fill(ds, "表名"); //用一個表去填充DataSet.

3.合並兩個DataSet

string conString="data source=127.0.0.1;database=codematic;user id=sa;

password=";           

SqlConnection myConnection = new SqlConnection(conString);

myConnection.Open();

string strSQL = "SELECT * FROM P_Product";

DataSet ds1 = new DataSet();           

SqlDataAdapter adapter1 = new SqlDataAdapter(strSQL, myConnection);

adapter1.Fill(ds1, "Product");


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM