소프트웨어/C# & ASP.NET

ADO.NET 2.0의 DataSet 및 DataTable

falconer 2009. 8. 7. 17:05

ADO.NET 2.0은 ADO.NET 1.x의 핵심 클래스에 몇 가지 흥미로운 기능 향상을 추가하고 성능, 융통성 및 효율을 높이는 다양한 새 클래스를 도입했습니다. 또한 새 일괄 업데이트 프로세스가 개선된 것을 비롯하여 ADO.NET 2.0 사전 베타 버전과 베타 버전의 수명 주기 동안 몇 가지 중요한 부분이 변경되었습니다. 접근 속도가 빨라지고 기능 집합의 안정성이 향상된 ADO.NET 2.0 최종 릴리스에 대한 자세한 설명을 시작하겠습니다.

이번 달에는 DataSet 및 DataTable 클래스의 개선 사항을 검토하는 것으로 시작하여 이러한 기능 향상이 가지는 의미와 사용 가능한 상황에 대해 알아보겠습니다. ADO.NET 1.x에서는 특히 행 집합을 대량으로 작업할 때 성능 문제가 발생할 수 있습니다. 여기서는 인덱싱 엔진의 변화를 통해 ADO.NET 2.0에서 이러한 성능 문제를 어떻게 해결했는지 설명하겠습니다. 또한 DataTable 클래스에 추가된 기능을 비롯하여 새 Load 메서드를 통한 로딩 옵션 및 행의 상태를 변경하는 새 메서드를 살펴보겠습니다. 이 칼럼의 다음 기사에서는 일괄 업데이트 기능 및 이진 serialization을 사용하여 전송할 DataSet를 압축하는 기능 등과 같은 다른 개선 사항에 대해 설명할 예정입니다.


DataTable 기능 향상

ADO.NET 1.x에서는 DataSet이 모든 주목을 받은 탓에 DataTable의 기능이 빛을 잃었습니다. 하지만 그렇다고 해서 DataTable이 유용한 클래스가 아니었다는 말은 아닙니다. DataTable은 행과 열의 컨테이너이며 모든 연결이 끊어진 데이터의 중심지로 생각할 수 있습니다. 하지만 DataSet은 DataRelation 및 DataTable 개체를 포함할 수 있기 때문에 많은 주목을 받았습니다.

ADO.NET 1.x에서 DataTable은 유용하기는 하지만 DataSet에는 없는 몇 가지 제약을 가지고 있습니다. 예를 들어, DataSet은 한 DataSet 내에서 두 개의 DataTable 개체를 병합할 수 있는 Merge 메서드를 제공하지만 DataTable은 Merge 메서드를 제공하지 않습니다. 따라서 DataSet 내에 포함되어 있지 않은 DataTable을 가지고 있고 이를 다른 DataTable 개체와 병합하려는 경우, 그림 1에서처럼 먼저 DataSet 개체를 만들고 첫 번째 DataTable을 여기에 넣은 다음 DataSet.Merge 메서드를 호출해야 합니다.

이는 어렵지는 않지만 번거로운 일입니다. 하지만 이제는 ADO.NET 2.0의 DataTable 개체에 Merge 메서드가 있으므로 다음과 같이 두 DataTable 개체를 병합할 수 있습니다.

dtCust1.Merge(dtCust2);

ADO.NET 1.x의 또 다른 불편 사항 중 하나는 반드시 DataSet과 연결한 후에야 DataTable에서 기본적인 XML 작업을 수행할 수 있다는 점입니다. 예를 들어, DataTable을 XML로 작성하려는 경우 DataTable을 DataSet으로 로드하고 DataSet의 WriteXml 메서드를 사용해야 합니다. 하지만 ADO.NET 2.0에서는 DataTable에 WriteXml 메서드가 있어 그럴 필요가 없습니다. ADO.NET 2.0의 DataTable에서는 WriteXml 메서드는 물론 ReadXml, ReadXmlSchema 및 WriteXmlSchema 메서드도 제공합니다

DataSet에도 몇 가지 새 메서드와 속성이 추가되었습니다. 실제로 이제 DataSet과 DataTable은 모두 Load 및 CreateDataReader 메서드뿐 아니라 RemotingFormat 속성까지 제공합니다. RemotingFormat 속성은 DataTable 또는 DataSet을 이진 또는 XML 형식으로 serialize할지 여부를 나타내는 데 사용됩니다. Load 메서드는 다양한 방법을 통해 데이터를 DataTable 또는 DataSet으로 로드하는 데 사용할 수 있습니다. 이에 대해서는 잠시 후 다시 설명하겠습니다.


간단하고 빠른 반복

DataTable의 CreateDataReader 메서드(이전 베타 버전의 GetDataReader)는 ADO.NET 2.0 DataTableReader의 인스턴스를 만듭니다. DataTable.CreateDataReader를 사용하여 작성한 DataTableReader는 DataTable과 동일한 행과 열을 제공합니다. DataTableReader가 DataSet 또는 DataTable의 CreateDataReader 메서드를 통해 만들어지면 DataTableReader에는 삭제된 행을 제외하고 컨테이너 개체의 모든 행이 포함됩니다.

DataTableReader는 DataReader(SqlDataReader)와 달리 DataTable보다 간단한 개체일 뿐 아니라 연결이 끊어져 있습니다. 이러한 특징은 DataReader와 마찬가지로 빨리 반복할 수 있는 간단한 개체를 얻을 수 있는 동시에 DataReader와 달리 데이터 원본과 연결이 끊어져 있으므로 유용한 역할을 합니다. DataTableReader는 기본 테이블의 행에 대한 iterator로 간주할 수 있으며 테이블 내용의 foreach 열거형과 유사합니다. 하지만 열거 중에 컬렉션에 행이 추가되거나 삭제되면 예외가 발생하는 테이블 행 열거와는 달리, DataTableReader는 기본 테이블에 대한 변경 사항을 탄력적으로 해결하며 발생하는 수정 사항을 고려하여 스스로 올바른 위치를 찾게 됩니다.

다음 예제에서는 DataTableReader를 만들어 DataGridView에 바인딩하는 방법을 보여 줍니다.

using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);
DataTableReader dtRdr = ds.CreateDataReader();
dgvCustomers.DataSource = dtRdr;
}

DataTableReader는 DataReader처럼 앞으로만 이동할 수 있습니다. 또한 DataReader와 마찬가지로 첫 번째 행으로 이동하려면 DataTableReader의 Read 메서드를 사용합니다. DataTableReader가 여러 DataTable이 포함된 DataSet에서 만들어진 경우, DataTableReader에는 여러 개의 결과 집합이 포함됩니다(DataTable당 하나). 또한 DataReader와 유사하게 NextResult 메서드를 호출하면 DataTableReader를 사용하여 각 후속 결과 집합에 액세스할 수 있습니다.

그림 2 에서는 두 DataTable 개체가 포함된 DataSet에서 DataTableReader를 만드는 방법을 보여 줍니다. DataSet에 두 개의 DataTable이 있으므로 DataTableReader에도 두 개의 결과 집합이 포함됩니다. 그림 2에 서처럼 Read 및 NextResult 메서드를 사용하여 DataTableReader에서 두 결과 집합을 반복할 수 있습니다. 여기서 DataTableReader는 앞으로만 이동한다는 점에 유의합니다. 따라서 DataTableReader를 다시 반복하기 위해 DataTableReader에 두 번 액세스하려는 경우, 처음에 해당 레코드를 모두 읽은 후 다시 로드해야 합니다.

그림 2에 서는 DataSet에서 CreateDataReader 메서드를 사용하여 DataTableReader를 만듭니다. DataTable의 결과 집합이 DataTableReader에 추가되는 순서는 DataSet에 나타나는 순서와 같습니다. DataTable 결과 집합의 순서를 지정하려는 경우에는 오버로드된 다른 CreateDataReader 메서드를 사용할 수 있습니다.


데이터 로드

DataTableReader는 DataTable 또는 DataSet을 채우는 데에도 사용할 수 있습니다. 실제로 DataSet 또는 DataTable의 새 Load 메서드를 사용하면 DataTableReader 또는 IDataReader 인터페이스를 구현하는 판독기 클래스를 전달할 수 있습니다. 다음 예제에서는 스키마와 몇 개의 행이 포함된 dt1이라는 DataTable이 있는 것으로 가정합니다. 이 예제에서는 dt1이라는 DataTable에서 DataTableReader를 만든 다음 곧바로 동일한 데이터가 있는 두 번째 DataTable(dt2라고 함)을 로드합니다.

DataTableReader dtRdr = dt1.CreateDataReader();
DataTable dt2 = new DataTable();
dt2.Load(dtRdr);

ADO.NET 1.x에서는 DataAdapter의 Fill 메서드를 사용하여 DataSet 또는 DataTable을 채울 수 있었습니다. 또 다른 방법으로는 DataSet의 ReadXml 메서드를 사용하여 XML에서 DataSet을 채울 수도 있었습니다. 하지만 ADO.NET 2.0에는 Load 메서드가 도입되어 DataTableReader 또는 SqlDataReader처럼 IDataReader를 구현하는 클래스에서 DataSet 또는 DataTable을 로드할 수 있게 되었습니다. Load 메서드를 사용하여 여러 행을 로드하는 경우에는 먼저 BeginLoadData 메서드를 호출하여 알림, 인덱스 유지 관리 및 제약 조건 확인을 해제한 후 EndLoadData 메서드를 호출하여 다시 설정할 수 있습니다. ADO.NET 1.x에서도 사용할 수 있는 이들 메서드는 ADO.NET에서 행이 끝날 때마다 중지하고 인덱스를 다시 계산하거나 알림을 호출하거나 제약 조건을 확인할 필요가 없으므로 데이터를 보다 빨리 로드할 수 있습니다. 여러분은 단지 이러한 기능을 다시 설정하는 것만 기억하면 됩니다.


LoadOption 열거자

Load 및 Fill 메서드에는 LoadOptions 열거형 값 중 하나를 허용하는 오버로드가 있습니다. 이러한 설정 덕분에 DataSet 또는 DataTable에 Fill 또는 Load 작업을 수행하는 동안 기존 행에서 몇 개의 행을 교체해야 하는지를 나타내는 데 사용할 수 있는 강력한 기능을 얻게 되었습니다. 이 프로세스에서는 행을 교체하거나 추가하는 방법을 결정하는 데 기본 키를 사용하므로 기본 키가 설정되어 있는 것으로 가정합니다. 이 열거자 값은 현재 값 및/또는 원래 값을 들어오는 행의 값으로 덮어쓸지 여부를 결정하는 데 도움이 됩니다. 그림 3에서는 세 가지 옵션과 그에 대한 간략한 설명을 보여 줍니다.

이러한 각 옵션은 상황에 따라 적절하게 응용 프로그램에서 사용됩니다. OverwriteChanges 옵션은 데이터가 포함된 DataTable이 있지만 데이터베이스에 있을 수 있는 변경된 값을 가져오려는 경우 유용합니다. 이 옵션을 사용하면 DataTable의 원래 값과 현재 값을 모두 데이터베이스의 값으로 덮어쓰게 됩니다. 여기서 중요한 점은 OverwriteChanges를 사용하는 경우 새 행이 추가되는 과정에 첫 번째 DataTable에서 수정된(원래 버전 또는 현재 버전) 데이터를 덮어쓰게 된다는 것입니다.

DataSet 열에서는 원래 값과 현재 값을 저장합니다. PreserveChanges는 원래 값을 덮어쓰면서 현재 값은 그대로 유지합니다. Upsert는 이와 반대로 현재 값을 덮어쓰면서 원래 값은 그대로 유지합니다.

다음은 PreserveChanges가 유용한 경우의 예입니다. Peggy라는 사용자가 화면을 열고 DataSet에서 고객의 DataGrid를 로드했다고 가정하겠습니다. Peggy는 CustomerID가 ALFKI인 고객의 도시를 베를린에서 뉴욕으로 수정했지만 저장 단추를 클릭하지 않은 채 커피를 마시러 나갔습니다. 그러는 동안 Katherine은 같은 고객의 도시를 베를린에서 마이애미로 수정했습니다. 이제 Peggy가 휴식을 마치고 돌아와 레코드를 저장하면 데이터 동시성 문제가 발생합니다. 따라서 이 상황에서 Peggy의 고객 레코드에 대한 원래 값은 베를린이었는데 뉴욕으로 변경했으므로 현재 값은 뉴욕이 됩니다. 하지만 데이터베이스의 도시는 이제 마이애미입니다. Peggy의 DataSet에 대한 원래 값을 데이터베이스에 있는 값으로 다시 설정하려는 경우, 데이터베이스에서 DataTableReader로 데이터를 가져온 다음 LoadOptions.PreserveChanges를 사용하여 DataSet으로 로드할 수 있습니다. 그림 4에서는 이러한 실행 방법을 보여 줍니다.

이러한 설정의 사용 방법을 직접 확인하려면 디버거에서 이 코드를 단계별로 실행해 보십시오. Switch-case 문 바로 앞에 나타나는 선택 변수를 변경하면 다른 LoadOption 열거자를 시험해 볼 수 있습니다. 그림 4에 나와 있는 코드에서는 출력 창에 지정된 열의 원래 버전과 현재 버전만 표시하는 ShowVersions 메서드를 호출합니다.


RowState 변경

행의 상태는 DataAdapter의 Update 메서드가 호출되면 업데이트, 삽입 또는 삭제될 행을 결정하는 데 도움이 되는 주요 요소입니다. 또한 GetChanges 메서드에서는 가져올 행을 결정하기 위해 RowState를 검사합니다. DataSet의 값을 변경하면 ADO.NET에서는 Modified, Added 또는 Unchanged 같은 RowState 값 중 하나로 이를 간접적으로 설정하여 RowState 설정을 자동으로 처리합니다.

이러한 기능은 이따금 행의 상태를 직접 설정하는 데 매우 유용합니다. 이 경우 DataRow의 새 SetAdded 및 SetModified 메서드를 통해 작업을 좀 더 수월하게 수행할 수 있습니다. 예를 들어, ADO.NET을 사용하여 한 데이터베이스에서 다른 데이터베이스로 몇 개의 행을 복사해야 하는 상황을 가정해 보겠습니다. ADO.NET 2.0에서는 DataAdapter의 Fill 메서드를 사용하여 데이터베이스의 DataTable을 채우고 행의 RowState 설정을 Added로 변경한 다음, 이를 두 번째 DataAdapter를 사용하여 추가될 두 번째 데이터베이스(같은 스키마를 사용한다고 가정)로 보낼 수 있습니다. 이렇게 되면 각 행의 SetAdded 메서드를 호출하여 행의 RowState를 Unchanged에서 Added로 변경할 수 있습니다.

이러한 메서드의 작동 방식을 설명하기 위해 필자는 그림 5의 코드에 나와 있는 다른 예제를 포함시켰습니다. 이 코드에서는 고객의 행 집합을 검색한 다음 한 행의 RowState 설정을 Modified로 설정하고 다른 행의 설정은 Added로 설정하고 있습니다. 그런 다음 필자는 DataTable의 GetChanges 메서드를 사용하여 RowState가 Modified인 행이 포함된 DataTable을 만들고 행 수를 저장합니다. 그리고 추가된 행 수를 가져와 MessageBox를 사용하여 표시합니다.

DataRow의 SetAdded 및 SetModified 메서드는 변경되지 않은 행에 대해서만 실행됩니다. 이러한 특징이 유용한 또 다른 상황은 웹 서비스에서 DataSet 또는 DataTable을 수신하고 행이 모두 Unchanged로 표시되는 경우입니다. DataTable에 따라 데이터베이스에 추가 또는 업데이트 작업을 수행하려는 경우 이러한 새 메서드를 사용하여 RowState를 설정할 수 있습니다. 그렇지 않고 RowState를 Unchanged로 두는 경우에는 DataAdpater의 Udpate 메서드에서 행을 UpdateCommand 또는 InsertCommand로 보내지 않습니다.


0 ~ 60

ADO.NET 2.0에 새로 도입된 가장 유용한 기능은 새로운 메서드나 클래스가 아니라 성능이 집중적으로 향상된 부분에 있습니다. DataSet 및 DataTable의 큰 단점은 행의 수가 많은 경우(100, 1,000, 10,000 또는 그 이상) 로드 속도가 매우 느려진다는 점입니다. 규모가 큰 DataTable을 이동하려는 경우에는 상황이 아주 힘들어질 수 있습니다. 이러한 실질적인 제한 사항을 해결하기 위해 ADO.NET 2.0에서는 인덱싱 엔진의 속도를 높이는 데 많은 노력을 기울였습니다. ADO.NET에서는 인덱싱 엔진을 다시 작성하는 작업을 통해 DataTable의 로드 및 병합을 비롯한 모든 영역에서 성능이 대폭 개선되었습니다. 그림 6에 서는 Visual Studio .NET 2003(ADO.NET 1.1 사용) 또는 Visual Studio 2005(ADO.NET 2.0 사용)에서 실행할 수 있는 몇 가지 샘플 코드를 보여 줍니다. 필자는 행 수를 변경해 가며 두 환경 모두에서 이 코드를 비교해 보았습니다.

그림 6의 코드에서는 DataTable을 만들고 여기에 두 개의 열을 추가한 다음 반복할 때마다 DataTable에 행을 추가하여 1백만 번을 반복합니다. 반복이 완료되면 MessageBox 메서드를 통해 경과된 시간(초)이 사용자에게 표시됩니다. 필자는 이 테스트를 ADO.NET 1.1 및 ADO.NET 2.0 모두에서 다양한 반복에 대해 수행했습니다. 테스트 결과는 그림 7에 나와 있습니다.

속도는 DataTable에 제약 조건이 없는 경우가 훨씬 더 좋았습니다. 예를 들어, Unique 제약 조건을 제거하자 두 버전의 환경에서 단 1초만에 1백만 개의 행을 로드할 수 있었습니다. 물론 열을 두 개만 로드했다는 점과 SomeNumber 열 값이 단순히 순차적으로 감소하는 정수였다는 점을 무시할 수 없습니다. 여러분의 결과는 제 경우와 다르겠지만 중요한 부분은 ADO.NET 2.0의 인덱싱 엔진 속도가 매우 빨라졌다는 것입니다. 이제는 DataTable을 사용하여 백만 개의 행을 포함시키는 일이 완연한 현실입니다.


요약

ADO.NET 2.0에서는 많은 수의 행을 로드하는 것 같이 이전 버전에서 문제가 되었던 여러 부분의 기능이 향상되었습니다. 또한 여러 가지 새로운 기능이 추가되어 개발 작업이 더욱 용이해졌습니다. DataTable 클래스에는 DataSet 클래스에 이미 존재했던 여러 메서드가 추가되었으며 새 DataTableReader 클래스도 새롭게 도입되었습니다. Data Points 칼럼의 다음 기사에서는 이진 serialization을 통한 성능 향상 방법, 일괄 업데이트 활용 방법, 새 DataView 기능, 새 SqlConnectionBuilder 클래스 등을 살펴보며 ADO.NET 2.0에 대한 설명을 이어가겠습니다.


그림 1 Merging Two DataTable Objects in ADO.NET 1.x
string sqlAllCustomers = "SELECT * FROM Customers";
string cnStr =
@"Data Source=.;Initial Catalog=northwind;Integrated Security=True";

using (SqlConnection cn = new SqlConnection(cnStr))
{
cn.Open();
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCust1 = new DataTable("Customers");
adpt.Fill(dtCust1);
dtCust1.PrimaryKey = new DataColumn[]{dtCust1.Columns["CustomerID"]};

DataTable dtCust2 = dtCust1.Clone();
DataRow row1 = dtCust2.NewRow();
row1["CustomerID"] = "ALFKI";
row1["CompanyName"] = "Some Company";
dtCust2.Rows.Add(row1);

DataRow row2 = dtCust2.NewRow();
row2["CustomerID"] = "FOO";
row2["CompanyName"] = "Some Other Company";
dtCust2.Rows.Add(row2);

DataSet ds = new DataSet("MySillyDataSet");
ds.Tables.Add(dtCust1);
ds.Merge(dtCust2);

dgTest.DataSource = dtCust1;
}

그림 2 Looping Through a DataTableReader
using (SqlConnection cn = new SqlConnection(cnStr))
{
// Create the Command and Adapter
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);

// Create a DataTable and fill it
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);

DataSet ds = new DataSet();
ds.Tables.Add(dtCustomers);
adpt.SelectCommand = new SqlCommand("SELECT * FROM Orders", cn);
adpt.Fill(ds, "Orders");

// Create the DataTableReader (it is disconnected)
using(DataTableReader dtRdr = ds.CreateDataReader())
{
do
{
Console.WriteLine("******************************");
while (dtRdr.Read())
{
Console.WriteLine(dtRdr.GetValue(0).ToString());
}
}
while (dtRdr.NextResult());
}
}

그림 3 LoadOption Enumerator Settings

Setting Description
PreserveChanges This is the default setting. Keeps the current values. Overwrites the original values with the incoming rows.
Upsert Overwrites the current values with the incoming rows. Keeps the original values.
OverwriteChanges Overwrites the current values with the incoming rows. Overwrites the original values with the incoming rows.

그림 4 Testing LoadOptions
using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers);
dtCustomers.PrimaryKey = new DataColumn[] {
dtCustomers.Columns["CustomerID"] };

// Pause here to execute this SQL directly against the database:
// UPDATE Customers SET City = ''''''''Miami'''''''' WHERE CustomerID = ''''''''ALFKI''''''''
System.Diagnostics.Debugger.Break();

// Change the CURRENT values in the DataTable
DataRow row = dtCustomers.Rows.Find("ALFKI");
row["City"] = "Somewhere";
// ORIGINAL city == Berlin
// CURRENT city == New York
DisplayDataRowVersions(row, "Immediately after I change the City" +
" from Berlin to New York", "City");

// Load another DataTable with customer data.
DataTable dtCustomers2 = new DataTable("Customers");
adpt.Fill(dtCustomers2);
DataTableReader dtRdrCustomers = dtCustomers2.CreateDataReader();

LoadOption opt = LoadOption.PreserveChanges;
switch (opt)
{
case LoadOption.OverwriteChanges:
// Overwrite ORIGINAL and Overwrite CURRENT values
dtCustomers.Load(dtRdrCustomers, LoadOption.OverwriteChanges);
// ORIGINAL city == Miami
// CURRENT city == Miami
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.OverwriteChanges",
"City");
break;
case LoadOption.Upsert:
// Keep ORIGINAL and Overwrite CURRENT values
dtCustomers.Load(dtRdrCustomers, LoadOption.Upsert);
// ORIGINAL city == Berlin
/// CURRENT city == Miami
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.Upsert", "City");
break;
case LoadOption.PreserveChanges:
// Overwrite ORIGINAL and Keep CURRENT values
dtCustomers.Load(dtRdrCustomers, LoadOption.PreserveChanges);
// ORIGINAL city == Miami
// CURRENT city == New York
row = dtCustomers.Rows.Find("ALFKI");
ShowVersions (row,
"Immediately after LoadOptions.PreserveChanges", "City");
break;
}
}

그림 5 Changing a Row''''''''s State
DataTable dtCustomers = new DataTable("Customers");

using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
adpt.Fill(dtCustomers);
dtCustomers.PrimaryKey = new DataColumn[] {
dtCustomers.Columns["CustomerID"] };
}

// Change the RowState of a few rows
DataRow row = dtCustomers.Rows.Find("ALFKI");
row.SetModified();
row = dtCustomers.Rows.Find("BOLID");
row.SetModified();
row = dtCustomers.Rows.Find("ANTON");
row.SetAdded();

int modRows = dtCustomers.GetChanges(
DataRowState.Modified).Rows.Count;
int addRows = dtCustomers.GetChanges(DataRowState.Added).Rows.Count;

StringBuilder bldr = new StringBuilder();
bldr.Append(modRows.ToString());
bldr.Append(" row(s) were modified.");
bldr.Append(Environment.NewLine);
bldr.Append(addRows.ToString());
bldr.Append(" row(s) were added");
MessageBox.Show(bldr.ToString());

그림 6 Speed Test
DataTable dt = new DataTable("foo");
DataColumn pkCol = new DataColumn("ID", Type.GetType("System.Int32"));
pkCol.AutoIncrement = true;
pkCol.AutoIncrementSeed = 1;
pkCol.AutoIncrementStep = 1;
dt.Columns.Add(pkCol);
dt.PrimaryKey = new DataColumn[] { pkCol };
dt.Columns.Add("SomeNumber", Type.GetType("System.Int32"));
dt.Columns["SomeNumber"].Unique = true;

int limit = 1000000;
int someNumber = limit;
DateTime startTime = DateTime.Now;
for (int i = 1; i <= limit; i++)
{
DataRow row = dt.NewRow();
row["SomeNumber"] = someNumber—;
dt.Rows.Add(row);
}
TimeSpan elapsedTime = DateTime.Now - startTime;
MessageBox.Show(dt.Rows.Count.ToString() +
" rows loaded in " + elapsedTime.TotalSeconds + " seconds.");

그림 7 Speed Test Results

Iterations ADO.NET 1.1 ADO.NET 2.0
10,000 0.20 0.20
100,000 7.91 3.89
1,000,000 1831.01 23.78