使用 DataAdapter 更新資料來源
適用於:.NET Framework .NET .NET Standard
呼叫 Update
的 DataAdapter 方法,可將 DataSet 的變更解析回資料來源。 Update
方法類似 Fill
方法,會將 DataSet
的執行個體以及選擇性 (Optional) DataTable 物件或 DataTable
名稱做為引數。 DataSet
執行個體是包含已進行之變更的 DataSet
,而 DataTable
則識別要從中擷取變更的資料表。 如果沒有指定任何 DataTable
,就會使用 DataTable
中的第一個 DataSet
。
當您呼叫 Update
方法時,DataAdapter
會分析已進行的變更,並執行適當的命令 (INSERT、UPDATE 或 DELETE)。 當 DataAdapter
發現 DataRow 的變更時,它會使用 InsertCommand、UpdateCommand 或 DeleteCommand 來處理變更。
這些屬性可讓您在設計階段指定命令語法,並在可能的情況下使用預存程序,將 ADO.NET 應用程式的效能最大化。 您必須在呼叫 Update
之前明確地設定命令。 如果呼叫 Update
之後,特定更新沒有適當命令可用 (例如,刪除的資料列沒有 DeleteCommand
),便會擲回例外狀況 (Exception)。
重要
如果您搭配 DataAdapter
使用 SQL Server 的預存程序來編輯或刪除資料,請確定您沒有在預存程序定義中使用 SET NOCOUNT ON
。 因為這樣會讓傳回的受影響資料列計數成為零,而 DataAdapter
會將它解譯為並行衝突。 在此事件中,系統會擲回 DBConcurrencyException。
命令參數可用來為 中每個修改的數據列 DataSet
指定 SQL 語句的輸入和輸出值或預存程式。 如需詳細資訊,請參閱 DataAdapter 參數 (機器翻譯)。
注意
請務必瞭解刪除 DataTable 中的資料列與移除資料列之間的差異。 當您呼叫 Remove
或 RemoveAt
方法時,系統會立即移除資料列。 如果您接著將 DataTable
或 DataSet
傳遞至 DataAdapter
並呼叫 Update
,則後端資料來源中對應的資料列皆不會受到影響。 當您使用 Delete
方法時,資料列會保留在 DataTable
中並標示為待刪除。 如果您接著將 DataTable
或 DataSet
傳遞至 DataAdapter
並呼叫 Update
,則會刪除後端資料來源中對應的資料列。
如果您的 DataTable
對應至或產生自單一資料庫資料表,則可以利用 DbCommandBuilder 物件來自動產生 DeleteCommand
、InsertCommand
以及 UpdateCommand
的 DataAdapter
物件。 如需詳細資訊,請參閱使用 CommandBuilder 產生命令 (機器翻譯)。
使用 UpdatedRowSource 將值對應至 DataSet
您可以使用 SqlCommand 物件的 UpdatedRowSource 屬性,在呼叫 DataAdapter
的 Update 方法之後,控制從資料來源傳回的值要如何對應回 DataTable
。 您可以透過將 UpdatedRowSource
屬性設為其中一個 UpdateRowSource 列舉值,控制要忽略 DataAdapter
命令所傳回的輸出參數,還是要將這些參數套用至 DataSet
中已變更的資料列。 您還能夠指定是否要將第一個傳回的資料列 (如果存在) 套用至 DataTable
中已變更的資料列。
下列表格說明 UpdateRowSource
列舉型別的各種值,以及這些值如何影響與 DataAdapter
搭配使用之命令的行為。
UpdatedRowSource 列舉型別 | 描述 |
---|---|
Both | 輸出參數和傳回結果集的第一個資料列都會對應至 DataSet 中已變更的資料列。 |
FirstReturnedRecord | 只有傳回結果集之第一個資料列內的資料會對應至 DataSet 中已變更的資料列。 |
None | 將忽略任何輸出參數或傳回結果集的資料列。 |
OutputParameters | 只有輸出參數會對應至 DataSet 中已變更的資料列。 |
Update
方法會將您的變更解析回資料來源,但是自從您上次填入 DataSet
之後,其他用戶端可能也已經修改資料來源的資料。 若要以目前的資料重新整理 DataSet
,請使用 DataAdapter
和 Fill
方法。 如此新資料列會加入資料表,而更新資訊也會合併入現有資料列。
Fill
方法會藉由檢查 DataSet
中資料列的主索引鍵值以及 SelectCommand
所傳回的資料列,決定要加入新資料列或更新現有資料列。 如果 Fill
方法發現 DataSet
中之資料列的主索引鍵值符合 SelectCommand
所傳回結果中之資料列的主索引鍵值,它就會以 SelectCommand
所傳回之資料列中的資訊更新現有資料列,並將現有資料列的 RowState 設為 Unchanged
。 如果 SelectCommand
傳回之資料列的主索引鍵值與 DataSet
中資料列的任何主索引鍵值都不相符,Fill
方法就加入 RowState
設定為 Unchanged
的新資料列。
注意
如果 SelectCommand
傳回外部聯結的結果,則 DataAdapter
不會為產生的 DataTable
設定 PrimaryKey
值。 您必須自己定義 PrimaryKey
,確保正確解析重複的資料列。
若要處理呼叫 Update
方法時可能會發生的例外狀況,您可以使用 RowUpdated
事件在發生例外狀況時回應資料列更新錯誤 (請參閱處理 DataAdapter 事件 (英文)),也可以在呼叫 Update
之前將 ContinueUpdateOnError 設定為 true
,然後在更新完成時回應儲存於特定資料列 RowError
屬性中的錯誤資訊。
注意
在 DataSet
、DataTable
或 DataRow
上呼叫 AcceptChanges
會導致 DataRow
的所有 Original
值都以 DataRow
的 Current
值覆寫。 如果識別資料列為唯一的欄位值已經被修改,則在呼叫 AcceptChanges
之後,Original
值就不會再與資料來源內的值相符。 呼叫 DataAdapter
的 Update
方法時,會為每個資料列自動呼叫 AcceptChanges
。 您可以先將 AcceptChangesDuringUpdate
的 DataAdapter
屬性設定為 false,或針對 RowUpdated
事件建立事件處理常式並將 Status 設定為 SkipCurrentRow,藉以在呼叫 Update 方法期間保留原始值。 如需詳細資訊,請參閱處理 DataAdapter 事件 (英文)。
下列範例示範如何明確設定 DataAdapter
的 UpdateCommand
並呼叫其 Update
方法,藉以更新已修改的資料列。
注意
在 UPDATE statement
的 WHERE clause
中所指定的參數會設定為使用 SourceColumn
的 Original
值。 這一點相當重要,因為 Current
值可能已經修改,而不符合資料來源中的值。 Original
值是用來填入資料來源之 DataTable
的值。
private static void AdapterUpdate(string connectionString)
{
using (SqlConnection connection =
new SqlConnection(connectionString))
{
SqlDataAdapter dataAdpater = new SqlDataAdapter(
"SELECT CategoryID, CategoryName FROM Categories",
connection);
dataAdpater.UpdateCommand = new SqlCommand(
"UPDATE Categories SET CategoryName = @CategoryName " +
"WHERE CategoryID = @CategoryID", connection);
dataAdpater.UpdateCommand.Parameters.Add(
"@CategoryName", SqlDbType.NVarChar, 15, "CategoryName");
SqlParameter parameter = dataAdpater.UpdateCommand.Parameters.Add(
"@CategoryID", SqlDbType.Int);
parameter.SourceColumn = "CategoryID";
parameter.SourceVersion = DataRowVersion.Original;
DataTable categoryTable = new DataTable();
dataAdpater.Fill(categoryTable);
DataRow categoryRow = categoryTable.Rows[0];
categoryRow["CategoryName"] = "New Beverages";
dataAdpater.Update(categoryTable);
Console.WriteLine("Rows after update.");
foreach (DataRow row in categoryTable.Rows)
{
{
Console.WriteLine("{0}: {1}", row[0], row[1]);
}
}
}
}
自動遞增資料行
如果來自資料來源的資料表具有自動遞增的資料行,您就可以將這些資料行填入 DataSet
中,方法包括:將自動遞增值當成預存程序的輸出參數傳回並將它對應至資料表的資料行、傳回結果集 (由預存程序或 SQL 陳述式所傳回) 之第一個資料列中的自動遞增值,或使用 RowUpdated
的 DataAdapter
事件來執行其他 SELECT 陳述式。 如需詳細資訊與範例,請參閱擷取識別或自動編號值。
插入、更新與刪除的順序
在許多情況下,以何種順序將透過 DataSet
所進行的變更傳送給資料來源是很重要的。 例如,如果更新了現有資料列的主索引鍵值,也加入了以新主索引鍵值當做外部索引鍵的資料列,則先進行更新再執行插入是很重要的。
您可以使用 Select
的 DataTable
方法來傳回僅參考具有特定 DataRow
之資料列的 RowState
陣列。 接著,您可以將傳回的 DataRow
陣列傳遞給 Update
的 DataAdapter
方法來處理已修改的資料列。 您可指定要更新的資料列子集,藉以控制插入、更新和刪除的處理順序。
範例
例如,下列程式碼確保先處理資料表的已刪除資料列,接著處理已更新資料列,然後再處理已插入資料列。
// Assumes that dataSet and adapter are valid objects.
DataTable table = dataSet.Tables["Customers"];
// First process deletes.
adapter.Update(table.Select(null, null, DataViewRowState.Deleted));
// Next process updates.
adapter.Update(table.Select(null, null,
DataViewRowState.ModifiedCurrent));
// Finally, process inserts.
adapter.Update(table.Select(null, null, DataViewRowState.Added));
使用 DataAdapter 來擷取和更新資料
您可以使用 DataAdapter 擷取和更新資料。
此範例會使用
DataAdapter.AcceptChangesDuringFill
來複製資料庫中的資料。 如果將屬性設定為 false,則在填滿資料表時不會呼叫 AcceptChanges,而且新增的資料列會被視為插入的資料列。 因此,範例會使用這些資料列,將新資料列插入資料庫。這些範例會使用
DataAdapter.TableMappings
來定義來源資料表與 DataTable 之間的對應。此範例會使用
DataAdapter.FillLoadOption
來決定配接器如何在 DbDataReader 中填入 DataTable。 當您建立 DataTable 時,您只能透過將屬性設定為 LoadOption.Upsert 或 LoadOption.PreserveChanges,來將資料從資料庫寫入目前版本或原始版本。此範例也會使用
DbDataAdapter.UpdateBatchSize
來更新資料表,以執行批次作業。
在編譯和執行範例之前,您必須建立範例資料庫:
USE [master]
GO
CREATE DATABASE [MySchool]
GO
USE [MySchool]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Course]([CourseID] [nvarchar](10) NOT NULL,
[Year] [smallint] NOT NULL,
[Title] [nvarchar](100) NOT NULL,
[Credits] [int] NOT NULL,
[DepartmentID] [int] NOT NULL,
CONSTRAINT [PK_Course] PRIMARY KEY CLUSTERED
(
[CourseID] ASC,
[Year] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Department]([DepartmentID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Budget] [money] NOT NULL,
[StartDate] [datetime] NOT NULL,
[Administrator] [int] NULL,
CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED
(
[DepartmentID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]
GO
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C1045', 2012, N'Calculus', 4, 7)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C1061', 2012, N'Physics', 4, 1)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C2021', 2012, N'Composition', 3, 2)
INSERT [dbo].[Course] ([CourseID], [Year], [Title], [Credits], [DepartmentID]) VALUES (N'C2042', 2012, N'Literature', 4, 2)
SET IDENTITY_INSERT [dbo].[Department] ON
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (1, N'Engineering', 350000.0000, CAST(0x0000999C00000000 AS DateTime), 2)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (2, N'English', 120000.0000, CAST(0x0000999C00000000 AS DateTime), 6)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (4, N'Economics', 200000.0000, CAST(0x0000999C00000000 AS DateTime), 4)
INSERT [dbo].[Department] ([DepartmentID], [Name], [Budget], [StartDate], [Administrator]) VALUES (7, N'Mathematics', 250024.0000, CAST(0x0000999C00000000 AS DateTime), 3)
SET IDENTITY_INSERT [dbo].[Department] OFF
ALTER TABLE [dbo].[Course] WITH CHECK ADD CONSTRAINT [FK_Course_Department] FOREIGN KEY([DepartmentID])
REFERENCES [dbo].[Department] ([DepartmentID])
GO
ALTER TABLE [dbo].[Course] CHECK CONSTRAINT [FK_Course_Department]
GO
using System;
using System.Data;
using System.Data.Common;
using Microsoft.Data.SqlClient;
using System.Linq;
using CSDataAdapterOperations.Properties;
class Program
{
static void Main(string[] args)
{
Settings settings = new Settings();
// Copy the data from the database. Get the table Department and Course from the database.
String selectString = @"SELECT [DepartmentID],[Name],[Budget],[StartDate],[Administrator]
FROM [MySchool].[dbo].[Department];
SELECT [CourseID],@Year as [Year],Max([Title]) as [Title],
Max([Credits]) as [Credits],Max([DepartmentID]) as [DepartmentID]
FROM [MySchool].[dbo].[Course]
Group by [CourseID]";
DataSet mySchool = new DataSet();
SqlCommand selectCommand = new SqlCommand(selectString);
SqlParameter parameter = selectCommand.Parameters.Add("@Year", SqlDbType.SmallInt, 2);
parameter.Value = new Random(DateTime.Now.Millisecond).Next(9999);
// Use DataTableMapping to map the source tables and the destination tables.
DataTableMapping[] tableMappings = { new DataTableMapping("Table", "Department"), new DataTableMapping("Table1", "Course") };
CopyData(mySchool, settings.MySchoolConnectionString, selectCommand, tableMappings);
Console.WriteLine("The following tables are from the database.");
foreach (DataTable table in mySchool.Tables)
{
Console.WriteLine(table.TableName);
ShowDataTable(table);
}
// Roll back the changes
DataTable department = mySchool.Tables["Department"];
DataTable course = mySchool.Tables["Course"];
department.Rows[0]["Name"] = "New" + department.Rows[0][1];
course.Rows[0]["Title"] = "New" + course.Rows[0]["Title"];
course.Rows[0]["Credits"] = 10;
Console.WriteLine("After we changed the tables:");
foreach (DataTable table in mySchool.Tables)
{
Console.WriteLine(table.TableName);
ShowDataTable(table);
}
department.RejectChanges();
Console.WriteLine("After use the RejectChanges method in Department table to roll back the changes:");
ShowDataTable(department);
DataColumn[] primaryColumns = { course.Columns["CourseID"] };
DataColumn[] resetColumns = { course.Columns["Title"] };
ResetCourse(course, settings.MySchoolConnectionString, primaryColumns, resetColumns);
Console.WriteLine("After use the ResetCourse method in Course table to roll back the changes:");
ShowDataTable(course);
// Batch update the table.
String insertString = @"Insert into [MySchool].[dbo].[Course]([CourseID],[Year],[Title],
[Credits],[DepartmentID])
values (@CourseID,@Year,@Title,@Credits,@DepartmentID)";
SqlCommand insertCommand = new SqlCommand(insertString);
insertCommand.Parameters.Add("@CourseID", SqlDbType.NVarChar, 10, "CourseID");
insertCommand.Parameters.Add("@Year", SqlDbType.SmallInt, 2, "Year");
insertCommand.Parameters.Add("@Title", SqlDbType.NVarChar, 100, "Title");
insertCommand.Parameters.Add("@Credits", SqlDbType.Int, 4, "Credits");
insertCommand.Parameters.Add("@DepartmentID", SqlDbType.Int, 4, "DepartmentID");
const Int32 batchSize = 10;
BatchInsertUpdate(course, settings.MySchoolConnectionString, insertCommand, batchSize);
}
private static void CopyData(DataSet dataSet, String connectionString, SqlCommand selectCommand, DataTableMapping[] tableMappings)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
selectCommand.Connection = connection;
connection.Open();
using (SqlDataAdapter adapter = new SqlDataAdapter(selectCommand))
{
adapter.TableMappings.AddRange(tableMappings);
// If set the AcceptChangesDuringFill as the false, AcceptChanges will not be called on a
// DataRow after it is added to the DataTable during any of the Fill operations.
adapter.AcceptChangesDuringFill = false;
adapter.Fill(dataSet);
}
}
}
// Roll back only one column or several columns data of the Course table by call ResetDataTable method.
private static void ResetCourse(DataTable table, String connectionString,
DataColumn[] primaryColumns, DataColumn[] resetColumns)
{
table.PrimaryKey = primaryColumns;
// Build the query string
String primaryCols = String.Join(",", primaryColumns.Select(col => col.ColumnName));
String resetCols = String.Join(",", resetColumns.Select(col => $"Max({col.ColumnName}) as {col.ColumnName}"));
String selectString = $"Select {primaryCols},{resetCols} from Course Group by {primaryCols}";
SqlCommand selectCommand = new SqlCommand(selectString);
ResetDataTable(table, connectionString, selectCommand);
}
// RejectChanges will roll back all changes made to the table since it was loaded, or the last time AcceptChanges
// was called. When you copy from the database, you can lose all the data after calling RejectChanges
// The ResetDataTable method rolls back one or more columns of data.
private static void ResetDataTable(DataTable table, String connectionString,
SqlCommand selectCommand)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
selectCommand.Connection = connection;
connection.Open();
using (SqlDataAdapter adapter = new SqlDataAdapter(selectCommand))
{
// The incoming values for this row will be written to the current version of each
// column. The original version of each column's data will not be changed.
adapter.FillLoadOption = LoadOption.Upsert;
adapter.Fill(table);
}
}
}
private static void BatchInsertUpdate(DataTable table, String connectionString,
SqlCommand insertCommand, Int32 batchSize)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
insertCommand.Connection = connection;
// When setting UpdateBatchSize to a value other than 1, all the commands
// associated with the SqlDataAdapter have to have their UpdatedRowSource
// property set to None or OutputParameters. An exception is thrown otherwise.
insertCommand.UpdatedRowSource = UpdateRowSource.None;
connection.Open();
using (SqlDataAdapter adapter = new SqlDataAdapter())
{
adapter.InsertCommand = insertCommand;
// Gets or sets the number of rows that are processed in each round-trip to the server.
// Setting it to 1 disables batch updates, as rows are sent one at a time.
adapter.UpdateBatchSize = batchSize;
adapter.Update(table);
Console.WriteLine("Successfully to update the table.");
}
}
}
private static void ShowDataTable(DataTable table)
{
foreach (DataColumn col in table.Columns)
{
Console.Write("{0,-14}", col.ColumnName);
}
Console.WriteLine("{0,-14}", "RowState");
foreach (DataRow row in table.Rows)
{
foreach (DataColumn col in table.Columns)
{
if (col.DataType.Equals(typeof(DateTime)))
Console.Write("{0,-14:d}", row[col]);
else if (col.DataType.Equals(typeof(Decimal)))
Console.Write("{0,-14:C}", row[col]);
else
Console.Write("{0,-14}", row[col]);
}
Console.WriteLine("{0,-14}", row.RowState);
}
}
}
namespace CSDataAdapterOperations.Properties
{
internal sealed partial class Settings : System.Configuration.ApplicationSettingsBase
{
private static readonly Settings defaultInstance =
((Settings)(System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default => defaultInstance;
[System.Configuration.ApplicationScopedSetting()]
[System.Configuration.DefaultSettingValue("Data Source=(local);Initial Catalog=MySchool;Integrated Security=True")]
public string MySchoolConnectionString => ((string)(this["MySchoolConnectionString"]));
}
}