码迷,mamicode.com
首页 > 数据库 > 详细

.net数据库连接详解

时间:2018-01-05 19:09:40      阅读:228      评论:0      收藏:0      [点我收藏+]

标签:组件   传递   实现   uri   access   任务   上层   服务   方案   

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");

.net数据库连接详解

标签:组件   传递   实现   uri   access   任务   上层   服务   方案   

原文地址:https://www.cnblogs.com/RENQIWEI1995/p/8206168.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!