6.8 DataTable类
我们知道,DataSet由表、关系和约束的集合组成。在ADO.NET中,DataTable对象用于表示DataSet中的表,一个内存内关系数据的表,表的架构或结构由列和约束表示。使用DataColumn对象以及ForeignKeyConstraint和UniqueConstraint对象定义DataTable的架构。表中的列可以映射到数据源中的列、包含从表达式计算所得的值、自动递增它们的值,或包含主键值。
除架构以外,DataTable还必须具有行,在其中包含数据并对数据排序。DataRow类表示表中包含的实际数据。DataRow及其属性和方法用于检索、计算和处理表中的数据。在访问和更改行中的数据时,DataRow对象会维护其当前状态和原始状态。
可以使用表中的一个或多个相关的列来创建表与表之间的父子关系。DataTable对象之间的关系可使用DataRelation来创建。然后,DataRelation对象可用于返回某特定行的相关子行或父行。
6.8.1 DataTable类概述
DataTable类是.NET Framework类库中System.Data命名空间的成员。你可以独立创建和使用DataTable,也可以作为DataSet的成员创建和使用,而且DataTable对象也可以与其他.NET Framework对象(包括DataView)一起使用。可以通过DataSet对象的Tables属性来访问DataSet中表的集合。它的常用属性与方法如表6-13与表6-14所示。
6.8.2 构建和操作DataTable
要使用DataTable,就得使用DataTable构造函数创建一个DataTable对象。如下面的代码所示:
DataTable myTable=new DataTable();
或者
DataTable myTable=new DataTable("Employee");
构造函数里的名称是区分大小写的,如“Employee”和“employee”是完全两个不同的DataTable表。创建DataTable时,一般不需要为TableName属性提供值,可以在其他时间指定该属性,或者将其保留为空。但是,在将一个没有TableName值的表添加到DataSet中时,该表会得到一个从“Table”(表示Table0)开始递增的默认名称TableN。
构造好DataTable对象之后,可以通过使用Add方法将其添加到DataTable对象的Tables集合中,将其添加到DataSet中。但在这里需要注意的是,将一个DataTable作为成员添加到一个DataSet的Tables集合中后,不能再将其添加到任何其他DataSet的表集合中。如下面的代码所示:
DataSet ds=new DataSet();
DataTable myTable=ds.Tables.Add("Employee");
或者
DataSet ds=new DataSet();
DataTable myTable=new DataTable("Employee")
ds.Tables.Add(Employee);
除此之外,也可以通过以下方法创建DataTable对象:
1)使用DataAdapter对象的Fill方法或FillSchema方法在DataSet中创建。
2)使用DataSet的ReadXml、ReadXmlSchema或InferXmlSchema方法从预定义的或推断的XML架构中创建。
1.在表中添加列与主键
初次创建DataTable时,它是没有架构(即结构)的。如果要定义表的架构,就必须创建DataColumn对象并将其添加到表的Columns集合中。DataTable包含了由表的Columns属性引用的DataColumn对象的集合。这个列的集合与任何约束一起定义表的架构(即结构)。
通过使用DataColumn构造函数,或者通过调用表的Columns属性的Add方法(它是一个DataColumnCollection),可在表内创建DataColumn对象。Add方法将接受可选的ColumnName、DataType和Expression参数,并将创建新的DataColumn作为集合的成员。它还会接受现有的DataColumn对象并会将其添加到集合中,并会根据请求返回对所添加的DataColumn的引用。由于DataTable对象不特定于任何数据源,所以在指定DataColumn的数据类型时会使用.NET Framework类型。
以下示例向DataTable中添加了四列:
DataTable myTable=new DataTable("Employee");
DataColumn col=myTable.Columns.Add("ID",typeof(Int32));
col.AllowDBNull=false;
col.Unique=true;
myTable.Columns.Add("Name",typeof(String));
myTable.Columns.Add("Email",typeof(String));
myTable.Columns.Add("Tel",typeof(String));
其中,示例中用于ID列的属性设置为不允许DBNull值,并将值约束为唯一。但是,如果你将ID列定义为表的主键列,AllowDBNull属性就会自动设置为false,并且Unique属性会自动设置为true。定义主键列的方法如下面的代码所示:
myTable.PrimaryKey=new DataColumn[]{myTable.Columns["ID"]};
值得注意的是,如果没有为一个列提供列名,则在将该列添加到DataColumnCollection时,该列会得到从“Column1”开始递增的默认名称ColumnN。所以,建议在提供列名时,避免使用“ColumnN”命名约定,因为那样提供的名称可能与DataColumnCollection中现有的默认列名冲突。如果提供的名称已经存在,将引发异常。
2.设置自增列
如果要确保列值的唯一性(如ID),可以将列值设置为在表中添加新行时自动递增。要创建自动递增的DataColumn,可将列的AutoIncrement属性设置为true。然后,DataColumn将从AutoIncrementSeed属性中定义的值开始,并且每添加一行,AutoIncrement列的值将按列的AutoIncrementStep属性中定义的值增加。同时,对于AutoIncrement列,建议将DataColumn的ReadOnly属性设置为true。如下面的示例演示了如何创建从值20开始并以1为增量递增的列:
DataColumn col=myTable.Columns.Add("ID",typeof(Int32));
col.AllowDBNull=false;
col.Unique=true;
col.AutoIncrement=true;
col.AutoIncrementSeed=20;
col.AutoIncrementStep=1;
col.ReadOnly=true;
3.设置计算表达式
有时候为了计算的需要,可以为列定义表达式,让它能够包含根据同一行中其他列值或根据表中多行的列值计算而得的值。要定义要计算的表达式,可使用目标列的Expression属性,并使用ColumnName属性在表达式中引用其他列。用于表达式列的DataType必须适合于表达式将返回的值。如下面的代码所示:
DataColumn salary=new DataColumn();
salary.DataType=System.Type.GetType("System.Double");
salary.ColumnName="Salary";
DataColumn salesTax=new DataColumn();
salesTax.DataType=System.Type.GetType("System.Double");
salesTax.ColumnName="SalesTax";
salesTax.Expression="Salary*0.15";
myTable.Columns.Add(salary);
myTable.Columns.Add(salesTax);
当然,也可以将该属性用做传递给DataColumn构造函数的第三个参数。如下面的代码所示:
myTable.Columns.Add("Salary",typeof(Double));
myTable.Columns.Add("SalesTax",typeof(Double),"Salary*0.15");
4.将约束添加到表
有时,为了维护数据的完整性,可以使用约束来对DataTable中的数据施加限制。约束是应用于某列或相关各列的自动规则,它决定了某行的值以某种方式更改时的操作过程。当DataSet的EnforceConstraints属性为true时,就可强制使用约束。
ADO. NET中支持两种约束,即ForeignKeyConstraint和UniqueConstraint。默认情况下,通过将DataRelation添加到DataSet来创建两个或多个表之间的关系时,两种约束都会自动创建。但是,也可以在创建关系时,通过指定createConstraints=false禁用这一行为。
(1)ForeignKeyConstraint
它强制使用有关如何对相关表所做更新和删除进行传播的规则。例如,如果更新或删除了一个表的某行中的值,并且一个或多个相关的表中也使用了同样的值,ForeignKeyConstraint会决定相关表中发生的操作。
ForeignKeyConstraint的DeleteRule和UpdateRule属性定义在用户试图删除或更新相关表中某行时采取的操作。表6-15描述可用于ForeignKeyConstraint的DeleteRule和UpdateRule属性的不同设置。
ForeignKeyConstraint可以限制并传播对相关列的更改。根据为列的ForeignKeyConstraint设置的属性,并且如果DataSet的Enforce-Constraints属性是true,对父行执行某些特定操作将会导致异常。例如,如果ForeignKeyConstraint的DeleteRule属性是None,那么在父行有子行的情况下,无法删除父行。
可以通过使用ForeignKeyConstraint构造函数创建单列之间或者一组列之间的外键约束。将生成的ForeignKeyConstraint对象传递给该表的Constraints属性的Add方法,该属性是一个ConstraintCollection。还可以将构造函数参数传递给ConstraintCollection的Add方法的几个重载,以创建ForeignKeyConstraint。在创建ForeignKeyConstraint时,可以将DeleteRule和UpdateRule值作为参数传递给构造函数,也可以作为属性进行设置。如下面的代码所示:
ForeignKeyConstraint fk=new ForeignKeyConstraint("CustFK",
ds.Tables["Employee"].Columns["EmployeeID"],
ds.Tables["Role"].Columns["EmployeeID"]);
fk.DeleteRule=Rule.None;
ds.Tables["Role"].Constraints.Add(fk);
这里使用的ForeignKeyConstraint构造函数原型如下:
public ForeignKeyConstraint(string constraintName,
DataColumn parentColumn, DataColumn childColumn);
(2)UniqueConstraint
该对象(可分配给DataTable中的单独一列或一组列)确保指定的某列或多个列中的所有数据对于每行都是唯一的。通过使用UniqueConstraint构造函数,可以为一列或一组列创建唯一的约束。将生成的UniqueConstraint对象传递给该表的Constraints属性的Add方法,该属性是一个ConstraintCollection。还可以将构造函数参数传递给ConstraintCollection的Add方法的几个重载,以创建UniqueConstraint。为一列或多列创建UniqueConstraint时,可以选择指定此列或这些列是不是主键。
还可以通过将列的Unique属性设置为true,为某列创建唯一约束。或者,通过将单列的Unique属性设置为false,可移除可能存在的任何唯一约束。如果将一列或多列定义为表的主键,会自动为一个或多个指定的列创建唯一的约束。如果从DataTable的PrimaryKey属性中移除一列,则UniqueConstraint也被移除。如下面的示例为DataTable的两列创建UniqueConstraint。
DataTable myTable=ds.Tables["Employee"];
UniqueConstraint emp=new UniqueConstraint(new DataColumn[]
{myTable.Columns["ID"],myTable.Columns["Name"]});
ds.Tables["Employee"].Constraints.Add(emp);
5.将数据行添加到表中
在为DataTable定义好架构之后,就可以通过将DataRow对象添加到表的Rows集合中来将数据行添加到表中。
如果要添加新行,可将一个新变量声明为DataRow类型。调用NewRow方法时,将返回新的DataRow对象。然后,DataTable会根据DataColumnCollection定义的表结构创建DataRow对象。下面的示例代码演示了如何通过调用NewRow方法来创建新行:
//构造一个DataRow
DataRow row=myTable.NewRow();
//将数据插入新行
row["Name"]="马伟";
row["Email"]="madengwei@hotmail.com";
row["Tel"]="13511111111";
//使用Add方法将行添加到DataRowCollection
myTable.Rows.Add(row);
也可以使用索引的方式来将数据插入新行,如下面的代码所示:
row[1]="马伟";
row[2]="madengwei@hotmail.com";
row[3]="13511111111";
除了上面的方法,还可以通过传入值的数组(类型化为Object),调用Add方法来添加新行,如下例所示:
myTable. Rows.Add(new Object[]{1,"马伟"});
这样,将类型化为Object的值的数组传递到Add方法,可在表内创建新行并将其列值设置为对象数组中的值,数组中的值会根据它们在表中出现的顺序相继与各列匹配。
6.行状态与行版本
ADO. NET用行状态和行版本管理表中的行。行状态指示行的状态;行版本在修改行中存储的值时维护各个阶段的值,包括当前值、原始值和默认值。例如,在修改了行中的某列后,该行的行状态将为Modified,并且有两个行版本:Current(包含行的当前值)和Original(包含列修改前行的值)。
每个DataRow对象都具有RowState属性,可以检查此属性来确定行的当前状态。表6-16给出了对每个RowState枚举值的简要说明。
在DataSet、DataTable或DataRow上调用AcceptChanges时,会移除行状态为Deleted的所有行。剩余的行会被赋予Unchanged行状态,并且Original行版本中的值会改写为Current行版本值。调用RejectChanges时,会移除行状态为Added的所有行。剩余的行会被赋予Unchanged行状态,并且Current行版本中的值会改写为Original行版本值。
通过用列引用来传递DataRowVersion参数,可以查看行的不同行版本,如下例所示:
DataRow row=myTable.Rows[0];
string drv=row["Name",DataRowVersion.Original].ToString()
表6-17给出了对每个DataRowVersion枚举值的简要说明。
通过调用HasVersion方法并将DataRowVersion作为参数传递,可以测试DataRow是否具有特定的行版本。例如,在调用AcceptChanges之前,DataRow.HasVersion(DataRow Version.Original)对新添加的行将返回false。
7.查看表中的数据
为DataTable添加好数据之后,就可以使用DataTable的Rows和Columns集合来访问DataTable中的内容,如下面的代码所示:
protected void Page_Load(object sender, EventArgs e)
{
DataTable myTable=new DataTable("Employee");
DataColumn col=myTable.Columns.Add("ID",typeof(Int32));
col.AllowDBNull=false;
col.Unique=true;
col.AutoIncrement=true;
col.AutoIncrementSeed=20;
col.AutoIncrementStep=1;
col.ReadOnly=true;
myTable.Columns.Add("Name",typeof(String));
myTable.Columns.Add("Email",typeof(String));
myTable.Columns.Add("Tel",typeof(String));
DataRow row=myTable.NewRow();
row["Name"]="马伟";
row["Email"]="madengwei@hotmail.com";
row["Tel"]="13511111111";
myTable.Rows.Add(row);
DataRow row1=myTable.NewRow();
row1["Name"]="马伟1";
row1["Email"]="madengwei@hotmail.com";
row1["Tel"]="13511111111";
myTable.Rows.Add(row1);
foreach(DataColumn column in myTable.Columns)
{
Label1.Text+=column.ColumnName
+"      ";
}
Label1.Text+="RowState<br/>";
foreach(DataRow rows in myTable.Rows)
{
foreach(DataColumn column in myTable.Columns)
{
Label1.Text+=rows[column].ToString()
+"  ";
}
Label1.Text+=rows.RowState.ToString()+"<br>";
}
}
运行结果如图6-31所示。
除此之外,还可以根据包括搜索条件、排序顺序和行状态在内的特定条件,使用Select方法返回DataTable中数据的子集。此外,用主键值搜索特定行时,还可使用DataRowCollection的Find方法。
DataTable对象的Select方法返回一组与指定条件匹配的DataRow对象。Select接受筛选表达式、排序表达式和DataViewRowState的可选参数。筛选表达式根据DataColumn值(例如LastName='Smith')标识要返回的行。排序表达式遵循用于为列排序的标准SQL约定,例如LastName ASC, FirstName ASC。
图 6-31 查看表中的数据运行示例
Select方法基于DataViewRowState确定要查看或处理的行的版本。表6-18说明了可能的DataViewRowState枚举值。
下面的示例代码演示了Select方法的使用环境:
DataRow[]currentRows=myTable.Select(null, null,
DataViewRowState.CurrentRows);
if(currentRows.Length<1)
Label1.Text+="没有Current Rows";
else
{
foreach(DataColumn column in myTable.Columns)
{
Label1.Text+=column.ColumnName
+"      ";
}
Label1.Text+="RowState<br/>";
foreach(DataRow rows in currentRows)
{
foreach(DataColumn column in myTable.Columns)
{
Label1.Text+=rows[column].ToString()
+"  ";
}
Label1.Text+=rows.RowState.ToString()+"<br>";
}
}
Select方法还可用于返回具有不同RowState值或字段值的行。下面的示例代码返回一个引用所有已删除行的DataRow数组,并返回另一个引用所有按Name排序的行(其中ID列大于20)的DataRow数组。
DataRow[]deletedRows=myTable.Select(null, null,
DataViewRowState.Deleted);
DataRow[]custRows=myTable.Select("ID>20","Name ASC");
8.编辑表中的数据
DataRow提供了三种可用于在编辑行时将行的状态挂起的方法,分别是BeginEdit、EndEdit和CancelEdit。
使用BeginEdit方法将DataRow置于编辑模式。在此模式中,事件被临时挂起,以便允许用户在不触发验证规则的情况下对多行进行多处更改。例如,如果需要确保总数列的值等于某行中借贷列的值,则可以将每一行都置入编辑模式,以便在用户尝试提交值之前挂起对行值的验证。
调用BeginEdit方法之后,可以通过调用EndEdit来确认编辑,也可以通过调用CancelEdit来取消编辑。示例如下面的代码所示:
DataRow row2=myTable.Rows[0];
row2.BeginEdit();
row2["Name"]="马伟2";
row2["Tel"]="13511111111";
row2.EndEdit();
值得注意的是,尽管EndEdit确实已确认你所做的编辑,但在调用AcceptChanges之前,表并没有实际接受更改。另外请注意,如果在使用EndEdit或CancelEdit结束编辑之前调用AcceptChanges,编辑将会结束,并接受Current和Original行版本的Proposed行值。同样,调用RejectChanges也会结束编辑,并放弃Current和Proposed行版本。在调用AcceptChanges或RejectChanges之后调用EndEdit或CancelEdit不会起作用,因为编辑已经结束。
9.从表中删除行
用于从DataTable对象中删除DataRow对象的方法有两种:DataRowCollection对象的Remove方法和DataRow对象的Delete方法。Remove方法从DataRowCollection中删除DataRow,而Delete方法只将行标记为删除。当应用程序调用AcceptChanges方法时,才会发生实际的删除。通过使用Delete,可以在实际删除之前先以编程方式检查哪些行标记为删除。如果将行标记为删除,其RowState属性会设置为Deleted。
在将DataSet或DataTable与DataAdapter和关系型数据源一起使用时,用DataRow的Delete方法移除行。Delete方法只是在DataSet或DataTable中将行标记为Deleted,而不会移除它。而DataAdapter在遇到标记为Deleted的行时,会执行其DeleteCommand方法以在数据源中删除该行。然后,就可以用AcceptChanges方法永久移除该行。如果使用Remove删除该行,则该行将从表中完全移除,但DataAdapter不会在数据源中删除该行。
DataRowCollection的Remove方法采用DataRow作为参数,并将其从集合中移除,如下例所示:
DataRow row2=myTable.Rows[0];
myTable.Rows.Remove(row2);
作为对比,以下示例演示了如何调用DataRow上的Delete方法来将其RowState改为Deleted:
DataRow row2=myTable.Rows[0];
row2.Delete();
如果将行标记为删除,并且调用DataTable对象的AcceptChanges方法,该行就会从DataTable中移除。相比之下,如果调用RejectChanges,行的RowState就会恢复到被标记为Deleted之前的状态。
6.8.3 使用DataAdapter填充DataTable
DataAdapter对象用做内存数据表((DtaSet)和数据源之间的桥接器以便于你更加方便地检索和保存数据。它通过映射Fill(填充DataSet或DataTable)和Update(为DataSet中每个已插入、已更新或已删除的行调用相应的Insert、Update或Delete语句)来提供这一桥接器。即在对DataTable的填充中,可以使用DataAdapter对象从数据源获取数据并装入DataTable对象中,也可以通过DataAdapter对象将DataTable对象中数据的修改写回到数据源。
下面的示例演示了如何使用SqlDataAdapter的Fill方法来填充一个DataTable对象:
public DataTable GetEmployee()
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
//创建一个DataAdapter对象
SqlDataAdapter adapter=new SqlDataAdapter(
"select*from Employee",connectionString);
//创建一个DataTable对象
DataTable dt=new DataTable();
//填充DataTable
adapter.Fill(dt);
return dt;
}
这里需要注意的是,上面的代码并未显式地创建SqlConnection对象。当调用SqlDataAdapter对象的Fill()方法时,SqlDataAdapter对象会自动创建并打开该数据库连接。从数据库中取得所需数据后,Fill()方法将自动地关闭数据库连接。因此,这里不需要使用try……catch语句来保护对Fill()方法的调用。因为在该方法内部,SqlDataAdapter对象已使用了try……catch语句来确保其连接会被关闭。
其实,打开和关闭数据库连接是一个很耗时的操作。如果在使用SqlDataAdapter对象后还需要执行其他的数据操作,那么可以像下面这样显式地创建SqlConnection对象并手动将其打开,如下面的代码所示:
public DataTable GetEmployee()
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
SqlConnection con=new SqlConnection(connectionString);
SqlDataAdapter adapter=new SqlDataAdapter(
"select*from Employee",con);
DataTable dt=new DataTable();
using(con)
{
con.Open();
adapter.Fill(dt);
//这里还可以做其他操作
}
return dt;
}
如上面的代码所示,如果在调用Fill()方法之前SqlConnection对象所表示的数据库连接已经被打开,那么Fill()方法在调用结束时就不会自动地关闭它。换句话说,Fill()方法会保持传入供其使用的数据库连接的状态。
除此之外,DataAdapter还具有四项用于从数据源检索数据和更新数据源中的数据的属性:
1)SelectCommand属性:用于从数据源中返回数据。
2)InsertCommand、UpdateCommand和DeleteCommand属性:用于管理数据源中的更改。
在调用DataAdapter的Fill方法之前,必须设置SelectCommand属性。根据对表中的数据作出的更改,在调用DataAdapter的Update方法之前,必须设置InsertCommand、UpdateCommand或DeleteCommand属性。例如,如果已添加行,在调用Update之前必须设置InsertCommand。当Update处理已插入、更新或删除的行时,DataAdapter将使用相应的Command属性来处理该操作。有关已修改行的当前信息将通过Parameters集合传递到Command对象。
下面的示例将使用SqlCommand、SqlDataAdapter和SqlConnection从数据库中选择记录,并用选定的行填充DataTable,然后返回已填充的DataTable。
public DataTable GetEmployee()
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
SqlConnection con=new SqlConnection(connectionString);
SqlDataAdapter adapter=new SqlDataAdapter();
DataTable dt=new DataTable();
SqlCommand cmd=new SqlCommand("select*from Employee
where employeename=@Name",con);
adapter.SelectCommand=cmd;
cmd.Parameters.Add("@Name",SqlDbType.VarChar,
100).Value="马伟";
using(con)
{
adapter.Fill(dt);
}
return dt;
}
其实,除了可以将SqlCommand对象分别赋值给SqlDataAdapter对象的4个属性之外,还可以使用另一个方法,即使用SqlCommandBuilder对象来创建所需的UpdateCommand、InsertCommand和DeleteCommand。SqlCommandBuilder类得到了带有select命令的SqlDataAdapter对象后,就会自动生成其他三个命令。
6.8.4 使用DataReader填充DataTable
除了可以使用DataAdapter来填充DataTable之外,也可以使用DataReader来填充DataTable。其方法很简单,只需要使用DataTable对象的Load()方法就可以了。Load()方法通过所提供的IDataReader,用某个数据源的值填充DataTable。如果DataTable已经包含行,则从数据源传入的数据将与现有的行合并。填充示例如下面的代码所示:
public DataTable GetEmployee()
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
DataTable dt=new DataTable();
using(SqlConnection connection=
new SqlConnection(connectionString))
{
SqlCommand command=new SqlCommand();
command.Connection=connection;
command.CommandText="select*from Employee";
connection.Open();
using(SqlDataReader reader=command.ExecuteReader
((CmmandBehavior.CloseConnection))
{
dt.Load(reader);
}
return dt;
}
}