David Sceppa Microsoft' ADO.NET Microsoft Press Дэвид Сеппа Microsoft ADO.NET ...
-- [ Страница 7 ] --" strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " & "FROM [Order Details] WHERE OrderlD = 10503 " & _ "ORDER BY ProductID" en = New OleDbConnection(strConn) da = New OleDbDataAdapterCstrSQL, en) cn.0pen() ResetOrderO da.Fill(tbl) DisplayOrder("Initial contents of database") ModifyOrderO DisplayOrder("Modified data in DataSet") SubmitChangesByHand() tbl.ClearO da.Fill(tbl) DisplayOrder("New contents of database") cn.Close() End Sub ГЛАВА 10 Передача обновлений в базу данных Private Sub ModifyOrderO Dim row As OataRow row = tbl.Rows(O) row.DeleteO row = tbl.Rows{1) row("Quantity") = CType{row("Quantity"), Int16) - row = tbl.Newflow row("OrderID") = row("ProductIO") = row("Quantity") = row("UnitPrice") = 18. tbl.Rows.Add(row) End Sub Public Sub DisplayOrderfByVal strStatus As String) Dim row As DataRow Dim col As DataColumn Console.WriteLine(strStatus) Console,WriteLine(" OrderlD ProductID "& "Quantity UnitPrice") For Each row In tbl.SelectC"", "ProductID") For Each col In tbl.Columns Console.Write(vbTab & row(col) & vbTab) Next Console.WriteLineC) Next Console.WriteLinef) End Sub Private Sub ResetOrderQ Dim strSQL As String Dim cmd As OleDbComrcand = cn.CreateCommand() StrSQL = "DELETE FROM [Order Details] WHERE OrderlD = 10503" cmd.CommandText = strSQL cmd. ExecuteNonQueryO StrSQL = "INSERT INTO [Order Details] " & _ (OrderlD, ProductID, Quantity, UnitPrice) " & _ VALUES (10503, 14, 70, 23.25) " cmd.CommandText = strSQL cmd. ExecuteNonQueryO strSQL = "INSERT INTO [Order Details] " & _ (OrderlD, ProductID, Quantity, UnitPrice) " & _ VALUES (10503, 65, 20, 21.05)" cmd.CommandText = strSQL cmd. ExecuteNonQueryO End Sub Автономная работа с данными: объект DataSei модели ADO.NET 360 Часть Public Function GenTableQ As DataTable Dim tbl As New DataTable("Order Details") Dim col As DataColumn With tbl.Columns col =.Addf'OrderlD", GetType(Integer)) col.AllowDBNull = False col =.AddC'ProductID", GetType(Integer)) col.AllowDBNull = False col =.AddC'Quantity", GetType(Int16}) col.AllowDBNull = False col =.AddC'UnitPrice", GetType{Decimal}> col.AllowDBNull = False End With tbl. PrimaryKey = New DataColumnO {tbl-Columns("OrderID"), tbl.ColumnsC'ProductlD")} Return tbl End Function Visual C#.NET static OleDbConnection en;
static OleDbDataAdapter da;
static DataTable tbl;
static void Main(string[] args) { string strConn, strSQL;
strConn = "Provider=SQLOlEDB;
Data Source=(local)\\NetSDK;
" + "Initial Catalog=Northwind;
Trusted_Connection=Yes;
";
strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER 8Y ProductID";
en = new OleDbConnection(strConn);
da = new 01eDbOataAdapter(strSQL, en);
tbl = GenTablef);
cn.OpenQ;
ResetOrderQ;
da.Fill(tbl);
DisplayOrder("Initial contents of database");
ModifyOrderO;
DisplayOrder("Modified contents of DataSet");
SubmitChangesByHand();
tbl.ClearO;
da.Fill(tbl);
DisplayOrderC'New contents of database");
cn.Close();
static void ModifyOrderO ГЛАВА 10 Передача обновлений в базу данных DataRow row;
row = tbl.Rows[0];
row.DeleteC);
row = tbl.Rows[1];
row["Quantity"] = (Int16) row["Quantity"] row = tbl.NewRowQ;
row["OrderID"] = 10503;
row["ProductID"] = 1;
rowf'Quantity"] = 24;
row["UnitPrice"] = 18.0;
tbl, Rows. Add(row);
static void DisplayQrder(string strStatus) { Console, WriteLine( strStatus);
Console. WriteLinef" OrderlD ProductID "+.Х "Quantity UnitPrice");
fo reach (DataRow row in tbl.Select("", "ProductID")) { foreacri(DataColurnn col in tbl. Columns) Console, Write("\t" + row[col] + "\t");
Console. WriteLineO;
} Console. WriteLineO;
static void ResetOrderC) < string strSQL;
OleDbCommand cmd = cn.CreateCommandO;
strSQL = "DELETE FROM [Order Details] WHERE OrderlO = 10503' cmd.CommandText = strSOL;
cmd. ExecuteNonQue ry( ) ;
strSOL = "INSERT INTO [Order Details] " + (OrderlD, ProductID, Quantity, UnitPrice) " + VALUES (10503, 14, 70, 23.25) " cnd.CommandText = strSQL;
cmd. ExecuteNonQue ry();
StrSQL = "INSERT INTO [Order Details] " + (OrderlD, ProductID, Quantity, UnitPrice) " + VALUES (10503, 65, 20, 21.05)";
cmd.CommandText = strSQL;
cmd. ExecuteNonQue ry();
static DataTable GenTableQ 1 3- Часть 111 Автономная работа с данными: объект DataSet модели ADO.NET DataTable tbl = new DataTable("Order Details");
DataColumn col;
col = tbl.Columns.Add("Order-ID", typeof(int));
col.AllowDBNull = false;
col = tbl.Columns.Add("ProductID", typeof(int));
col.AllowDBNull = false;
col = tbl.Columns.Add("Quantity", typeof(Int16));
col.AllowDBNull = false;
col = tbl.Columns.Add("UnitPrice", typeof(Decimal));
col.AllowDBNull = false;
tbl.PrimaryKey = new DataColumn[] {tbl.Columns["OrderID"], tbl.ColumnspProductlD"]};
return tbl;
!
Мы только что написали большой объем кода для передачи отложенных из менений. Код, на основе которого генерируются параметризованные объекты Command, уникален для исходного запроса. Тем не менее код процедуры Submit CbangesByHand универсален. Он просматривает каптированные изменения в объек те DataTable, определяет, как именно изменены объекты DataRow, вызывает фун кцию, выполняющую запрос для передачи отложенного изменения, и затем, в зависимости от возвращенного значения функции, соответствующим образом помечает объект DataRow.
По существу, мы воссоздали функциональность обновления, предоставляемую объектом DataAdapter. Подробнее о нем Ч ниже.
Передача обновлений с использованием объектов DataAdapter ADO.NET В главе 5 рассказывалось об использовании объекта DataAdapter для записи ре зультатов запросов в объекты DataTable. но это лишь половина функциональнос ти DataAdapter. Данный объект также предназначен для передачи отложенных изменений из объектов DataSet, Чтобы создать логику обновления, используемую объектами DataAdapter для передачи изменений в БД, можно:
Х вручную программно сконфигурировать объекты DataAdapter, Х воспользоваться в период выполнения объектом CommandBuilder.
Х воспользоваться в период разработки мастером Data Adapter Configuration Wizard.
У каждого из этих способов есть свои преимущества и недостатки, которые я подробно разберу далее.
Конфигурирование объектов DataAdapter вручную Объект DataAdapter предоставляет четыре свойства, содержащих объекты Command.
Как вы помните, свойство SelectCommand содержит объект Command, при помо щи которого DataAdapter заполняет ваш объект DataTable. Три остальных свой ГЛАВА 10 Передача обновлений в базу данных ства, UpdateCommand, InsertCommand и DeleteCommand, содержат объекты Com mand, при помощи которых DataAdapter передает отложенные изменения.
Такая архитектура сильно отличается от объектной модели ADO. Волшебная технология черный ящик* больше не используется. Вы управляете тем, как Data Adapter передает отложенные изменения, поскольку предоставляете используемые им объекты Command.
Метод Update объекта DataAdapter очень гибок и принимает объект DataSet, DataSet и имя таблицы, объект Data-Table или массив объектов DataRou*. Незави симо от того, как вызван метод DataAdaplerUpdate, DataAdapter попытается пере дать отложенные изменения при помощи соответствующего объекта Command.
Всю работу, выполнявшуюся нами ранее с помощью процедуры SubmitCbangesBy Hand, удается выполнить посредством одного вызова метода DataAdapter.Update.
Связанные параметры Созданная нами процедура SubmtiCbangesByHana'не особенно сложна. Кроме того, она выполняет не слишком много работы. Вместо этого процедура делегирует ее одной из трех функций: SubmitUpdate, Submitlnsert или SubmilDelele. Эти функции на основании содержимого измененной записи подставляют в соответствующий запрос значения параметров.
Для передачи отложенных изменений с использованием DataAdapter приме няются такие же параметризованные запросы.
UPDATE [Order Details] SET OrderlD = ?, ProductID = ?, Quantity = ?, UnitPrice = ?' WHERE OrderlD = ? AND ProductID = ? AND Quantity = ? AND UnitPrice = ?
INSERT INTO [Order Details] (OrderlD, ProductID, Quantity, UnitPrice) VALUES (?, ?, ?, ?) DELETE FROM [Order Details] WHERE OrderlD = ? AND ProductID = ? AND Quantity = ? AND UnitPrice = ?
Тем не менее при добавлении параметров в объекты Command, хранящиеся в свойствах объекта DataAdapter, мы используем два свойства объекта Parameter ADO.NET. предназначенные специально для обновлений на основе DataAdapter:
SourceColumn и SourceVersion.
По сути, эти свойства связывают объект Parameter с объектом DataColumn из состава Data-Table. Перед выполнением запроса DataAdapter на основе данных свойств определяет, какое значение задать свойству Value объекта Parameter, ана логично тому, как это осуществлялось в функциях SubmitUpdate, Submitlnsert и SubmitDelete. Подробнее Ч на рис. 10-2.
Следующий фрагмент кода не только создает параметризованные объекты Command, но и задает значения свойств SourceColumn it SourceVersion объектов Parameter. Значение свойства по умолчанию SourceVersion Ч DataRouVersion.Current, и задавать его следует, только если объект Parameter требуется связать с ориги нальными значениями нужного столбца.
Часть III Автономная работа с данными: объект DataSet модели ADO.NET Quantity ProductiD LJnitPrice OrderlD DstaRow 10503 65 21. (текущая версия) UPDATE [Order Details] \ \ ^я SET OrcterlD = ?, ProductiD = ?, Quantity = ?, UnitPrice = ?
WHERE OrderlD = ? AND ProductiD = ? AND Quantity = ? AND UnitPrice = ?
DataRow 10503 65 40 21. (оригинальная версия) QrderiD ProductlD Quantity UnitPrice Рис. 10-2. Связывание объектов Parameter с объектами DataColumn Visual Basic.NET Private Function CreateDataAdapterUpdateCommandO As OleDbCommand Dim strSQL As String strSQL = "UPDATE [Order Details] " & _ SET OrderlD = ?, ProductiD = ?, " & _ Quantity = ?, UnitPrice = ? " & _ WHERE OrderlD = ? AND ProductiD = ? AND " & _ Quantity = ? AND UnitPrice = ?" Dim cmd As New QleDbCommandCstrSQL, en) Dim pc As OleDbParameterCollection = cmd.Parameters pc.Add("OrderID_New", OleDbType.Integer, 0, "OrderlD") pc.Add("ProductIDJIew", OleDbType.Integer, 0, "ProductiD"} pc.Add("Quantity_New", OleDbType.Smalllnt, 0, "Quantity") pc.Add("UnitPrice_New", OleDbType.Currency, 0, "UnitPrice") Dim param As OleDbParameter param = pc.Add("OrderID_Orig", OleDbType.Integer, 0, "OrderlD") param.SourceVersion = DataRowVersion.Original param = pc.Add("ProductIDJ3rig", OleDbType.Integer, 0, "ProductiD").
param.SourceVersion = DataRowVersion.Original param = pc.Add("Quantity_Orig", OleDbType.Smalllnt, 0, "Quantity") param.SourceVersion = DataRowVersion.Original param = pc.Add("UnitPrice_Orig", OleDbType.Currency, 0, "UnitPrice") param.SourceVersion = DataRowVersion.Original Return cmd End Function Private Function CreateDataAdapterlnsertCommandO As OleDbCommand ГЛАВА 10 Передача обновлений в базу данных Dim strSQL As String strSQL = "INSERT INTO [Order Details] " & _ (OrderlD, ProductID, Quantity, UnitPrice) " & VALUES (?, ?, ?, ?)" Dim cmd As New OleDbCommandCstrSQL, on) Dim pc As OleDbParameterCollection = cmd.Parameters pc.Add("OrderlD", OleDbType.Integer, 0, "OrderlD") pc.Add("ProductID", OleDbType.Integer, 0, "ProductID") pc.Add("Quantity", OleDbType.Smalllnt, 0, "Quantity") pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrice") Return cmd End Function Private Function CreateDataAdapterDeleteCommand() As OleDbCommand Dim strSQL As String StrSQL = "DELETE FROM [Order Details] " & _ WHERE OrderlD = ? AND ProductID = ? AND " & _ Quantity = ? AND UnitPrice = ?" Dim cmd As New 01eDbCommand(strSQL, en) Dim pc As OleDbParameterCollection = cmd.Parameters Dim param As QleDbParatneter pc.Add("OrderID", OleDbType.Integer, 0, "OrderlD") param.SourceVersion = DataRowVersion.Original pc.AddC'ProductID", OleDbType.Integer, 0, "ProductID") param.SourceVersion = DataRowVersion.Original pc.Add("Quantity", OleDbType.Smalllnt, 0, "Quantity") param.SourceVersion = DataRowVersion.Original pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrice") pa ram.SourceVersion = DataRowVersion.Original Return cmd End Function Visual C#.NET static OleDbCommand CreateDataAdapterUpdateCommandO { string strSQL;
StrSQL = "UPDATE [Order Details] " & _ SET OrderlD = ?, ProductID = ?, " + Quantity = ?, UnitPrice = ? " + WHERE OrderlD = ? AMD ProductIO = ? AND " + Quantity = ? AND UnitPrice = ?";
OleDbCommand cmd = new 01eDbCommand(strSQL, en);
OleDbParameterCollection pc = cmd.Parameters;
pc.Add("OrderID_New", OleDbType.Integer, 0, "OrderlD");
366 Часть 111 Автономная работа с данными: объект DataSet модели ADO. NET pc.Add("ProductID_New", OleDbType. Integer, 0, "ProductID");
pc.Add("Quantity_New", OleDbType. Smalllnt, 0, "Quantity");
pc. Add("UnltPrice_New", OleDbType. Currency, 0, "Unit Price");
OleDbParameter param;
param = pc.Add("Order!D_Orig", OleDbType. Integer, 0, "OrderlD");
param. SourceVersion = DataRowversion. Original;
param = pc.Add("ProductID_Orig", OleDbType. Integer, 0, "ProductID");
param. SourceVersion = DataRowversion. Original;
param = pc.Add("Ouantity_Orig", OleDbType. Smalllnt, 0, "Quantity");
param. SourceVersion = DataRowversion. Original;
param = pc.Add("UnitPrice_Orig", OleDbType. Currency, 0, Х'UnitPrice");
param. SourceVersion = DataRowversion. Original;
return cmd;
static OleDbCommand CreateDataAdapterInsertCommand( ) i string strSQL;
strSQL = "INSERT INTO [Order Details] " + (OrderlD, ProductID, Quantity, UnitPrice) " + VALUES {?, ?, ?, ?)";
OleDbCommand cmd = new OleDbComniancKstrSQL, en);
OleDbParameterCollection pc = cmd. Parameters;
pc,Add("OrderID", OleDbType. Integer, 0, "OrderlD");
pc.Add("ProductID", OleDbType. Integer, 0, "ProductID");
pc.Add( "Quantity". OleDbType. Smalllnt, 0, "Quantity");
pc.Add( "Unit Price", OleDbType. Currency, 0, "UnitPrice");
return cmd;
i static OleDbCommand CreateOataAdapterDeleteCommandO { string strSQL;
strSQL = "DELETE FROM [Order Details] " + WHERE OrderlD = ? AND ProductID = ? AND " + Quantity = ? AND UnitPrice = ?";
OleDbCommand cmd = new 01eDbCommand(strSQL, en);
OleDbParameter param;
OleDbParameterCollection pc = cmd. Parameters;
param = pc.Add( "OrderlD", OleDbType. Integer, 0, "OrderlD");
param. SourceVersion = DataRowversion. Original;
param = pc.AddC'ProductID", OleDbType. Integer, 0, "ProductID");
ГЛАВА 10 Передача обновлений в базу данных param.SourceVersion = DataRowVersion. Original;
param = pc.Add("Quantity", QleDbType.Smalllnt, 0, "Quantity");
param.SourceVersion = DataRowVersion. Original;
param = pc.AddC'UnitPrtce", OleDbType. Currency, 0, "UnitPrice");
param.SourceVersion = DataRowVersion. Original;
return cmd;
} Теперь процедуры SubmttChangesByHand, SubmitUpdate, Siibmittnsert и SubmitDt-lete можно заменить следующим кодом;
Visual Basic.NET Private Sub SubmitChangesViaDataAdapter() da.UpdateCommand = CreateDataAdapterUpdateCommandQ da. InsertCommand = CreateDataAdapterlnsertCommandO da.DeleteCommand = CreateDataAdapterDeleteCommandO da.Update(tbl) End Sub Visual C#.NET static void SubmitChangesViaDataAdapterO { da.UpdateCommand = CreateDataAdapterUpdateCommandO;
da. InsertCommand = CreateDataAdapterlnsertCommandO;
da.DeleteCommand = CreateDataAdapterDeleteCommandO;
da.Update(tbl);
Передача обновлений с использованием хранимых процедур Одна из наиболее частых жалоб разработчиков, получавших данные из БД сред ствами ADO, Ч невозможность использования метода Recordset.UpdateBatch для передачи обновлений при помощи хранимых процедур.
Как уже говорилось, DataAdapter позволяет вам определить собственную ло гику обновления. В приведенных ранее фрагментах кода показано, как создать собственные объекты Command, с помощью которых DataAdapter будет переда вать отложенные изменения в БД. Аналогичный код годится и для передачи об новлений средствами хранимых процедур.
Во-первых, в БД Northwind следует определить хранимые процедуры, позво ляющие изменять, вставлять и удалять записи таблицы Order Details. Для созда ния процедур, которые будут вызываться в нашем коде, скопируйте и выполните следующий фрагмент кода в SQL Server Query Analyzer. Если у вас установлено только ядро MSDE и, как следствие, нет доступа к SQL Server Query Analyzer, вызовите процедуру CreateSprocs (приводится в одном из последующих фрагментов кода) и создайте нужные хранимые процедуры.
Автономная работа с данными: объект DataSet модели ADO.NET 368 Часть USE Northwind GO CREATE PROCEDURE spUpdateDetail (@OrderID_New int, @ProductID_New int, @Quantity_New smallint, @UnitPrice_New money, @OrderID_Orig int, @ProductID_Orig int, @Quantity_Orig smallint, @UnitPrice_Qrig money) AS UPDATE [Order Details] SET OrderlD = @OrderlD_New, ProductID = @ProductID_New, Quantity = @Quantity_New, UnitPrice = @UnitPrice_New WHERE OrderlD = @OrderID_Orlg AND ProductID = @ProductID_Orig AND Quantity = @Quantity_Orig AND UnitPrice = 3UnitPrice_Orig GO CREATE PROCEDURE spInsertDetail (@OrderID int, gProductlD int, ^Quantity smallint, ^UnitPrice money) AS INSERT INTO [Order Details] (OrderlD, ProductID, Quantity, UnitPrice) VALUES (@OrderID, 0ProductID, ^Quantity, @UnitPrice) GO CREATE PROCEDURE spDeleteDetail (@OrderID int, ^ProductID int, йQuantity smallint, йUnitPrice money) AS DELETE FROM [Order Details] WHERE OrderlD = @OrderID AND ProductID = зProductID AND Quantity = ^Quantity AND UnitPrice = йUnitPrice Имея хранимые процедуры для передачи обновлений в таблицу Order Details, можно написать объекты Command, автоматически вызывающие их при вызове метода DataAdapter.Update.
Следующий фрагмент кода включает функции, которые создают объекты Com mand, содержащие вызовы приведенных выше процедур. Кроме того, он включа ет процедуру для создания всех этих хранимых процедур в БД. Все, что остается сделать для передачи обновлений с помощью хранимых процедур Ч связать наши новые объекты Commands объектом DataAdapter. Это осуществляется в процеду ре SubmitCbangesViaStoredProcedures.
Visual Basic.NET Private Sub SubmitChangesViaStoredProceduresO da.UpdateCommand = CreateUpdateViaSPComraandO da.InsertCommand = CreatelnsertViaSPCommandO ГЛАВА 10 Передача обновлений в базу данных da.DeleteCommand = CreateDeleteViaSPCommandQ da.Update(tbl) End Sub Private Function CreateUpdateViaSPCommandO As OleDbCommand Dim cmd As New 01eDbCommand("spUpdateDetail", en) cmd.CommandType = CommandType.StoredProcedure Dim pc As OleDbParameterCollection = cmd.Parameters pc.Add("OrderlD_New", OleDbType.Integer, 0, "OrderlD") pc.Add("ProductID_New", OleDbType,Integer, 0, "ProductID") pc.Add("Quantity_New", OleDbType.Smalllnt, 0, "Quantity") pc.Add("UnitPrice_New", OleDbType.Currency, 0, "UnitPrice") Dim param As OleDbParameter param = pc.Add("OrderID_Orig", OleDbType.Integer, 0, "OrderlD") param.SourceVersion = DataRowVersion.Original param = pc.Add("ProductID_Orig", OleDbType,Integer, 0, "ProductID") param.SourceVersion = DataRowVersion.Original param = pc.Add("Quantity_Orig". OleDbType.Smalllnt, 0, "Quantity") pa ram.SourceVersion = DataRowVersion.Original param = pc.Add("UnitPrtce_Orig", OleDbType.Currency, 0.
"UnitPrice") param.SourceVersion = DataRowVersion.Original Return cmd End Function Private Function CreatelnsertViaSPCommandO As OleDbCommand Dim cmd As New OleDbCommandC'spInsertDetail", en) cmd.CommandType = CommandType.StoredProcedure Dim pc As OleDbParameterCollection = cmd.Parameters pc.AddC"OrderlD", OleDbType.Integer, 0, "OrderlD") pc.Add("ProductID", OleDbType.Integer, 0, "ProductID") pc.Addt"Quantity", OleDbType.Smalllnt, 0, "Quantity") pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrice") Return cmd End Function Private Function CreateDeleteViaSPCommandO As OleDbCommand Dim cmd As New 01eDbCommand{"spDeleteDetail", on) cmd.CommandType = CommandType.StoredProcedure Dim pc As OleDbParameterCollection = cmd.Parameters Dim param As OleDbParameter param = pc.Add("OrderID", OleDbType.Integer, 0, "OrderlD") Автономная работа с данными: объект DataSet модели ADO.NET 370 Часть III param.SourceVersion = DataRowVersion.Original param = pc.Add("ProductID", OleDbType.Integer, 0, "ProductlD") param.SourceVersion = DataRowVersion.Original param = pc.AddC"Quantity", OleDbType,Smalllnt, 0, "Quantity") param.SourceVersion = DataRowVersion.Original param = pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrice") param.SourceVersion = DataRowVersion.Original Return cmd End Function Private Sub CreateSprocsO Dim cmd As OleDbCommand = cn.CreateComrnand Dim strSQL As String strSQL = "CREATE PROCEDURE spUpdateDetail " & vbCrLf & _ (@OrderID_New int, @ProductID_New int, " & vbCrLf & _ @Quantity_New smallint, " & vbCrLf & @UnitPrice_New money, " & vbCrLf & @OrderID_Orig int, " & vbCrLf & _ @ProductID_Orig int, " & vbCrLf & @Quantity_Orig smallint, " & vbCrLf & @UnitPrice_Orig money) " & vbCrLf & "AS " & vbCrLf & _ "UPDATE [Order Details] " & vbCrLf & _ SET OrderlD = @OrderID_New, " & vbCrLf & _ ProductlD = @ProductID_New, " & vbCrLf & _ Quantity = @Quantity_New, " & vbCrLf & _ UnitPrice = @UnitPrice_New " & vbCrLf & WHERE OrderlD = @OrderID_Qrig AND " & vbCrLf &_ ProductlD = @ProductID_Orig AND " & vbCrLf & Quantity = @Quantity_Orig AND " & vbCrLf UnitPrice = @UnitPrice_Orig" cmd.ConnnandText = strSQL cmd. ExecuteNonQueryO strSQL = "CREATE PROCEDURE spInsertDetail " & vbCrLf & _ (@OrderID int, @ProductID int, " & vbCrLf & _ ^Quantity smallint, ^UnitPrice money) " & vbCrLf & "AS " & vbCrLf & _ "INSERT INTO [Order Details] " & vbCrLf & _ (OrderlD, ProductlD, Quantity, UnitPrice} " & vbCrLf & _ VALUES (@OrderID, @ProductID, ^Quantity, @UnitPrice)" cmd.CommandText := strSQL end. ExecuteNonQueryO strSQL = "CREATE PROCEDURE spDeleteDetail " & vbCrLf & _ (йOrderlD int, йProductld int, " & vbCrLf & ^Quantity smallint, @UnitPrice money) " & vbCrLf & "AS " & voCrLf & Передача обновлений в базу данных ГЛАВА "DELETE FROM [Order Details] " & vbCrLf & _ WHERE OrderlD = @OrderID AND " & vbCrLf & _ ProductID = @ProductID AND " & vbCrLf & Quantity = ^Quantity AND UnitPrice = @UnitPrice" cmd.CommandText = strSQL cmd. ExecuteNonQueryC ) End Sub Visual C#.NET static void SubmitChangesViaStoredProcedu res( ) { da.UpdateCommand = CreateUpdateViaSPCommandO;
da.InsertCommand = CreatelnsertViaSPCommandO;
da.DeleteComrnand = CreateDeleteViaSPCommandO;
da.Update(tbl);
static OleDbCommand CreateUpdateViaSPCommandO { OleDbCommand cmd = new OleDbCommandC'spUpdateDetail", en);
cmd. CommandType = CommandType. StoredProcedure;
OleDbParameterColLection pc = cmd. Parameters;
pc.Add("OrderID_New", OleObType. Integer, 0, "OrderlD");
pc.Add( "Product ID_New", OleDbType. Integer, 0, "ProductID");
pc.Add("Guantity_New", OleDbType. Smalllnt, 0, "Quantity");
pc.Add("UnitPrice_New", OleDbType. Currency, 0, "UnitPrice");
OleDbParameter param;
param = pc.Add("OrderID_Orig", OleDbType. Integer, 0, "OrderlD");
param. SourceVersion = DataRowVersion. Original;
param = pc.Add("ProductID_Orig", OleDbType. Integer, 0, "ProductID");
param. SourceVersion = DataRowVersion. Original;
param = pc.Add("Quantity_Orig", OleDbType. Smalllnt, 0, "Quantity");
param. SourceVersion = OataRowVerston. Original;
param = pc.Add( "Unit Price_0 rig", OleDbType. Currency, 0, "UnitPrice");
param. SourceVersion = DataRowVersion. Original;
return cmd;
static OleDbCommand CreatelnsertViaSPCommandO i OleDbCommand cmd = new OleDbCommandC'spInsertDetaiL", en);
cmd.CommandType = CommandType. StoredProcedure;
OleDbPararrteterCollection pc = cmd. Parameters;
pc.Add("OrderrD", OleDbType. Integer, 0, "OrderlD");
pc.Add("ProductID", OleDbType. Integer, 0, "ProductID");
372 Часть III Автономная работа с данными: объект DataSet модели ADO.NET рс.Add("Quantity", OleDbType.Smalllnt, 0, "Quantity"};
pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrice");
return cmd;
) static OleDbCommand CreateDeleteViaSPCommandO { OleDbCommand cmd = new OleDbCoromandC'spDeleteDetail", en);
cmd.CommandType = CommandType.StoredProcedure;
OleDbParameterCollection pc = cmd.Parameters;
OLeDbParameter param;
param = pc.Add("Order!D", OleDbType.Integer, 0, "QrderlD");
param.SourceVersion = DataRowVersion,Original;
param = pc.Add("ProductID", OleDbType.Integer, 0, "ProductID");
param.SourceVersion = DataRowVersion.Original;
param = pc,Add{"Quantity", OleDbType.Smalllnt, 0, "Quantity");
param.SourceVersion = DataRowVersion.Original;
param = pc.Add("UnitPrice", OleDbType.Currency, 0, "UnitPrtce");
param.SourceVersion = DataRowVersion.Original;
return cmd;
} static void CreateSprocsQ { OleDbCommand cmd = cn.CreateCommandC);
string strSQL;
strSQL = "CREAJE PROCEDURE spUpdateDetail \n\r" + (@OrderID_New int, @ProductID_New int, \n\r" + @Quantity_New smallint, @UnitPrice_New money, \n\r" + @OrderID_Orig int, @ProductID_Orig int, \n\r" + @Quantity_Orig smallint, @UnitPrice_Orig money) \n\r" + "AS \n\r" + "UPDATE [Order Details] \n\r" + SET OrderlD = @OrderID_New, \n\r" + ProductID = @ProductIDjJew, \n\r" + Quantity = @Quantity_New, \n\r" + UnitPrice = @UnitPrice_New \n\r" + WHERE OrderlD = @QrderID_Orig'AND \n\r" + ProductID = @ProductID_Orig AND \n\r" + Quantity = @Quantity_Orig AND \n\r" + UnitPrice = @UnitPrice_Orig";
cmd.CommandText = strSQL;
cmd.ExecuteNonQueryO;
strSQL = "CREATE PROCEDURE spInsertDetail \n\r" + C^OrderlD int, йProductID int, \n\r" + ГЛАВА 10 Передача обновлений в базу данных ^Quantity smallint, @UnitPrice money) \n\r" + "AS \n\r" + "INSERT INTO [Order Details] \n\r" + (OrderlD, ProductID, Quantity, UnitPrice) \n\r" + VALUES (@OrderID, &ProductID, ^Quantity, @>UnitPrice)";
cmd.CommandText = strSQL;
cmd.ExecuteNonQuery();
strSQL = "CREATE PROCEDURE spDeleteDetail \n\r" + (@OrderID int, @ProductID int, \n\r" + ^Quantity smallint, @UnitPrice money) \n\r" + "AS \n\r" + "DELETE FROM [Order Details] \n\r" + WHERE OrderlD = @OrderID AND \n\r" + ProductID = @ProductID AND \n\r" + Quantity = йQuantity AND UnitPrice = @UnitPrice' crnd. CommandText = strSOL;
cmd.ExecuteNonQue ry();
Использование собственной логики обновления Рассмотрим преимущества и недостатки применения собственной логики обнов ления в коде.
Преимущества Два важнейших преимущества использования собственной логики обновления в коде Ч расширенные возможности управления и производительность. По срав нению с предыдущими технологиями доступа к данным Microsoft, объект Data Adapter ADO.NET предоставляет самые широкие возможности управления. Исчез ло требование передавать обновления непосредственно в таблицы;
наконец-то стало возможно быстро и эффективно использовать хранимые процедуры.
Кроме того, поскольку вы не определяете происхождение данных с помощью технологии доступа к данным, можно считать, что любой набор результатов под держивает обновление. Когда ядро курсоров ADO не умело собрать метаданные, необходимые для передачи изменений в БД, предоставить такие данные программ но было нельзя. В ADO.NET объект DataSet разрешается заполнять результатами вызова хранимой процедуры, запроса к временной таблицы, сводными результа тами множества запросов или любым другим удобным для вас способом, и вы все равно сможете передать изменения в БД.
Предоставление логики обновления в коде повышает производительность приложения. Во фрагменте кода, передававшем обновления в БД с помощью дра курсоров ADO, меньше строк, однако ядру приходилось получать из БД имя ис ходной таблицы, исходных столбцов, а также сведения о первичном ключе ис ходной таблицы. На получение метаданных из системных таблиц БД и генера цию логики обновления на их основе требуется больше времени, чем на загрузку та ких данных из локального кода.
Автономная работа с данными: объект DataSet модели ADO.NET 374 Часть III Недостатки Недостатки использования собственной логики обновления в коде Ч зеркальное отражение преимуществ ядра курсоров ADO. Во-первых, собственная логика об новления занимает больше места. Вернитесь назад и посмотрите, сколько кода требуется для передачи обновлений с использованием объекта DataAdapter ADO.NET и сколько Ч для передачи с использованием ядра курсоров ADO. Написание та кого кода Ч утомительное занятие, отнимающее много времени.
Еще один недостаток в том, что при создании собственной логики обновле ния многие разработчики чувствуют 1 себя неуверенно. Им хотелось бы не зада вать вопросов типа Нужно ли взять имя таблицы в запросе в символы-раздели тели?, Какие маркеры параметров использовать?, <Х Какие столбцы использовать в разделе WHERE запросов UPDATE и DELETE?, Каково наиболее подходящее значение свойства OleDbType для параметра, содержащего значение дата/время? К счастью, если более быстрые способы создания логики обновления. Подробнее о них Ч далее в этой главе.
Создание логики обновления с помощью объекта CommandBuilder Объектная модель ADO.NET позволяет вам не только определить собственную логику обновления, но и предоставляет средства динамической генерации такой логики с использованием объекта CommandBuilder, по аналогии с ядром курсо ров ADO. Если создать экземпляр CommandBuilder и сопоставить его с объектом DataAdapter^ CommandBuilder попытается сгенерировать логику обновления на основе запроса, хранящегося в свойстве SelectCommand объекта DataAdapter.
Чтобы продемонстрировать принципы работы объекта CommandBuilder, я с его помощью сгенерирую логику обновления для кода, выполняющего запросы к таб лице Order Details. Следующий фрагмент создает экземпляр объекта OleDhCom mandBuilder, передавая в конструкторе объект OleDbDataAdapter. Затем он выво дит запрос, хранящийся в свойстве InsertCommand и сгенерированный объектом CommandBuilder для передачи новых записей.
Visual Basic.NET Dim strConn, strSQL As String strConn = "Provider=SQLOLEDB;
Data Source=(local)\NetSDK;
" & _ "Initial Catalog=Northwind;
Trusted_Connection=Yes;
" strSQL = "SELECT OrderlD, ProductlD, Quantity, UnitPrice " & _ "FROM [Order Details] WHERE OrderlD = 10503 " i _ "ORDER BY ProductlD" Dim da As New 01eDbDataAdapter(strSQL, strConn} Dim cb As New OleDbCommandBuilder(da) Console.WriteLine(cb,GetInsertCommand.CommandText) Visual C#.NET string strConn, strSQL;
strConn = "Provider=SQLOLEDB;
Data Source=(local)\\NetSDK;
" + "Initial Catalog=Northwind;
Trusted_Connection=Yes;
";
Передача обновлений в базу данных ГЛАВА strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER BY ProductID";
OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, strConn);
OleDbCommandBuilder cb = new OleDbCommandBuiLder(da);
Console.WriteLine(cb.GetInsertCommand().CommandText);
Как видно, текст запроса довольно сильно походит на запросы, создававшие ся нами в предыдущих разделах главы для передачи новых записей:
INSERT INTO Order Details( OrderlD, ProductID, Quantity, UnitPrice ) VALUES ( ?, ?, ?, ? ) Как объект CommandBuilder генерирует логику обновления Логика, на основе которой CommandBuilder генерирует запросы UPDATE, INSERT и DELETE, не представляет собой ничего сложного. Как и ядро курсоров ADO.
CommandBuilder обращается к БД за именами базовой таблицы и столбцов, а так же за сведениями о ключевых столбцах набора результатов запроса. Объект Com mandBuilder сгенерирует логику обновления, если выполняются все следующие условия:
Х запрос возвращает данные только из одной таблицы;
Х на таблице определен первичный ключ;
Х первичный ключ есть в результатах вашего запроса.
Как уже говорилось, первичный ключ гарантирует, что CommandBuilder обно вит: не более одной записи. Почему объект CommandBuilder налагает ограниче ние на число таблиц, упомянутых в результатах запроса? Подробнее об этом Ч в следующих разделах главы.
CommandBuilder выбирает метаданные, необходимые для генерации логики обновления, с использованием свойства SelectCommand объекта DataAdapter, На самом деле об этом уже говорилось вкратце в главе 4- Метод ExecuteReader объек та Command позволяет получить эти метаданные вместе с результатами запроса, как показано ниже:
Visual Basic.NET Dim strConn, strSQL As String StrConn = "Provider=SQLOLEDB;
Data Source=(local)\NetSDK;
" & "Initial Catalog=Northwind;
Trusted_Connection=Yes;
" strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " & _ "FROM [Order Details] WHERE OrderlD = 10503 " & _ "ORDER BY ProductID" Dim en As New OleDbConnection(strConn) Dim cmd As New 01eDbCommand(strSQL, en) cn.0pen{) Dim rdr As OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.SchemaOnly Or _ ComtnandBehavior.Keylnfo) Dim tbl As DataTable = rdr.GetSchemaTable rdr.Closef) 376 Часть III Автономная работа с данными: объект DataSet модели ADO.NET cn.CloseO Dim row As DataRow Dim col As DataColumn For Each row In tbl.Rows For Each col In tbl.Columns Console.WriteLineCcol.ColumnNarne & ": " & row(col).ToString) Next col Console. Writel_ine() Next row Visual C#.NET string strConn, strSQL;
strConn = "Provider=SQLOLEDB;
Data Source=(local)\\NetSDK;
" + "Initial Catalog=Northwind;
Trusted_Connection=Yes;
";
strSQL = "SELECT OrderlD, ProductlD, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER BY ProductlD";
OleDbConnection en = new OleDbConnection(strConn);
OleDbCommand cmd = new 01eDbCominand{strSQL, en);
cn.OpenO;
OleDbDataReader rdr;
rdr = cmd.ExecuteReader(CommandBehavior,SchemaOnly | CommandBehavior.Keylnfo);
DataTable tbl = rdr.GetSchemaTableO;
rdr.Close();
cn.CloseO;
foreach (DataRow row in tbl.Rows) { foreach {DataColumn col in tbl.Columns) Console.WriteLine(col.ColumnName + ": " + row[col].ToStringQ);
Console. WriteLineO;
' Запустив этот код, вы увидите все данные, которыми объект CommandButt der должен обладать о каждом столбце, чтобы сгенерировать логику обновления.
Имя столбца? Имена базовой таблицы и базового столбца для данного столбца?
Является ли столбец частью первичного ключа базовой таблицы? Содержит ли столбец большой объем текстовых или двоичных данных? И т.д., и т.п.
Преимущества и недостатки использования объекта CommandBuilder Сравнив фрагмент кода, сгенерированный объектом CommandBuilder, и код, на основе которого мы создавали собственную логику обновления, вы выявите два основных преимущества использования объекта CommandBuilder. Во-первых, требуется меньше кода. Во-вторых, с помощью CommandBuilder удается создавать ГЛАВА 10 Передача обновлений в базу данных логику обновления, имея даже поверхностное представление о SQL-синтаксисе запросов UPDATE, DELETE и INSERT.
Кроме того, объект CommandBuilder полезен, если у вас возникли проблемы с генерацией собственной логики обновления. Если CommandBuilder успешно сге нерирует необходимую логику, просмотрите значение свойства CommandText созданных им объектов Command или значения свойств созданных им объектов Parameter.
Объект CommandBuilder также весьма полезен в приложениях, которым тре буется поддержка обновления данных и в которых вы не хотите просматривать структуру запросов в период разработки.
Как и ядро курсоров ADO, объект CommandBuilder автоматически генерирует для вас логику обновления в период выполнения. Таким образом, он подвержен тем же проблемам и ограничениям, что и ядро курсоров ADO.
Объект CommandBuilder не предоставляет максимальной производительнос ти периода выполнения. Вы можете написать и добавить в код собственную ло гику обновления за время, меньшее, чем объекту CommandBuilder потребуется, чтобы выбрать и обработать необходимые для создания аналогичного кода ме таданные. Кроме того, CommandBuilder не позволяет управлять генерацией логи ки. Нельзя указать нужный способ оптимистического управления параллелизмом.
Нельзя передавать обновления средствами хранимых процедур.
Если бы только существовал легкий и быстрый способ генерировать логику обновления в период разработки.., Создание логики обновления средствами мастера Data Adapter Configuration Wizard В главе 5 рассказывалось, как с помощью мастера Data Adapter Configuration Wizard создавать объекты DataAdapter при работе с поставщиками OLE DB и SQL Client.NET Data Provider. Кроме того, мастер генерирует логику обновления и сохраня ет се в коде.
Одно из назначений мастера Data Adapter Configuration Wizard Ч сгенериро вать в период разработки логику обновления, упростив и ускорив вам создание эффективного кода для обновления данных. Ясно, что это глобальная цель. И хотя мастер защищен от неосторожного обращения (что такое мастер?), в большин стве ситуаций он действительно создает такой код.
Откройте в Visual Studio.NET проект с элементом, предоставляющим конст руктор (например Windows-форму, Web-форму. Web-сервис или компонент), и добавьте в конструктор объект OleDbDataAdapter. Определите строку подключе ния к своей любимой БД Northwind и на вкладке SQL statement мастера введите такой запрос:
SELECT OrderlD, ProductlD, UnltPrice. Quantity FROM [Order Details] WHERE OrderlD = ? ORDER BY ProductlD Щелкните Next. Откроется окно, аналогичное показанному на рис. 10-3 Часть Ml Автономная работа с данными: объект DataSet модели ADO.NET view wizard Results Review the hit of [asks the wizard has pnforried. Click Finish to zomplete or Back to make changes.
Generated SELECT йМипиг*.
Generated tabk mssamgs, SenestKJINSEM ateinert.
Рис. 10-3. Окно View Wizard Results мастера Data Adapter Configuration Wizard Просмотр структуры объекта DataAdapter Мастер сгенерировал для нового объекта DataAdapter запросы UPDATE, INSERT и DELETE (рис. 10-3). Щелкните кнопку Finish мастера. Выберите в панели компо нентов новый объект DataAdapter. Затем в окне Properties найдите свойство Delete Command этого объекта. Выберите свойство CommandText и щелкните кнопку справа от его значения. Откроется окно Query Builder с объектом DeleteCommand (рис. 10-4).
ь OrderTO [Order Dels ProdmtlD [Order Detj -;
Cuiantltv FCIder Dets Рис. 10-4. Объект DeleteCommand, сгенерированный мастером Как видно, запрос, сгенерированный мастером Data Adapter Configuration Wizard для передачи отложенных удалений, идентичен запросу, созданному нами вруч ную в одном из предыдущих разделов главы. Кроме того, можно найти в окне Properties свойства InsertCommand и UpdateCommand объекта DataAdapter и про смотреть прочую сгенерированную мастером логику обновления.
ГЛАВА 10 Передача обновлений в базу данных Параметры генерации логики обновления В окне SQL Statement мастера есть кнопка Advanced Options, щелкнув которую вы откроете диалоговое окно, аналогичное показанному на рис. 10-5. Это окно пре доставляет ограниченные возможности управления логикой обновления, которую генерирует мастер Data Adapter Configuration Wizard.
Если ваш объект DataAdapter только выбирает данные из БД, вы сэкономите время на разработку и выполнение, сняв флажок Generate Insert, Update And Delete Statements.
По умолчанию мастер Data Adapter Configuration Wizard добавляет в раздел WHERE запросов, передающих отложенные изменения и удаления, все столбцы, не содержащие BLOB-данных. Если снять флажок Use Optimistic Concurrency, ма стер добавит в раздел WHERE таких запросов только поля первичного ключа.
Advanced SQL Generation Options Additional Insert, Update, and Delete statements can be generated Co update th* dsta uj Х V 'Generate Insert, Update and Delete statements I Generates iniert, Update, and Delete statements based on your 5e!лct statement.
'Х/ IJse optimistic concurrency Modifies Update and Delete statements to detect whether the database has changed since the record was loaded into the daEaset TNs helps prevent concurrency conflicts.
'Х> Refresh the DataSet Adds a Select statement after Insert and Update (tatements to retrieve identity column values, drfaufc values, and other values cafcuWed by the database.
Рис. 10-5. Параметры генерации логики обновления Некоторые БД, например SQL Server, поддерживают пакетные запросы, возвра щающие записи данных. Если вы с помощью мастера Data Adapter Configuration Wizard создаете объект DataAdapter, взаимодействующий с такой БД, станет дос тупным и будет помечен флажок Refresh The DataSet. При этом мастер генериру ет запросы, повторно выбирающие содержимое измененной записи сразу после передачи соответствующих изменений. Это означает, что после вызова метода DataAdapter.Update в объекте DataSet появятся новые сгенерированные сервером значения, например значения типа timestamp и значения автоинкремента.
Подробнее об этом Ч в следующей главе. Кроме того, мы рассмотрим реали зацию аналогичной функциональности для БД, которые не поддерживают пакет ные запросы, возвращающие результаты.
Передача обновлений с помощью хранимых процедур Мастер Data Adapter Configuration Wizard также позволяет создавать объекты DataAdapter, передающие изменения в БД SQL Server посредством хранимых про цедур. В окне Choose A Query Type мастера поставьте переключатель в положение Use Existing Stored Procedures (рис. 10-6) и затем щелкните Next.
Часть Ml Автономная работа с данными: объект DataSet модели ADO.NET Нои should the date.adapter access the. dal abase' '' lite SQL wernent* Spedv a Selesi лаятв* tg-lpad fffrtrnew jDrcdprocedure <' : Choose er. *risif g oied pлfOure for each opeuten (лtat. лиг!, update snd It}, Рис. 10-6. Окно Choose A Query Type мастера Data Adapter Configuration Wizard В открывшемся окне можно выбрать для всех объектов Command объекта DataAdapter хранимые процедуры. Сначала задайте значение свойства SelectCom mand. Доступные процедуры перечислены в раскрывающемся списке (рис. 10-7). При выборе процедуры в списке справа отображается возвращаемый ею столбец. SOaia ftdaplei ConllgUfalim Wizard find Commands to Existing Stored Procedures Choose tfie riotedprasziU'sscacJenLJscedVaiY'eoj'' Рис. 10-7. Задание значения свойства SelectCommand объекта DataAdapter Задав значение свойства SelectCommand, определите значения свойств Insert Command и UpdateCommand объекта DataAdapter. Чтобы задать значение свойства SourceColumn параметров ваших обновляющих хранимых процедур, воспользуй тесь списком в правой части окна мастера (рис. 10-8). Примечание Мастер Data Adapter Configuration Wizard не позволяет задавать значение свойства SourceVersion объектов Parameter. Поскольку по умол чанию оно равно Current, вам следует с помощью окна Properties изме нить его для всех параметров, которые вы хотите связать с оригиналь ным значением измененных столбцов. ГЛАВА 10 Передача обновлений в базу данных Bind Commands to Existing Stared Procedures a scored рюевАяв far each и*гл*сл. If Ич arocedu hfch tmif* Нч data row nrtains the рагвгийг Set Irtr( oraeedur; parameters I Pe-amл*f Source Cofcjrm jeOraerl!} СтдвгШ i^Produd^D ProdutilC i@Quanl4r Quanttv jiJUntPrite Lntf-bie Рис. 10-8. Значения свойства SourceColumn параметров объекта InsertCommand Если у вас установлена Visual Studio.NET Enterprise Edition, можно также ука зать SQL-запрос, и мастер Data Adapter Configuration Wizard сгенерирует новые хранимые процедуры SQL Server для свойств SelectCommand. UpdateCommand, InsertCommand и DeleteCommand вашего объекта DataAdapter. В окне Choose A Query Туре мастера поставьте переключатель в положение Create New Stored Procedures; мастер предложит вам ввести SQL-запрос, возвращающий данные из БД (рис. 10-9). atB Aiplpr Canfieurniion УНгатй Generate the stored procedures The Stlлt stemeni Mil bg used to male th Meet. Зкеге, Update id Mete stored t*xedures. Tycein vour a^3etelstetлTient-B1^ttw>jefy Mder to^ep^all/ hit dtBihouU)lh= data adapter toad info the dalasrt? 5ELECI --. ?roductlDp ftdwnted Opt Рис. 10-9- Ввод SQL-запроса для новых хранимых процедур В следующем окне можно ввести имена генерируемых мастером хранимых процедур. Кроме того, здесь имеется кнопка Preview SQL Script, при щелчке кото рой открывается диалоговое окно с SQL-сценарием, который мастер Data Adapter Configuration Wizard сгенерировал для создания ваших хранимых процедур (рис 10-10). Если вы создаете приложение для работы с тестовой БД, воспользуйтесь этим диалоговым окном и сохраните SQL-сценарий в файл, чтобы позже выпол нять его в рабочей БД. Автономная работа с данными: объект DataSet модели ЛЕЮ.NET 382 Часть III SET МОССЛЛГ W; SELECT OKierta o-MucllO ?.-*Ч-:-. Unltfritt FfiOMfOrder Details] WHERE - (OtdsHD - ЮО-Я-Ю) O4XR BV =-(rfucSP; IM5EBT INTO[OrdH E><*] (OntotlD, Р ЗИ.ЕСТ DriterlO, ProdutllD, IjHrHh', LN(P" FRCrt J5rdw W*^ MtRE (OrdwlD. pOrdjrlC) *NO (Produc:ID - JProBultES OCS! ВУ ProdutllP; S^ve As,. ; ppy** Рис. 10-10. Просмотр SQL-сценария, сгенерированного мастером для создания новых хранимых процедур По завершении работы с мастером в БД появятся новые хранимые процедуры и объект DataAdapter будет сконфигурирован для работы с ними. Преимущества и недостатки использования мастера Как уже говорилось, одно из назначений мастера Data Adapter Configuration Wizard Ч сгенерировать логику обновления, упростив и ускорив вам создание эф фективного кода для обновления данных. Мастер предоставляет больше возмож ностей, чем объект CommandBuilder. Кроме того, он генерирует скучный код, ко торый многие разработчики предпочитают не писать. И хотя для создания логики обновления мастер запрашивает из БД ту же ин формацию схемы, что и объект CommandBuilder, он делает это лишь единожды, в период разработки, и затем сохраняет сгенерированную логику в коде. Таким образом, производительность приложения в период выполнения не падает, в от личие от использования объекта CommandBuilder. Но увы, мастер Data Adapter Configuration Wizard несовершенен. В первой версии Visual Studio.NET он работает только с объектами DataAdapter поставщиков OLE DB и SQL Client.NET Data Provider. Кроме того, он предоставляет ограниченные возможности контроля параллелизма. Изменять объекты Command, генерируемые мастером, можно, но при изменении конфигурации объекта DataAdapter все эти изменения будут утеряны. И все же, несмотря на свое несовершенство, мастер Data Adapter Configuration Wizard Ч мощная и полезная утилита. Прочие проблемы обновления Вы уже изучили основные принципы обновления содержимого БД с использова нием отложенных изменений, хранящихся в объекте DataSet. Если вы генерируе те собственную логику1 обновления (в форме запросов INSERT, UPDATE и DELETE или вызовов хранимых процедур), вам необходимо знать больше, чем просто основы. ГЛАВА 10 Передача обновлений в базу данных Например, как управлять параллелизмом, чтобы случайно не перезаписать изменения, сделанные другим пользователем? Как обрабатывать значения NULL при управлении параллелизмом? Как передавать обновления в транзакции? Какую роль играет набор TableMappings объекта DataAdapter при передаче обновлений? Подробнее о том, как осуществить это Ч в последующих разделах, Способы оптимистичного управления параллелизмом При создании многопользовательского приложения для работы с БД. передающего обновления с применением оптимистичного управления параллелизмом, важно реализовать в обновляющих запросах оптимистичный контроль параллелизма. Скажем, два пользователя вашего приложение запросили одну и ту же запись дан ных и пытаются обновить ее. Что произойдет? Это зависит от структуры обнов ляющих запросов. Предлагается четыре основных способа оптимистичного управления парал лелизмом. Использование только полей первичного ключа В SQL-запросы UPDATE и DELETE можно включать только поля первичного клю ча; при этом возникает ситуация побеждает пришедший последним. Обе попытки обновления завершатся успешно. Понятно, что БД не способна поддерживать оба набора обновлений. Должен остаться лишь один. Изменения, сделанные первым пользователем, будут переопределены изменениями, внесенными последним поль зователем. Вот кратко последовательность действий при возникновении подобной си туации: Х пользователь А выбирает запись; Х пользователь Б выбирает запись; Х пользователь Б изменяет запись и успешно передает изменения; Х пользователь А изменяет запись и успешно передает изменения, перезаписы вая изменения, внесенные пользователем Б. Пользователь А даже не знает, что в период времени между выполнением ис ходного запроса и передачей обновлений в БД соответствующая запись БД была изменена другим пользователем. Если ситуация побеждает пришедший последним Ч именно то, что вам нужно, данный способ управления параллелизмом подойдет вам. Однако когда требует ся исключить возможность непреднамеренной перезаписи чужих изменений дру гими пользователями, этот способ неприемлем. В отличие от мастера Data Adapter Configuration Wizard, объект CommandBuilder не предоставляет такого варианта оптимистичного управления параллелизмом. На вкладке Advanced Options диалогового окна мастера снимите флажок Use Optimistic Concurrency. Использование всех полей в разделе WHERE Что, если вариант побеждает пришедший последним* вам не подходит? Напри мер, требуется исключить возможность перезаписи пользователем А изменений. 384 Часть III Автономная работа с данными: объект DataSet модели ADO.NET внесенных в БД другими пользователями в период между выполнением пользо вателем А оригинального запроса и передачей им обновлений в БД. По умолчанию объект CommandBuilder и мастер Data Adapter Configuration Wizard включают в раздел WHERE все поля. Такая логика исключает перезапись изменений, сделанных другими пользователями в интервал времени между тем моментом, когда ваш код выбрал запись, и тем, когда он попытался передать от ложенные изменения этой записи в БД. Рассмотрим пример. Скажем, пользователи А и Б выбрали одну и ту же запись о клиенте. Пользователь Б изменил значение поля ContactName и передал изме нение в БД. При обновлении на основе запросов приложение включает в раздел WHERE все поля, и поэтому запрос UPDATE выглядит следующим образом: UPDATE Customers SET CustomerlD = 'ABCDE', CompanyName = 'Original Company Name', ContactName = 'New Contact', Phone = '600-555-1212' WHERE Customer-ID = 'ABCDE' AND CompanyName = 'Original Company Name' AND ContactName = 'Original Contact' AND Phone = '800-555-1212' Тем временем пользователь А изменил значение поля CompanyName той же записи. Поскольку пользователь А выбрал запись до того, как пользователь Б пе редал измененное значение поля ContactName в БД. запрос UPDATE пользователя А будет выглядеть так; UPDATE Customers SET CustomerlD = ' A B C D E ', CompanyName = 'New Company Name', ContactName = 'Original Contact', Phone = '800-555-1212' WHERE CustomerlD = 'ABCDE' AND CompanyName = 'Original Company Name' AND ContactName = 'Original Contact' AND Phone = 'BOO-555-1212' Значение поля ContactName этой записи в БД изменилось, и поэтому ни одна запись таблицы не удовлетворяет критериям раздела WHERE. Следовательно, БД не изменит запись о клиенте. Объект DataAdapter обращается к БД, чтобы узнать число измененных запросом записей, обнаруживает, что нужная запись не откор ректирована, и соответствующим образом помечает объект DataRow. Подробнее о выявлении и разрешении таких конфликтов Ч в главе 11. Именно этот способ управления параллелизмом использует объект Command Builder. Мастер Data Adapter Configuration Wizard применяет данный способ уп равления параллелизмом по умолчанию. Примечание Вообще говоря, БД не позволяют сравнивать два ВЮВ-значения. В столбце с ВЮВ-данными можно хранить сотни мегабайт информа ции, и сравнение двух ВЮВ-значений окажется весьма неэффективным, если вообще возможным. Средства генерации кода типа объекта Сот mandBuilder и мастера Data Adapter Configuration Wizard исключают столбцы с ВЮВ-данными из раздела WHERE обновляющих запросов. Помните об этом при создании собственной логики обновления. ГЛАВА 10 Передача обновлений в базу данных Использование полей первичного ключа и полей типа timestamp Поля типа timestamp позволяют упростить раздел WHERE обновляющих запро сов. На самом деле в SQL Server поле типа timestamp содержит не сведения о дате и времени, а двоичные данные, уникальные в пределах БД. В таблице SQL Server можно определить поле типа timestamp, и при каждом изменении содержимого записи SQL Server будет изменять значение поля типа timestamp этой записи. Давайте добавим в таблицу Customers поле типа timestamp и изменим предыдущий запрос таким образом; UPDATE Customers SET CustomerlD = ' A B O D E ', CompanyName = 'Original Company Name', ContactName = 'New Contact', Phone = '800-555-1212'' WHERE CustomerlD = 'ABODE' AND TimestampColumn = OxOOOOOOOOOOOOOOCC Поскольку при каждом обновлении записи сервер генерирует новое значение поля типа timestamp, в разделе WHERE можно воспользоваться комбинацией по лей первичного ключа и поля timestamp и тем самым предотвратить перезапись изменений, сделанных другими пользователями. Большинство БД поддерживают аналогичный тип данных. В одних использу ется уникальное двоичное значение, в других Ч значение дата/время. Просмот рите документацию вашей СУБД, чтобы узнать конкретный тип данных и опре делить, как заставить БД обновлять соответствующее значение при каждом изме нении содержимого записи. На настоящий момент ни объект CommandBuilder, ни мастер Data Adapter Configuration Wizard не поддерживают генерацию логики для данного варианта оптимистичного управления параллелизмом. Примечание В SQL Server 2000 типы данных rowversion и timestamp аналогич ны, и в документации SQL Server рекомендуется использовать ключевое слово rowversion. а не timestamp. Термин timestamp используется здесь потому, что на момент написания данной книги он наиболее широко известен. Для контроля параллелизма я предпочитаю использовать комбинацию полей первичного ключа и поля типа timestamp, поскольку при этом логика обновле ния гораздо проще, и БД при попытке обновления требуется просматривать меньше полей. Использование полей первичного ключа и измененных полей По умолчанию ядро курсоров ADO включает в раздел WHERE обновляющих за просов только поля первичного ключа и оригинальные значения измененных по лей. Кроме того, в раздел SET запросов UPDATE ядро включает только изменен ные поля. Рассмотрим наш пример, в котором реализована данная стратегия обновле ния. Предположим, пользователи А и Б одновременно выбрали одну и ту же за пись о клиенте. Пользователь А изменил значение поля CompanyName, а пользо ватель Б Ч значение поля ContactName. Пользователь Б первым передал отложенное изменение поля ContactName. Его запрос UPDATE выглядит так: Автономная работа с данными: объект DataSet модели ADO.NET 386 Часть III UPDATE Customers SET ContactName = 'New Contact' WHERE Customer-ID = 'ABCDE' AND ContactName = 'Original Contact' Затем пользователь А передает отложенное изменение поля CompanyName с помощью такого запроса UPDATE: UPDATE Customers SET CompanyName = 'New Company Name' WHERE Customer-ID = 'ABCDE' AND CompanyName = 'Original Company Name' Содержимого записи изменится с CustomerlD CompanyName ContactName ABCDE Original Company Name Original Contact на CustomerlD CompanyName ContactName ABCDE Original Company Name New Contact и затем на CustomerlD CompanyName ContactName ABCDE New Company Name New Contact Оба изменения будут переданы успешно, и изменение пользователя А не пе резапишет изменение, внесенное пользователем Б. Структура объекта DataAdapter ADO.NET не подходит для этой стратегии об новления, поскольку вам требуется откорректировать структуру запроса на основе полей, измененных в записи, которая содержит отложенные изменения. Объект DataAdapter предоставляет значения параметров обновляющих запросов для каждой записи отдельно, но реальную структуру параметризованного запроса не редак тирует. Теоретически можно написать код, динамически изменяющий структуру со ответствующего объекта Command, и использовать его при обработке события RowUpdating объекта DataAdapter. Полагаю, у данной стратегии есть свои преиму щества, однако затраты на ее реализацию перевешивают их. Работа со значениями NULL В таблице Customers БД Northwind есть поле Region, принимающее строки до символов длиной, а также значения NULL. Поле Region многих записей имеет зна чение NULL. Для получения таких записей большинство программистов восполь зуется следующим запросом: SELECT CustomerlD, CompanyName, ContactName, Phone FROM Customers WHERE Region = NULL ГЛАВА 10 Передача обновлений в базу данных Воспользовавшись этим запросом в ADO.NET или выполнив его в SQL Query Analyzer, вы увидите, что он вернул ноль записей. Значения NULL Ч особый случай в мире БД, особенно когда дело доходит до сравнения таких значений в запросах. Согласно стандартам ANSI, сравнивать зна чения NULL с помощью оператора = нельзя. Вместо этого следует использовать оператор IS NULL. Показанный далее запрос возвращает записи таблицы Customers, значение поля Region которых Ч NULL: SELECT Customer-ID, CompanyName, Contact Name, Phone FROM Customers WHERE Region IS NULL Что общего имеют значения NULL с передачей обновлений в БД при помощи объекта DataAdapter? Давайте кратко обсудим значение свойства Commandlext объекта Command, созданного нами ранее для передачи измененных записей в таблицу Order Details; UPDATE [Order Details] SET Order-ID = ?, ProductID = ?, Quantity = ?, UnitPrice = ? WHERE OrderlD = ? AND ProductID = ? AND Quantity = ? AND UnitPrice = ? Ни одно из указанных в запросе полей не принимает значений NULL. Как след ствие, раздел WHERE этого запроса относительно прост. Но что, если бы поля Quantity и UnitPrice принимали такие значения? Скажем, вы меняете текущее зна чение поля Quantity, NULL, одной из записей на 20, Если заменить параметры ре альными значениями, запрос будет выглядеть так: UPDATE [Order Details] SET OrderlD = 12345, ProductlD = 1, Quantity = 20, UnitPrice = WHERE OrderlD = 12345 AND ProductID = 1 AND Quantity = Null AND UnitPrice = Из-за оператора Quantity = Null, включенного в раздел WHERE, запрос не из менит ни одной записи. Значение поля Quantity нужной нам записи БД Ч Null, однако сравнение Null = Null вернет false, и поэтому БД не изменит ни одной за писи, Так как же изменить раздел WHERE наших запросов и использовать значения NUJ^L при контроле параллелизма? Если определенное поле принимает значения NULL, часть запроса ColumnName = ? можно заменить следующей (ColumnName = ? OR ((ColumnNarne IS NULL) AND {? IS NULL))) Нам нужно, чтобы операция сравнения возвращала true, когда поле и параметр имеют одинаковое, отличное от NULL значение или оба имеют значение NULL. Предположим, объект DataAdapter выбирает из таблицы Customers столбцы CustomerlD, Company-Name, ContactName и Phone. Поля CustomerlD и CompanyName не принимают, а поля ContactName и Phone принимают значения NULL. Следова тельно, в разделах WHERE обновляющих запросов следует реализовать проверку па наличие значений NULL. Если вы создаете логику обновления средствами мае 388 Часть 111 Автономная работа с данными: объект DataSet модели ADO.NET тера Data Adapter Configuration Wizard, то обнаружите, что мастер сгенерировал для передачи изменений следующий код и дополнил его соответствующими про верками на наличие значений NULL: UPDATE Customers SET Customer-ID = ?, CompanyName = ?, ContactName = ?, Phone = ? WHERE (Customer-ID = ?} AND (CompanyName = ?) AND {ContactName = ? OR ((? IS NULL) AND (ContactName IS NULL))) AND (Phone = ? OR ((? IS NULL) AND (Phone IS NULL))) Как уже говорилось, мастер Data Adapter Configuration Wizard выполняет очень большую работу по созданию логики обновления. Даже если вы разрабатываете собственную логику, просмотрите сгенерированный мастером код, чтобы проконт ролировать себя и свою работу. Передача обновлений в транзакциях А если вам требуется передать все изменения в виде отдельной единицы работы, чтобы или все они были успешно переданы в БД, или ни одно из них. Простей шее решение Ч поместить обновления в транзакцию. Тем не менее у объекта DataAdapter нет свойства Transaction. Объект DataAdapter в действительности не передает изменения. Он лишь пе редает работу объектам Command, хранящимся в свойствах UpdateCommand. Insert Command и DeleteCommand. Объект Command предоставляет свойство Transaction, и поэтому для передачи изменений с помощью DataAdapter нужно задать значе ние свойства Transaction объекта Command, используемого объектом DataAdapter, как показано ниже: Visual Basic.NET Dim strConn, strSQL As String strConn = "Provider=SQLOLEDB; Data Source=(local)\NetSOK; " & _ "Initial Catalog=Northwind; Trusted_Connection=Yes; " strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " & "FROM [Order Details] WHERE OrderlD = 10503 " & _ "ORDER BY ProductID" Dim tbl As New DataTableO Dim en As New OleDbConnection(strConn) Dim da As New OleDfaOataAdapterfstrSQL, en) 'Define updating logic for the DataAdapter. 'Открываем соединение и выбираем результаты запроса сл.0реп() da.Fill(tbl) 'Изменяем содержимое объекта DataTable 'Создаем новую транзакцию Dim txn As OleDbTransaction = cn,BeginTransaction() 'Задаем значение свойства Transaction объектов Command, 'используемых объектом DataAdapter ГЛАВА 10 Передача обновлений в базу данных da.UpdateComitiand.Transaction = txn da.InsertCommand.Transaction = txn da.DeleteCommand.Transaction = txn 'Передаем изменения da.Update(tbl) 'Подтверждаем изменения и закрываем транзакцию txn.Commit() cn.Close() Visual C#.NET string strConn, strSQL; strConn = "Provider=SGLOLEDB; Data Source=(local)\\NetSDK; " + "Initial Catalog=Northwind; Trusted_Connection=Yes; "; strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER BY ProductID"; DataTable tbl = new DataTableO; DleDbConnection en = new OleDbConnection(strConn); OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, en); //Define updating logic for the DataAdapter. //Открываем соединение и выбираем результаты запроса сп.ОрепО; da.Fill(tbl); //Изменяем содержимое объекта DataTable //Создаем новую транзакцию OleDbTransaction txn = cn.BeginTransactionO; //Задаем значение свойства Transaction объектов Command, //используемых объектом DataAdapter da.UpdateCommand,Transaction = txn; da.InsertCommand.Transaction = txn; da.DeleteCommand.Transaction = txn; //Передаем изменения da.Update(tbl); //Подтверждаем изменения и закрываем транзакцию txn.CommitO; cn.CloseO; Когда логику обновления генерируют средствами объекта CommandBuilder, передавать изменения в транзакции несколько сложнее. В действительности со здаваемый экземпляр объекта CommandBuilder не генерирует логику обновления. Он делает это лишь после того, как вы вызовете метод DataAdapter Update. Если вы хотите передавать изменения в транзакции с использованием объекта Command Builder, описанная выше модель поведения создает некоторые проблемы, 390 Часть III Автономная работа с данными; объект DataSet модели ADO.NET При попытке передать изменения с использованием аналогичного следующе му кода ADO.NET сгенерирует исключение: Visual Basic.NET Dim strConn, strSQL As String Dim tbl As New DataTable() Dim en As New 01eDbConnection{strConn) Dim da As New 01eDbDataAdapter(strSQL, en) Dim cb As New OleDbCommandBuilder(da) cn.OpenO da.Fill(tbl) Dim txn As OleDbTransaction = en.Begin!ransaction() da.Update(tbl) txn.CommitO cn.Close() Visual C#.NET string strConn, strSQL; DataTabLe tbl = new DataTableC); OleDbConnection en = new OleDbConnection(strConn); OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, Cn); OleDbCommandBuilder cb = new QleDbCommandBuilder(da); cn.OpenO; da.Fill(tbl); OleDbTransaction txn = cn.BeginTransaction(); da.Update(tbl); txn.CommitO; cn.CloseO; При вызове метода DataAdapterL'pdate объект CommandBuilder выбирает из БД нужные метаданные, используя свойство SelectCommand объекта DataAdapter. Мы не сопоставили объект Command в свойстве SelectCommand с только что создан ной транзакцией. Следовательно, объекту CommandBuilder не удастся воспользо ваться этим свойством, и он генерирует исключение. Если добавить перед вызовом метода Update объекта DataAdapter такую стро ку, код успешно выполнится: da.SelectCommand.Transaction = txn Однако это означает, что CommandBuilder выбирает из БД информацию схе мы в транзакции. Вообще говоря, в течение транзакции следует как можно мень ше трогать данные БД. Более удобный вариант Ч заставить объект CommandBuilder сгенерировать логику обновления перед началом транзакции, вызвав метод Сот mandBuilder.GelUpdateCommand (или GetlnsertCommand, или GetDeleteCommand), Затем, используя следующий код, можно сопоставить сгенерированные объек том CommandBuilder объекты Command с новым объектом Transaction, и DataAdapter передаст обновления в транзакции: ГЛАВА 10 Передача обновлений в базу данных Visual Basic.NET Dim strConn, strSQL As String Dim tbl As New DataTable() Dim en As New OleDbConnection(strConn) Dim da As New 01eDbDataAdapter(strSQL, en) Dim cb As New OleDbCommandBuilder(da) en.Open() cb,GetUpdateCommand() da.Fill(tbl) Dim txn As OleDbTransaction = en.Begin!ransaction() cb.GetUpdateCommand.Transaction = txn cb.GetlnsertCommand.Transaction = txn cb.GetDeleteCommand.Transaction = txn da.Update(tbl) txn.Commit() cn.Close() Visual C#.NET string strConn, strSQL; DataTable tbl = new DataTableO; OleDbConnection en = new OleDbConnection(strConn); OleDbDataAdapter da = new 01eObDataAdapter(strSQL, en); OleDbComroandBuilder cb = new OleDbCommandBuilder(da); cn.0pen(); cb.GetUpdateCommandO; da.Fill(tbl); OleDbTransaction txn = en. BeginTransactionO; cb.GetUpdateComniandC).Transaction = txn; cb.GetlnsertCommandO. Transaction = txn; cb. GetDeleteCommandO.Transaction = txn; da.Update(tbl); txn.CommitO; cn.Close(); Использование набора TableMappings В главе 5 рассказывалось, что набор TableMappings объекта DataAdapter опреде ляет, как метод DataAdapterftil заполняет объект DataSet. В следующем коде при вызове метода DataAdapterFill создается новый объект DataTable, свойству TableName которого задается значение Table: Visual Basic.NET Dim strConn, strSQL As String strConn = "Provider=SQLOLEDB; Data Source=(local)\NetSDK; " & _ "Initial Catalog=Northwind; Trusted_Connection=Yes; " strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " & Часть III Автономная работа с данными: объект DataSet модели ADO.NET "FROM [Order Details] WHERE OrderlD = 10503 " & _ "ORDER BY ProductlD" Dim da As New 01eDbDataAdapter(strSQL, strConn) Dim ds As New DataSet() da.Fill(ds) Visual C#.NET string strConn, strSQL; strConn = "Provider=SQLOLEDB; Data Source=(local)\\NetSDK; " + "Initial Catalog=Northwind; Trusted_Connection=Yes; "; strSQL = "SELECT OrderlD, ProductlD, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER BY ProductlD"; OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, strConn); DataSet ds = new DataSetO; da.FillCds); Если новому объекту DataTable требуется задать имя Order Details, код можно изменить двумя способами. Во-первых, задать свойству TableName нужное значе ние при помощи перегруженного метода Fill. Visual Basic.NET Dim da As New 01eDbDataAdapter(strSQL, strConn) Dim ds As New DataSetO da.Fill(ds, "Order Details") Visual C#.NET OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, strConn); DataSet ds = new DataSetO; da.Flll(ds, "Order Details"); Во-вторых, добавить элемент в набор TableMappings объекта DataAdapter, что бы последний знал, что сопоставлен с объектом DataTable под именем Order Details: Visual Basic.NET Dim da As New 01eDbDataAdapter(strSQL, strConn) da.TableMappings.Add("Table", "Order Details") Dim ds As New DataSetO da.Fill(ds) Visual C#.NET OleDbDataAdapter da = new 01eDbDataAdapter(strSOL, strConn); da.TableMappings.Add("Table", "Order Details"); ГЛАВА 10 Передача обновлений в базу данных DataSet ds = new DataSetO; da.Fill(ds); При передаче обновлений набор TableMappings работает аналогичным обра зом. Если методу DataAdapter.Update передать лишь объект DataSet, DataAdapter на основе своего набора TableMappings определит, какой объект DataTable из соста ва DataSet нужно просмотреть: Visual Basic.NET Dim da As New 01eDbDataAdapter{strSQL, strConn) da,TableHappings.Add("Table", "Order Details") 'Определяем логику обновления Dim ds As New DataSetO cJa.Fill(ds) 'Изменяем группу записей da.Update(ds) Visual C#.NET OleDbDataAdapter da = new 01eDbDataAdapter(strSQL, strConn); //Определяем логику обновления.. da.TableMappings.Add("Table", "Order Details"); DataSet ds = new DataSetO; da.Fill(ds); //Изменяем группу записей da.Update(ds); Если набор TableMappings объекта DataAdapter не заполнялся, используйте либо метод Update, принимающий объект DataSet и имя таблицы, либо метод Update, принимающий объект DataTable: Visual Basic.NET Dim da As New QleDbDataAdapter(strSQL, strConn) 'Определяем логику обновления Dim ds As New DataSetO da.FilKds, "Order Details") 'Изменяем группу записей da.Update{ds, "Order Details") Dim da As New 01eDbDataAdapter(strSQL, strConn) 'Определяем логику обновления Dim tbl As New DataTableC) da.Fill(tbl) 'Изменяем группу записей da.Update(tbl) 394 Часть III Автономная работа с данными: объект DataSet модели ADO.NET Visual C#.NET QleDbDataAdapter da = new 01eDbDataAdapter(strSQL, strConn); //Определяем логику обновления DataSet ds = new DataSetQ; da,Fill(ds, "Order Details"); //Изменяем группу записей da.Updatefds, "Order Details"); //или OleDbDataAdapter da = new 01eDbOataAdapter(strSQL, strConn); //Определяем логику обновления DataTable tbl = new DataTableO; da.Fill(tbl); //Изменяем группу записей da.Update(tbl); Как правило, управлять объектом DataTable, используемым при вызове мето дов DataAdapterFill и DataAdapterlJpdate, следует посредством одинаковой логики. Лучший способ обновления ADO.NET предоставляет множество вариантов передачи обновлений. Можно ге нерировать логику обновления в период выполнения при помощи объектов Сот mandButtder. Или предоставить собственную логику обновления в коде, переда вая изменения средствами запросов INSERT, UPDATE и DELETE или вызовов хра нимых процедур. Кроме того, к вашим услугам мастер Data Adapter Configuration Wizard, который позволяет быстро создать логику обновления в период разработки. Что же выбрать? Ответ в значительной степени зависит от параметров приложения. Чтобы до стичь максимальной производительности, можно сконфигурировать объекты DataAdapter для передачи обновлений посредством вызовов хранимых процедур. Однако если приложение должно работать с БД, например Microsoft Access, не поддерживающими хранимые процедуры, данное решение неприемлемо. Восполь зуйтесь запросами INSERT, UPDATE и DELETE. Все это следует учитывать при вы боре подходящего для приложения способа обновления. С общей точки зрения, рекомендую вам по возможности передавать измене ния с помощью вызовов хранимых процедур. Если ваш приоритет Ч взаимодей ствие с различными СУБД, используйте обновления на основе запросов (INSERT, UPDATE и DELETE). Независимо от выбранного вами способа обновления, созда вайте собственную логику обновления. Применяйте средства генерации кода типа мастера Data Adapter Configuration Wizard, чтобы сэкономить время на разработ ку, и избегайте генерировать логику обновления в период выполнения. Если из главы вы запоминаете что-то одно, то пусть это будет вот что: используйте объекты Comm-andBuilder в приложениях только при крайней необходимости. ГЛАВА 10 Передача обновлений в базу данных Есть еще несколько сложных случаев обновления, о которых я собираюсь нам рассказать. Как выбрать только что сгенерированные значения автоинкремента? Как передать из объекта DataSet, содержащего новые и удаленные записи, изме нения в несколько связанных таблиц? Как выявить и обработать неудачные по пытки обновления? Как средствами ADO.NET работать с распределенными тран закциями? Подробнее об этих и других сложных ситуациях Ч в следующей главе. Особенности объекта OleDbCommandBuilder Здесь обсуждается объект CommandBuilder ADO.NET, и я полагаю, что небесполезно рассмотреть свойства и методы объекта OleDbCommandBuilder. На одной из особенностей объекта CommandBuilder стоит остановиться отдель но. Объекты OleDbCommandBuilder и SqlCommandBuilder не происходят от одно го базового класса. Фактически в первом выпуске объектной модели ADO.NET базовый класс CommandBuilder отсутствует, Написание кода, получающего от СУБД необходимые метаданные и преобра зующего их в логию,' обновления, Ч непростая задача. В противном случае объект CommandBuilder был бы не нужен, Поскольку7 код, создающий класс CommandBuilder для поставщика данных.NET, весьма тривиален и использование объектов Сет mandBuilder в период выполнения приводит к падению производительности, по явление сторонних поставщиков данных.NET без класса CommandBuilder меня не удивит, Свойства объекта OleDbCommandBuilder В табл. 10-1 перечислены свойства объекта CommandBuilder. Таблица 10-1. Свойства объекта CommandBuilder Описание Тип данных Свойство Возвращает объект DataAdapter. для которого DataAdapter DataAdapter CommandBuilder генерирует логику обновления Содержит префикс, используемый объектом QuotePrefix String CommandBuilder для имен таблиц и столбцов Содержит суффикс, используемый объектом String QuoteSuffix CommandBuilder для имен таблиц и столбцов Свойство DataAdapter Свойство DataAdapter объекта CommandBuilder позволяет просмотреть или изме нить объект DataAdapter. сопоставленный с объектом CommandBuilder. Значение этого свойства можно также задать в конструкторе объекта CommandBuilder, Свойства QuotePrefix и QuoteSuffix Свойства QuotePrefix и QuoteSuffix содержат символы-разделители, используемые объектом CommandBuilder для имен таблиц и столбцов в генерируемых им за просах. Значение этих свойств по умолчанию Ч пустая строка. 396 Часть III Автономная работа с данными: объект DataSet модели ADO.NET Методы объекта OleDbCommandBuilder В табл. 10-2 перечислены методы объекта CommandBuilder. Таблица 10-2. Методы объекта CommandBuilder Метод Описание Возвращает сведения о параметрах объекта Command, вызываю DeriveParatneters щего хранимую процедуру Возвращает объект Command с логикой для свойства GetDeleteCommand DeleteComtnand объекта DataAdapter Возвращает объект Command с логикой для свойства GetlnsertCommand InsertCommand объекта Data/\dapter Возвращает объект Command с логикой для свойства GetUpdateCotnmand UpdateCommand объекта DataAdapter Указывает объекту CommandBuilder создать логик)' обновления RefreshScbema заново Метод DeriveParameters Объект CommandBuilder способен не только генерировать логику обновления для объектов DataAdapter, но и получать сведения о параметрах хранимых процедур. Следующий фрагмент кода с помощью метода CommandBuilderDerivePdrameters получает и выводит сведения о параметрах вызова хранимой процедуры: Visual Basic.NET Dim strConn As String strConn = "Provider=SQLOLEDB; Data Source=(local)\NetSDK; " & "Initial Catalog=Northwind; Trusted_Connection=Yes; " Dim en As New OleDbConnection(strConn) Dim cmd As New 01eDbCommand("CustOrdersOrders", en) cmd.CommandType = CommandType.StoredProcedure Dim cb As New 01eDbCommandBuilder() cn,0pen() cb.OeriveParameters(cmd) cn.CloseO Dim param As OleDbParameter For Each param In cmd.Parameters Console.WriteLine(pa ram.ParameterName) Console.WriteLine(vbTab & param.Direction.ToString) Console.WriteLine(vbTab & param.OleDbType.ToStrtng) Console. WriteLineO Next param Visual C#.NET string strConn; strConn = "Provider=SQLOLEDB; Data Source=(local)\\NetSDK; " + "Initial Catalog=Northwind; Trusted_Connection=Yes; "; OleDbConnection en = new OleDbConnection(strConn); OleDbCommand cmd = new OleDbCommandC'CustOrdersOrders", en); ГЛАВА 10 Передача обновлений в базу данных cmd.ComrnandType = CommandType.StoredProcedure; OleDbCommandBuilder cb = new 01eDbCommandBuilder(); cn.OpenO; cti.DeriveParameters(cmd); cn.Close(); foreach {OleDbParameter param in cmd.Parameters) { Console.WriteLine{pa ram,ParameterName); Console. WriteLine("\t" + param. Direction. ToStringO); Console.WriteLine("\t" + param.OleDbType.ToStringO); Console.WriteLine(); } Если вы пытаетесь создать набор Parameters для объекта Command, вызываю щего хранимую процедуру, но не знаете, какие значения задать свойствам Size, Precision и Size, наберите и используйте приведенный выше код в период разра ботки. Примечание Для использования метода DeriveParameters должен быть досту пен объект Connection, определенный для указанного объекта Command. Методы GetDeleteCommand, GetlnsertCommand и GetUpdateCommand Методы GetDeleteCommand, GetlnsertCommand и GetUpdateCommand позволяют просматривать логику, сгенерированную объектом CommandBuilder. Кроме того, они полезны в период разработки. Можно в коде небольшого приложения создать объект CommandBuilder и затем с помощью данных методов выводить значение свойства CommandText и параметры, сгенерированные объектом CommandBuilder. Затем эту же логику обновления следует использовать в коде, набрав вручную тот же запрос и те же параметры. Метод RefreshSchema Если в приложении вы изменяете структуру запроса, используемого объектом DataAdapter, вам, возможно, пригодится метод RefreshSchema объекта Command Builder. Когда изменяется значение свойства CommandText запроса SelectCommand. определенного для объекта DataAdapter, какие-либо события объекта DataAdapter не наступают. После того как CommandBuilder сгенерирует вашу логику обновле ния, его работа, по его мнению, будет закрнчена. Если изменили структуру запроса, используемого объектом DataAdapter и объект}' CommandBuilder требуется гене рировать логику обновления заново, можно вызывать метод CommandBuilderRef reshSchema. При этом объект CommandBuilder не приступит к работе сразу же. Будет про сто установлен флаг, указывающий, что текущая логика неправильна. Command Builder сгенерирует логику обновления заново только при вызове метода Data Adapter Update или одного из методов Get Ответ. Вам предоставлена возможность заполнить объект DataSet и передать изменения в БД при помощи одного и того же объекта DataAdapter, однако это не обязательно. Скажем, у вашего объекта промежуточного уровня имеются два простых ме тода, один из которых возвращает новый объект DataSet, а другой Ч передает отложенные изменения из DataSet в БД. Для каждого из методов можно задейство вать отдельные объекты DataAdapter. Если вы просто заполняете объект DataSet, объект)- DataAdapter логика обновления не требуется. Если же вы используете DataAdapter только для передачи изменений, для него, напротив, не требуется определять значение свойства SelectCommand, На самом деле для объекта DataAdapter следует определять только те объекты Command, которые будут выполняться. Так, если вы знаете, что DataAdapter будет только передавать новые записи (а не изменять или удалять существующие), можно определить только значение свойства InsertCommand. Поскольку DataAdapter не будет выполнять объекты Command, хранящиеся в свойствах SelectCommand. Update Command и DeleteCommand, задавать значения этих свойств не требуется. Единственное исключение из данного правила связано с определением логи ки обновления для объекта DataAdapter при помощи объекта CommandButtder. Последнему не удастся сгенерировать логику обновления, если не определено значение свойства SelectCommand объекта DataAdapter. Вопрос. Мне нужно заполнить объект DataTable результатами соединяющего запроса, изменить содержимое этого объекта и затем передать изменения в БД с помощью объекта DataAdapter. Создать требуемую логику обновления нельзя ни средствами мастера Data Adapter Configuration Wizard, ни объекта CommandButtder. Как быть? Ответ. Прежде всего, рекомендую просмотреть раздел главы 7, посвященный соединяющим запросам. Ни один из указанных компонентов не способен создавать логику обновления потому, что неясно, что же на самом деле будет означать изменение возвращен ных запросом данных. Возьмем для примера запрос, возвращающий сведения о заказанных товарах, и изменим его так, чтобы в наборе результатов он возвра щал и названия товаров: SELECT D.OrderlD, P.ProductName, D.ProductID, D.Quantity, D.UnitPrice FROM [Order Details] D, Products P WHERE D.OrderlD = 10503 AND D.ProductID = P.ProductID ORDER BY P.ProductID ГЛАВА 10 Передача обновлений в базу данных Если выберем результаты запроса в объект DataTable и изменим одну из запи сей, то как следует отредактировать содержимое БД? Нам ответ ясен. Нужно из менить соответствующую запись таблицы Order Details БД Однако для мастера Data Adapter Configuration Wizard и объекта CommandBuilder такой ответ не очевиден. Ядро курсоров ADO автоматически генерирует для вас логику обновления, даже при работе с соединяющими запросами, однако эта логика многих разработчи ков разочаровала. Если сгенерировать с помощью показанного запроса объект Recordset ADO и изменить лишь поля, соответствующие таблице Order Details, ядро курсоров ADO попытается изменить только соответствующую запись таблицы Order Details. Однако, если вы захотите заказать другой товар и, чтобы запись данных ото бражалась правильно, отредактируете значения полей ProductlD (таблица Order Details) и ProductName (таблица Products), ядро курсоров ADO попытается изме нить значение поля ProductlD в таблице Order Details и значение поля ProductName в таблице Products. Спорю, что это вам совсем не нужно. К счастью, в отличие от ADO, ADO.NET не является черным ящиком и позво ляет вам создавать собственную логику обновления. В рассматриваемом случае обновления нужно передать только в таблицу Order Details и поэтому можно опре делить собственную логику обновления, игнорирующую изменения содержимо го столбца ProductName. Как создать собственную логику обновления? В случае с соединяющим запро сом ни объект CommandBuilder, ни мастер Data Adapter Configuration Wizard не окажут вам сколь либо значительной помощи. Однако можно временно опустить поле ProductName, создать нужную логику обновления с помощью любого из этих компонентов и затем снова добавить поле ProductName в запрос. Окольными путями, зато эффективно. Вернемся к первой моей рекомендации. В разделе Использование объектов DataRelation в объектах DataColumn, основанных на выражениях* главы 7 приво дится фрагмент кода, демонстрирующий, как средствами нескольких объектов DataTable и объекта DataRelation симулировать результаты соединяющего запро са. Одно из преимуществ такого подхода Ч то, что логика обновления значительно упрощается. Данные объектов DataTable соответствуют отдельным таблицам БД, Объект CommandBuilder и мастер Data Adapter Configuration Wizard способны сгенерировать необходимую вам логику обновления. Вопрос. Вы рассказали только об оптимистичном управлении блокировками. Как в ADO.NET использовать пессимистичное управление ими? Ответ. При пессимистичном управлении блокировками на изменяемую запись предварительно налагается блокировка. Поскольку содержимое DataSef отсоеди нено от БД, простого способа, позволяющего перед изменением записи объекта DataSet наложить блокировку на данные БД, нет. Тем не менее реализовать ана логичную функциональность удается при помощи транзакций, Допустим, пользователь редактирует данные на экране и, чтобы гарантировать успешную передачу этих изменений, вам требуется наложить блокировку на со ответствующие данные БД. Можно открыть транзакцию и выполнить в ней еле Автономная работа с данными: объект DataSet модели ADO.NET 400 Часть III дующий запрос, чтобы наложить блокировку на нужные записи БД и запретить другим пользователям изменять их: SELECT * FROM [Order Details] HOLDLOCK WHERE OrderlD = Примечание Показанный запрос предназначен специально для SQL Server 2000. Не все БД поддерживают такой синтаксис. Если вы работаете с другими БД, подробнее о блокировке данных в запросах Ч в документации БД. У этого подхода есть несколько значительных недостатков. Что, если пользо ватель забудет щелкнуть кнопку Submit Changes приложения и пойдет на кухню за пончиком и порцией кофе? Записи БД останутся блокированными. Чем больше данных блокировано и чем дольше, тем ниже масштабируемость приложения. Настала пора мне исповедаться. Не единожды в жизни я совершал ошибки. Много лет назад я использовал подобный подход в одном из приложений, но не потому, что был молод или мне требовались деньги. Такая возможность была необходима пользователям этого приложения. Они хотели избежать ситуаций, ког да сделанные измеЕ1ения невозможно передать в БД, и данные приходится позже вводить заново. Один из сотрудников, скажем, Стив (половину сотрудников той компании звали Стивами), постоянно забывал подтверждать сделанные им изменения. Когда дру гим пользователям не удавалось изменить данные БД, они искали меня, и я искал Стива, на что иногда требовалось довольно много времени. И хотя я объяснял им, что они сами хотели такой функциональности, веселее никому не становилось. ЭЙ, тогда я учился в колледже. Именно в этом возрасте дети экспериментиру ют со штуками типа пессимистического управления блокировками. Я получил хороший урок и никому особо не навредил. Даже Стиву. Вопрос. Что, если мой объект DataSet содержит столбцы с BLOB-данными, а мне требуется передавать обновления? Ответ. Простейшее решение Ч разделить исходный запрос на два, один из ко торых возвращает только поля первичного ключа и поле с BLOB-данными, а дру гой возвращает все прочие поля. Структура запросов, при помощи которых DataAdapter передает изменения, статична, и поэтому в разделе SET запроса, заданного свойству Commandlext объекта UpdateCommand, используются значения всех полей, даже если измененные дан ные содержит только одно поле. Эта, казалось бы, маленькая для большинства за просов помеха (и необходимое зло при передаче обновлений средствами храни мой процедуры) может создать значительную проблему при работе с полями, со держащими BLOB-данные. Почему? Скажем, мы обрабатываем сведения о сотрудниках и в БД есть таблица Emp loyees, содержащая столбцы с именем, идентификационным номером, должнос тью и фотографией сотрудника. В столбце с фотографиями хранится большой объем двоичной информации Ч содержимое JPEG-файлов. Если у вас есть объект DataTable с аналогичными столбцами и вам требуется изменить лишь поле должность отдельной записи, DataAdapter включит в запрос, ГЛАВА 10 Передача обновлений в базу данных обновляющий содержимое записи БД, текущие значения всех полей. Это означа ет, что даже при изменении небольшого поля со строковым типом данных в БД все равно передается двоичное содержимое фотографии сотрудника, Другой способ заключается в том, чтобы разделить данные на отдельные таб лицы (рис. 10-11). На рисунке показаны два объекта DataTable, между которыми определено отношение на основе объекта DataRelation. Родительский объект DataTable включает основные столбцы таблицы Employees Ч EmployeelD, LastName и FirstName. Дочерний DataTable объект содержит столбец Photo с ВЮВ-данны ми, а также столбец EmployeelD, позволяющий поддерживать связь с родительс ким объектом DataTable. Employees LastName FirslName EmployeelD 1 DaDavolio Nancy 2 Fuller Andrew 3 Leverling Janet 4 Peacock Margaret EmployeesPhatos EmployeelD Photo 1 <Двоичное содержимое поля Photo> 2 <Двоичное содержимое поля Photo 3 <Двоичное содержимое поля Photo 4 <Двоичное содержимое поля Photo> Рис. 10-11. Деление содержимого объекта DataTable на основе столбца с BLOB-данными Если на основе этой архитектуры создать объект DataSet и использовать для каждого объекта DataTable отдельный объект DataAdapter, при изменении поля Title генерируется запрос, обновляющий содержимое БД и не включающий поля Photo. Содержимое поля Photo передается в БД только в том случае, если оно бу дет изменено. Конечно, если хранить двоичные данные в файле, а в БД Ч лишь путь к нему, вся эта дискуссия напрасна. Вопрос. Я пытался воспользоваться приводившимся в одном из предыдущих разделов главы фрагментом кода, передающим изменения в таблицу Order Details при помощи объекта CommandBuilder, однако система выдала ошибку <Х Incorrect syntax near the keyword 'Order' (некорректный синтаксис ключевого слова 'Order'). Что не так? Автономная работа с данными: объект DataSet модели ADO.NET 402 Часть III Ответ. Не люблю отвечать вопросом на вопрос, но зачем было добавлять про бел в имя таблицы? Предвижу возгласы Приятель, я определенно рад возможно сти использовать пробелы в именах таблиц и столбцов. Это сильно упростило мне жизнь, однако отвечать на них я не стану. На момент написания этой книги объект CommandBuilder не выполняет запрос к БД, чтобы определить, какие сим волы-разделите л и используются для имен таб лиц и столбцов, содержащих пробелы и другие недопустимые символы или пред ставляющих собой зарезервированные слова. Если вы с помощью объекта Сот mandBuilder генерируете логику обновления для запроса, включающего такие имена таблиц или столбцов, и не определили значения свойств QuotePrefix и QuoteSuffix этого объекта, передать обновления в БД вам не удастся. Что, если вам не хочется задавать значения данных свойств, поскольку ваш код должен нормально рабо тать с разными СУБД? При работе с поставщиком OLE DB.NET Data Provider мож но воспользоваться методом OleDbConnection.GetOleDbSchemaTable и выбрать из БД соответствующие символы-разделители. Я протестировал следующий код: он успешно работает со всеми OLE DB-ноставщиками, взаимодействующими с SQL Server, Oracle и Access: Visual Basic.NET Dim strConn, strSQL As String strConn = "Provider=SQLOLEDB; Data Source=(local)\NetSDK; " & "Initial Catalog=Northwind; Trusted_Connection=Yes; " strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " & _ "FROM [Order Details] WHERE OrderlD = 10503 " & _ "ORDER BY ProductID" Dim en As New OleDbConnection(strConn) Dim da As New 01eDbDataAdapter(strSQL, en) cn.0pen() Dim cb As New OleDbCommandBuilder(da) Dim tblSchema As DataTable tblSchema = cn.Get01eDbSchemaTable{01eDbSchernaGuld.DbInfoLiterals, New ObjectQ {}) cn.CloseO tblSchema,PrimaryKey = New DataColumn{) {tblSchema.ColumnsC'LiteralName")} Dim row As DataRow row = tblSchema.Rows.Flnd("Quote_Prefix") If Not row Is Nothing Then cb.QuotePrefix = row("LiteralValue") End If row = tblSchema.Rows.Find("Quote_Suffix") If Not row Is Nothing Then cb.QuoteSuffix = row("LiteralValue") End If Visual C#.NET string strConn, strSQL; StrConn = "Provider=SQLOLEDB; Data Source=(local)\\NetSDK; " + ГЛАВА 10 Передача обновлений в базу данных "Initial Catalog=Northwind; Trusted_Connection=Yes; "; strSQL = "SELECT OrderlD, ProductID, Quantity, UnitPrice " + "FROM [Order Details] WHERE OrderlD = 10503 " + "ORDER BY ProductID"; OleDbConnection en = new OleDbConnection(strConn); OleDbDataAdapter da = new 01eDbOataAdapter{strSQL, en); cn.OpenO; OleDbCommandBuilder cb = new OleDbCommandBuilder(da); DataTable tblSchema; tblSchema = cn.Get01eDbSchemaTable(01eDbSchemaGuid.DbInfoLiterals, new object[] {}); cn.CloseO; tblSchema.PrimaryKey = new DataColumn[] {tblSchema.Columns["LiteralName"]}; DataRow row; row = tblSchema.Rows,Find("Quote_Prefix"); if (row != null) cb.QuotePreflx = row["LiteralValue"]; row = tblSchema.Rows.Find("Quote_Suffix"); if (row != null) cb.QuoteSuffix = row["LiteralValue"]; Конечно, этих проблем удастся избежать, исключив из имен таблиц и столб цов недопустимые символы. Вопрос. Я передаю в БД новые записи с помощью объекта DataAdapter ADO.NET, и поля новых записей БД содержат не значения по умолчанию, определенные в БД, а значения null. При использовании ADO поля содержали свои значения по умолчанию. В чем дело? Ответ. SQL Server и другие БД позволяют определять значения столбцов БД по умолчанию. Как я говорил в главе б, свойство DefaultValue объекта DataColumn ADO.NET не обеспечивает точного соответствия данной функциональности, и поэтому ADO.NET не генерирует определенные в БД значения пг> умолчанию ав томатически. Туг сказывается и еще один фактор. Если вы выполняете запрос, в котором имя поля опущено или вместо значе ния этого поля указано ключевое слово DEFAULT, БД автоматически сгенерирует для соответствующего поля новой записи его значение по умолчанию. ADO.NET не позволяет опустить имя поля или указать ключевое слово DEFAULT. ADO, предшественница ADO.NET, генерирует динамические обновления отдель ных записей. При передаче обновлений ADO исключала из запросов INSERT поля, значения которых не изменялись. Таким образом, новые записи БД, создаваемые средствами ADO, автоматически содержат значения по умолчанию, а записи, со здаваемые средствами ADO.NET Ч нет. В случае с ADO.NET простейшее решение Ч добавить в приложение код, кото рый при создании новой записи автоматически определял бы значения полей по умолчанию. ГЛАВА Сложные случаи обновления данных 13 главе 10 рассказывалось о передаче изменений в БД с использованием функ ций обновления, предоставляемых объектом DataAdapter. Вы научились генери ровать логику обновления средствами мастера Data Adapter Configuration Wizard и объекта CommandBuilder. Кроме того, теперь вы понимаете структуру SQL-запро сов UPDATE, UNSERT и DELETE, генерируемых этими утилитами для преобразо вания отложенных изменений, хранящихся в объекте DataSet, в изменения содер жимого БД. Примеры главы 10 представляют собой простые случаи обновления данных: все попытки обновления завершались успешно и после передачи изменений не требовалось заново выбирать из БД какую-либо информацию. Таблицы, задейство ванные во фрагментах кода, не содержат столбцов с генерируемыми сервером данными (например, значения автоинкремента или значения типа timestamp), и изменения всегда передаются в одну таблицу, Тем не менее в приложениях, ско рее всего, реализуются более сложные случаи обновления данных, Так, при работе с таблицами, включающими столбцы с автоинкрементом, ве роятно, вам потребуется получать значения автоинкремента, генерируемые БД для новых записей. В других случаях вам может понадобиться повторно выбрать со держимое записи после передачи обновлений в БД, например при оптимистич ном управлении параллелизмом на основе полей типа timestamp. Чем сложнее приложение, тем сложнее возможные ситуации обновления дан ных. Например, непросто передать изменения иерархичных данных. С многоуров невыми приложениями связаны проблемы иного рода Ч например, передача объектов DataSet, содержащих только необходимые для передачи обновлений в БД данные, и повторная интеграция только что выбранных значений типа time stamp и автоинкремента в имеющийся объект DataSet. Сложные случаи обновления данных ГЛАВА Попытки оптимистичного обновления не всегда завершаются успешно. Так, они завершается ошибкой, если другой пользователь успел изменить нужные вам за писи. Рекомендую вам научиться изящно разрешать такие проблемы вместо того, чтобы пытаться любой ценой избежать их возникновения, В этой главе подробно рассматриваются эти и другие сложные случаи обнов ления. Однако здесь я несколько изменил манеру изложения. Предыдущие главы изобилуют фрагментами кода, которые можно копировать и вставлять в консольные приложения и затем, ничего не изменяя, успешно их выполнять. Создать похо жий полнофункциональный кода для сложных случаев обновления, обсуждаемых в этой главе, нереально. Поэтому здесь показаны небольшие фрагменты кода приложений, записанных на прилагаемом к книге компакт-диске. Обновление отображаемого содержимого записи после передачи изменений В главе 10 рассказывалось о создании и использовании запросов INSERT, UPDATE и DELETE для передачи изменений в БД. По сути, эти запросы Ч улица с односто ронним движением. БД изменяет содержимое записи на основе переданной в запросе информации, И хотя она сообщает о числе обработанных запросом за писей, новое содержимое измененных записей БД не возвращает, Иногда требуется, чтобы передача обновлений в БД походила на улицу с двух сторонним движением. Как рассказывалось в главе 10, предотвратить ненамеренную перезапись одним пользователем изменений другого пользователя удается при помощи типа данных timestamp Microsoft SQL Server. Когда содержимое записи изменяется. БД генерирует для нее новое значение поля типа timestamp. Рассмот рим следующую ситуацию. Ваше приложение отображает содержимое заказа. Пользователь добавляет в заказ новый товар, соответствующий одной из записей вашей таблицы, аналогич ной таблице OrderDetails БД Northwind. Тем не менее в таблице есть поле типа timestamp, значение которого используется в логике обновления. Когда пользо ватель добавляет новый заказанный товар в БД. та генерирует для новой записи новое значение поля timestamp. Здесь все нормально. Теперь предположим, что клиентская часть приложения использует Windows, а не Web-интерфейс. После того как пользователь передаст в БД новый заказан ный товар, приложение по-прежнему отображает содержимое заказа. Что, если пользователю требуется изменить запись об этом же товаре и снова передать изменение в БД? Как вы помните, логика обновления объекта DataAdapter использует значение поля timestamp в свойстве UpdateCommand. При вставке новой записи БД гене рирует для нее новое значение поля timestamp. Если этого значения не окажется в объекте DataRow, попытка обновления завершится неудачно. Аналогичная проблема возникает, если вы, изменив запись, передадите это изменение в БД, и затем снова попытаетесь изменить эту же запись. При внесе нии первого изменения БД сгенерирует новое значение поля timestamp. Если это значение не передать каким-либо способов в объект DataRow, вторая попытка обновления завершится ошибкой. Автономная работа с данными: объект DataSet модели ADO.NET 406 Часть III Получение новых значений поля timestamp после передачи обновления Предположим, исходный запрос для выборки данных из таблицы Order Details выглядит так: SELECT OrderlD, ProductID, Quantity, UnitPrice, TimestampColumn FROM [Order Details] WHERE OrderlD = ? Как вы знаете из главы 10. передавать обновления в таблицу можно средства ми следующего параметризованного запроса: UPDATE [Order Details] SET OrderlD = ?, ProductID = ?, Quantity = ?, UnitPrice = ? WHERE OrderlD = ? AND ProductID = ? AND TSCol = ? А получить новое значение поля timestamp, сгенерированное БД, таким образом: SELECT TSCol FROM [Order Details] WHERE OrderlD = ? AND ProductID = ? Конечно, можно выполнить этот запрос вручную после передачи обновления. Но что, если нужно передать группу изменений? Посмотрим, как посредством этого запроса и объектной модели ADO.NET ав томатически получать значения автоинкремента после передачи обновлений. Выборка данных при помощи пакетных запросов после передачи обновления В главе 5 рассказывалось о выборке результатов пакетного запроса, аналогично го следующему, с помощью объекта DataAdapter. SELECT CustornerlD, CompanyName, ContactName, Phone FROM Customers; SELECT OrderlD, CustomerID, EmployeelD, OrderDate FROM Orders Кроме того, пакетные запросы позволяют выбирать данные после передачи обновлений. Пакетный запрос разрешает объединить запрос UPDATE и запрос, выбирающий новое значение timestamp. Задайте свойству CommandText коман ды UpdateCommand объекта DataAdapter такой запрос: UPDATE [Order Details] SET OrderlD = ?, ProductID = ?, Quantity = ?, UnitPrice = ? WHERE OrderlD = ? AND ProductID = ? AND TSCol = ?; SELECT TSCol FROM [Order Details] WHERE OrderlD = ? AND ProductID = ? Примечание Не все БД поддерживают пакетные запросы, возвращающие за писи. Например, в отличие от Microsoft SQL Server, в БД Oracle и Microsoft Access поддержки таких запросов нет. Подробнее о поддержке данной функциональности Ч в документации БД. Передавая отложенное изменение, объект DataAdapter также выполняет по следующий запрос SELECT и помещает результаты в измененный объект DataRow. DataRow содержит новое значение поля timestamp, и следовательно, запись раз решается снова изменить и успешно передать эти изменения в БД. ГЛАВА 11 Сложные случаи обновления данных Свойство UpdatedDataSource объекта Command Объект DatoAdapter передает обновления в БД с использованием объектов Com mand, хранящихся в свойствах InsertCommand, UpdateCommand и DeleteCommand. Но как DataAdapter узнает о необходимости просмотреть результаты работы Insert Command или UpdateCommand''. Все дело в свойстве UpdatedRowSource объекта Command, Данное свойство принимает значения из перечисления UpdateRowSource (табл. 11-1). По умолчанию объект Command выбирает для измененной записи новые данные, проверяя параметры вывода и первую возвращенную запросом запись. Таблица 11-1. Элементы перечисления UpdateRowSource Константа Значение Описание 3 Указывает объекту Command выбрать для записи но Both вые данные, используя параметры вывода и первую возвращенную запись. Значение по умолчанию Указывает объекту Command выбрать для записи но FirstRetumedRecord вые данные с использованием первой возвращенной записи 0 Указывает объекту Command по завершении выпол None нения не выбирать для записи новые данные 1 Указывает объекту Command выбрать для записи но OutputParameters вые данные с использованием параметров вывода Чтобы повысить производительность обновления, достаточно задать свойству UpdatedDataSource требуемое значение. Я провел для данного свойства малень кий неформальный тест, выбирая содержимое таблицы Order Details и вставляя его в новую таблицу с той же структурой. Для начала я не менял значение свой ства по умолчанию UpdatedDataSource, Both. В тесте объект InsertCommand просто передавал новую запись. Он не включа ет пакетного запроса для изменения содержимого записи после передачи обнов ления, и поэтому задавать свойству UpdatedDataSource объекта InsertCommand значение, отличное от None, не требуется. При таком значении производитель ность оказывалась на 1Ч2% выше. Если свойству UpdatedDataSource задано значение по умолчанию, Both, объект Command проверяет параметры вывода и первую, возвращенную им запись, даже если он вообще не возвращает данных. Задавайте свойству UpdatedDataSource объектов Command нужное значение, иначе случится небольшое, но совершенно неоправданное падение производительности. Выборка новых данных с помощью параметров вывода Свойство UpdatedDataSource позволяет также указать, что новые данные требует ся выбрать с помощью параметров вывода. Создадим хранимую процедуру, которая средствами аналогичного запроса UPDATE изменяет запись таблицы Order Details и возвращает новое значение типа timestamp с использованием параметра вывода: 408 Часть III Автономная работа с данными: объект DataSet модели ADO.NET CREATE PROCEDURE spUpdateDetail (@OrderIDNew int, @ProductIDNew int, @QuantityNew smallint, laUnitPriceNew money, @OrderIDOrig int, @ProductIDOrig int, @TSCol timestamp OUTPUT) AS UPDATE [Order Details] SET OrderlD = @OrderIDNew, ProductlD = @ProductIDNew, Quantity = @QuantityNew, UnitPrice = @UnitPriceNew WHERE OrderlD = @OrderIDOrig AND ProductlD = ^ProductlDOrig AND TSCol = ffTSCol; IF @@ROWCOUNT = SELECT @TSCol = TSCol FROM [Order Details] WHERE OrderlD = @OrderIDNew AND ProductlD = @OrderIDNew Все, что осталось сделать, Ч задать свойству CommandText объекта UpdateCom mandхранимую процедуру, создать набор Parameters, объекта Commands задать свойству UpdatedRowSource объекта Command значение QutputParameters али Both. Этот процесс более эффективен, чем возврат данных средствами запроса SELECT. Проверить значение параметра гораздо быстрее, чем выбрать результаты запро са. Кроме того, БД типа Oracle поддерживают параметры вывода, но не поддер живают пакетные запросы, возвращающие записи. Выборка данных с помощью события RowUpdated объекта DataAdapter после передачи обновления Некоторые БД, например Microsoft Access, не поддерживают пакетные запросы, возвращающие записи, и не поддерживают параметры вывода хранимых проце дур. При работе с такими БД два описанных выше способа выборки данных пос ле передачи обновления становятся недоступными. Тем не менее есть еще один способ, не связанный с переходом на БД, поддерживающую нужную функциональ ность. У объекта DataAdapter есть два события, наступающих при передаче измене ний, кэшированных в объекте DataRow Ч RowUpdating и RowUpdated. Как следует из их имен, первое событие наступает непосредственно перед передачей изме нения, а последнее Ч сразу после передачи. Если передаются изменения нескольких записей, события RowUpdating и Ron-Up dated наступают для каждой из них. Добавив код регистрации событий, вы уви дите в журнале следующие записи: RowUpdating event fired for row # RowUpdated event fired for row # RowUpdating event fired for row # RowUpdated event fired for row RowUpdating event fired for row RowUpdated event fired for row С помощью события RowUpdated удается выбрать новое значение, генерируе мое БД для обновленной записи. Следующий фрагмент кода демонстрирует ра бот}- с событием RowUpdated. Для краткости он ссылается на выдуманные функ Сложные случаи обновления данных ГЛАВА ции, создающие объекты DataTable, DataAdapter и Command для выборки нового значения timestamp. Заметьте: в обработчике события RotvUpdated код проверяет успешность об новления и то, что изменение записи яааяется обновлением или вставкой. Понятно, что при удалении записи БД потребности в запросе для получения нового значе ния timestamp не возникнет. Кроме того, поскольку объект Command, выбираю щий новое значение timestamp, иыбирает только одно значение, код получает его с помощью метода ExecuteScalar. Visual Basic.NET Dim da As OleDbDataAdapter = CreateMyDataAdapterQ Dim cmdGetNewTS As OleDbCommand = CreateGetNewTSCommandO AddHandler da.RowUpdated, AddressOf HandleRowUpdated Dim tbl As DataTable = CreateMyDataTableO da.Fill(tbl) da.Update(tbl) Private Sub HandleRowUpdated(ByVal sender As Object, ByVal e As QleDbRowUpdatedEventArgs) If e.Status = UpdateStatus.Continue AndAlso (e.StatementType = StatementType.Insert OrElse e.StatementType = StatementType.Update) Then cmdGetNewTS.Parameters("!SOrderID"). Value = e.Row("OrderID") cmdGetNewTS.Parameters("@ProductID").Value = e,Row("ProductID") e.How("TSCol") = CTypeCcmdGetNewTS.ExecuteScalar, ByteO) e.Row.AcceptChanges() End If End Sub Visual C#.NET OleDbDataAdapter da = CreateMyDataAdapter{); OleDbCommand cmdGetNewTS = CreateGetNewTSCommandO; da.RowUpdated += new OleDbRowUpdatedEventHandler(HandleRowUpdated); DataTable tbl = CreateHyDataTableO; da.Fill(tbl); da.Update(tbl); private void HandleRowUpdated(object sender, OleDbRowUpdatedEventArgs e) { if ((e.Status == UpdateStatus.Continue) 8A ((e.StatementType == StatementType.Insert) || (e.StatementType == StatementType,Update))) \ cmdGetNewTS.Parameters["@OrderID"].Value = e.Row["OrderID"]; cmdGetNewTS.Parameters["@ProductID"].Value = e.Row["ProductID"]; e.Row["TSCol"] = (byte[]) cmdGetNewTS.ExecuteScalarO; 410 Автономная работа с данными: объект DataSet модели AOO.NET Часть III е.Row.AcceptChangesC); I Получив новое значение timestamp и задав его соответствующему полю объекта DataRow, код с помощью метода DataRowAcceptChanges подтверждает это изменение. Без вызова метода AcceptCbanges объект DataRotr кэшировал бы изменение, что бы его позже передать в БД. Это совсем не то, для чего мы получали новое значе ние timestamp. Вызывая метод AcceptChanges, мы указываем ADO.NET просто при нять хранящиеся в DataRow изменения. Данный метод очень гибок, поскольку работает в любой БД; тем не менее за гибкость приходится платить производительностью. В выполненных мной тестах производительность выборки значений timestamp с использованием событий оказалась на 35% ниже производительности пакетных запросов и на 50% ниже производительности параметров вывода хранимых процедур. Приложение Timestamp На прилагаемом к книге компакт-диске записано приложение Timestamp (на Visual Basic.NET и на Visual С*.NET), иллюстрирующее все три способа выборки дан ных после передачи обновления (рис. 11-1). Приложение демонстрирует исходную проблему: если не выбирать после пе редачи изменений в БД новые значения timestamp, последующие попытки обнов ления этих же записей завершатся неудачей. в Ifi-fresfningafter update Id reirieve new timestamp values Summary f his sample demonslrales various '"ays Ы refreshing dala in a row alter submitting updates. When you click any ol the buttons below, the code will run a test lhal adds a new row to a table- that :ontains a timestamp column. The lest wil then modify the contents ol the DataRow and try to submil lhal change to the database, li i the update succeeds, the test w В arched Gueiy por Update succeeded! Ddete succeeded! Рис. 11-1. Приложение Timestamp Выборка новых значений автоинкремента SQL Server, Access, Sybase и другие БД используют столбцы с автоинкрементом, также называемые столбцами IDENTITY (IDENTITY columns). Вы можете вставить в таб ГЛАВА 11 Сложные случаи обновления данных лицу новую запись, и БД сгенерирует для нее новое значение поля с автоинкре ментом. Многие таблицы БД Northwind, например Employees. Orders и Products, используют столбцы с автоинкрементом в качестве первичного ключа. Почему работа со столбцами с автоинкрементом считается сложным случаем обновления? Вы можете передать в БД новую запись, но БД сгенерирует для нее новое значение поля с автоинкрементом. Это означает, что после передачи но вой записи в БД значение ее поля с автоинкрементом вам неизвестно. Вообще говоря, вам требуется знать значение первичного ключа записей, Так как же средствами объектной модели ADO.NET получить для записи новое значение автоинкремента? Работа с SQL Server Представьте на минуту, что вы передаете изменения в БД не с помощью объекта DataAdapter, а разрабатываете для этой цели собственные запросы, При работе со сведениями о заказах из БД Northwind получить данные из таб лицы Orders можно средствами следующего запроса: SELECT OrderlD, CustomerlD, EmployeelO, OrderDate FROM Orders Для вставки новой записи в таблицу используйте такой запрос: INSERT INTO Orders (CustomerlD, EmployeelD, OrderDate) VALUES (?, ?, ?) Получить значение автоинкремента, сгенерированное БД для новой записи, можно с помощью показанного ниже запроса: SELECT etIDENTITY Примечание Почему можно, а не нужно? Подробнее об этом Ч в разделе этой главы, посвященном сравнению функции @@IDENTITY и ключево го слова SCOPEJDENTITY. Этот запрос Ч ключ к получению значения автоинкремента. Данный запрос используется в объектной модели ADO.NET так же, как и запрос из предыдущего примера, возвращающий значение timestamp. Можно изменить значение свойства CommandText команды InsertCommand объекта DataAdapter и выполнять запрос SELECT @@IDENTITY после каждой вставки: INSERT INTO Orders (CustomerlD, EmployeelD, OrderDate) VALUES (?, ?, ?); SELECT л^IDENTITY AS OrderlD Заметьте: SELECT @@IDENTITY запрос включает псевдоним, указывающий объек ту Command, в какой столбец поместить результаты запроса. Как и при выборке новых значений timestamp, вернуть новое значение авто инкремента удается средствами параметра вывода хранимой процедуры: CREATE PROCEDURE spOrdersInsert (@OrderID int OUTPUT, ^CustomerlD nchar(5), @EmployeeID int, eOrderDate datetime) AS INSERT INTO Orders (CustomerlD, EmployeelD, OrderDate) Автономная работа с данными: объект DataSet модели ADO.NET 412 Часть III VALUES (@CustomerID, @EmployeeID, @OrderDate) SELECT @OrderID = ййIDENTITY Наконец, можно воспользоваться событием RowUpdated объекта DataAdapter и выполнить запрос, выбирающий новое значение автоинкремента: Visual Basic.NET Dint da As OleDbDataAdapter = CreateMyDataAdapterQ Dim en As OleDbConnection = da.SelectCommand.Connection Dim cmdGetldentity As New 01eDbCommand("SELECT ^IDENTITY", en) AddHandler da.RowUpdated, AddressOf HandleRowUpdated Dim tbl As DataTable = CreateHyDataTableO da.Fill(tbl) da.Update(tbl) Private Sub HandleRowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs) If e.Status = UpdateStatus.Continue AndAlso e.StatementType = StatementType.Insert Then e.Row("OrderID") = CType(cmdGetIdentity.ExecuteScalar, Integer) e. Row.AcceptChangesO End If End Sub Visual C#.NET OleDbDataAdapter da = CreateHyDataAdapterO; OleDbConnection en = da.SelectCommand.Connection; OleDbCommand cmdGetldentity = new OleDbCommandC"SELECT ^IDENTITY", on); da.RowUpdated += new OleDbRowUpdatedEventHandler(HandleRowUpdated); DataTable tbl = CreateMyDataTableO; da.Fill(tbl); da.Update(tbl); private void HandleRowUpdated(object sender, OleDbRowUpdatedEventArgs e) if ((e.Status == UpdateStatus.Continue) && ((e.StatementType == StatementType.Insert)) e.Row["OrderID"] = (int) cmdGetldentity.ExecuteScalarO; e.Row.AcceptChanges(); : У этого фрагмента кода и фрагмента, выбиравшего новые значения timestamp с использованием события RowUpdated, два отличия. Первое, самое очевидное Ч то, что для выборки данных выполняется другой запрос. Второе отличие Ч в производительности. Какой самый быстрый способ выб рать новые значения автоинкремента? Полученные мной при тестах величины ГЛАВА 11 Сложные случаи обновления данных производительности соответствовали значениям, полученным при тестах на вы борку значений timestamp. Параметры вывода хранимых процедур обеспечива ют максимальный, пакетные запросы Ч средний, а использование события Row Updated Ч наименьший уровень производительности. Функция @@ IDENTITY и ключевое слово SCOPEJDENTITY Запрос SELECT @@IDENTITY возвращает последнее значение IDENTITY, сгенери рованное на данном соединении. Это означает, что работа других пользователей па других соединениях не влияет на результаты вашего запроса. Однако из этого не следует, что вы получите ожидаемое значение. Администраторы БД зачастую отслеживают изменения содержимого БД с по мощью собственных таблиц аудита. Для регистрации изменений обычно приме няются триггеры и хранимые процедуры (рис. 11-2), Рис. 11-2. Отслеживание изменений с помощью таблиц аудита Почему посреди дискуссии о выборке значений автоинкремента я углубился в обсуждение журналов аудита и триггеров? Предположим, что таблица аудита, на которую ссылается триггер на рис, 11-2, включает столбец с автоинкрементом. Вставив в таблицу Orders новую запись и выполнив запрос SELECT @@IDENTITY, вы получите значение автоинкремента, сгенерированное триггером для новой записи таблицы аудита. Помните: запрос SELECT @@IDENTITY возвращает последнее значение автоин кремента, сгенерированное на данном соединении. Дад решения этой проблемы в SQL Server 2000 реализован новый способ вы борки значений автоинкремента: ключевое слово SCOPEIDENTITY(). Выполнив в описанной ситуации запрос SELECT SCOPE_IDENTITY(), вы получите значение автоинкремента, сгенерированное для новой записи таблицы Orders. Если вы работаете с SQL Server или Microsoft Desktop Engine (MSDE) версии или более поздней, используйте SCOPEJDENTITY вместо ййIDENTITY. Однако из этого правила есть одно небольшое исключение. Если вы вставляете новую запись средствами хранимой процедуры и хотите после ее вызова получить значение автоинкремента, SCOPE_IDENTITY() вернет Null. Как я и говорил, это небольшое Автономная работа с данными: объект DataSet модели ADO.NET 414 Часть III исключение. Если вы вставляете новые записи с помощью хранимой процедуры и вам требуется получить новое значение автоинкремента, воспользуйтесь пара метром вывода. Подробнее о различиях @@IDENTITY и SCOPEJDENTITY - в SQL Server Books Online. Работа с Access При работе с БД Access новые значения автоинкремента также получают посред ством запроса SELECT @@IDENTITY. Эта возможность реализована в 4 версии по ставщика OLE DB Jet Provider и поддерживается только БД Access версии 2000 или более поздней. Как и в SQL Server, в Access запрос SELECT @@IDENTITY возвраща ет последнее значение автоинкремента, сгенерированное на данном соединении. БД Access не поддерживают параметры вывода QueryDef Ч хранимых запро сов, аналогичных представлениям и хранимым процедурам. Поставщик OLE DB Jet Provider не предоставляет поддержки пакетных запросов. Таким образом, един ственный способ выбрать новые значения автоинкремента Ч воспользоваться событием RowUpdated объекта DataAdapter, как рассказывалось ранее. Работа с последовательностями Oracle БД Oracle не поддерживают столбцы с автоинкрементом, зато предоставляют ана логичную конструкцию Ч последовательность (sequence). В SQL Server вы поме чаете столбец как столбец с автоинкрементом, и SQL Server автоматически гене рирует новые значения соответствующего поля для вставляемых записей. После довательность Oracle ведет себя несколько иначе. Обычно последовательность создают, чтобы генерировать новые значения для столбца БД, однако прямой связи между последовательностью и таблицей ил столбцом нет. Последовательность Oracle Ч это объект, наподобие таблицы или хранимой процедуры. Приведенный ниже запрос создает последовательность Oracle: CREATE SEQUENCE MySequence При создании последовательности Oracle можно указать ряд параметров, в ча стности, ее минимальное и максимальное значения. Примечание Я не считаю себя экспертом по последовательностям Oracle. Я умею создавать и использовать их в простых запросах INSERT и SELECT, но не более того. Подробнее о последовательностях Ч в документации Oracle. Известно два способа использования последовательности. Во-первых, на нее можно сослаться в запросе INSERT INSERT INTO NyTable (ID, Other-Column) VALUES (MySequence.NEXTVAL, 'New Row') При каждом выполнении запроса последовательность будет возвращать новое значение. После вставки новой записи выполняют запрос к последовательности и опре деляют последнее использованное значение: ГЛАВА 11 Сложные случаи обновления данных SELECT MySequence.CURRVAL FROM DUAL Как и в случае запроса SELECT @@IDENTITY в БД Access и SQL Server, работа других пользователей, вставляющих новые записи с применением этой же пос ледовательности, на результаты данного запроса не влияет. Так как вставить новые значения последовательности в объекты DataRow? Oracle не поддерживает пакетные запросы, возвращающие данные, и поэтому в свойстве CommandText команды InsertCommand нельзя использовать оператор Sequence.CURR VAL Тем не менее разрешается выполнить в обработчике события RowUpdated объекта DataAdapter такой запрос: Visual Basic.NET Dim da As OleDbDataAdapter = CreateMyDataAdapter() Dim en As OleDbConnection = da.SelectCommand,Connection Dim strSQL As String = "SELECT HySequence.CURRVAL FROM DUAL" Dim cmdGetSequence As New 01eDbCommand(strSQL, en) AddHandler da.RowUpdated, AddressOf HandleRowUpdated Dim tbl As DataTable = CreateMyDataTableO da.Fill(tbl) ч da.Update(tbl) Private Sub HandleRowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs) If e.Status = UpdateStatus.Continue AndAlso e.StatementType = StatementType.Insert Then e.Row("OrderID") = CType(cmdGetSequence.ExecuteScalar, Integer) e.Row.AcceptChangesC) End If End Sub Visual C#.NET OleDbDataAdapter da = CraateHyDataAdapterO; OleDbConnection en = da,SelectCommand.Connection; string StrSQL = "SELECT MySequence.CURRVAL FROM DUAL"; OleDbCommand cmdGetSequence = new 01eDbCommand(strSQL, en); da.RowUpdated += new OleDbRowllpdatedEventHandler(HandleRowUpdated); DataTable tbl = CreateHyDataTableO; da.Fill(tbl); da.Update(tbl); private void HandleRowUpdated(object sender, OleDbRowUpdatedEventArgs e) { if ((e.Status == UpdateStatus.Continue) && Це. StatementType == StatementType.Insert)) { e.Row["OrderID"] = (int) cmdGetSequence.ExecuteScalarO; Автономная работа с данными: объект DataSet модели ADO.NET 416 Часть lit e.Row.AcceptChangesO; } I Вам не надо использовать в запросе INSERT INTO команду Sequence.NEXTVAL Обратиться к последовательности разрешается перед выполнением запроси INSERT INTO. Следующий фрагмент кода создает новую процедуру, которая выполняет запрос к последовательности для получения нового значения и помещает это значение в переменную. Затем процедура использует это значение для вставки новой записи при помощи запроса INSERT INTO. CREATE PROCEDURE MyStoredProc (pOtherCol IN VARCHAR2, pID OUT NUMBER) IS BEGIN SELECT MySequence.NE)CTVAL INTO pID FROM DUAL; INSERT INTO MyTable (ID, OtherCol) VALUES (pID, pOtherCol); END; Хранимая процедура возвращает новое значение последовательности с исполь зованием параметра вывода. Такую процедуру следует задать свойству InsertCom mand объекта DataAdapter, чтобы вставлять новые записи. Если связать параметр вывода с соответствующим полем объекта DataRow, DataRow получит новое зна чение сразу же после передачи новой записи и БД. Примечание Помните: прямой связи между последовательностью и столбцом таблицы нет. Использовать последовательность при вставке новой записи в таблицу не требуется. Если пользователи вставляют новые записи без ссылок на последовательность, та генерирует новые значения, уже име ющиеся в БД. Во избежание проблем убедитесь, что единственный спо соб вставить новую запись в таблицу Ч вызвать хранимую процедуру, ссылающуюся на последовательность. Генерирование фиктивных значений для объектов DataColumn последовательности Последовательности не являются столбцами с автоинкрементом, но если задать свойству Autolncrement соответствующих объектов DataColumn значение True, можно заставить ADO.NET генерировать для новых записей фиктивные значения. Задавать значение свойства придется вручную. Прямой связи между последо вательностью и таблицей нет. Если получить информацию схемы БД с помо щью метода DataAdapterfillSche та или средствами мастера Data Adapter Con figuration Wizard, ADO.NET не узнает, что столбец таблицы сопоставлен с по следовательностью. Аналогичные проблемы возникают при генерации логики обновления для объектов DataAdapter. Объект CommandBuilder и мастер Data Adapter Configuration Wizard не узнают, что в логике, заданной свойству InsertCommand, нужно опус тить соответствующий столбец. Если вы собираетесь создавать логику обновле Сложные случаи обновления данных ГЛАВА ния при помощи указанных средств, вам потребуется внести в нее незначитель ные изменения. Может показаться, что я критикую эти утилиты. На самом деле это не так. Ин формации схемы, которая связывала бы столбец таблицы и последовательность, не существует, и поэтому данные утилиты делают все возможное, чтобы создать подходящий код. Тот факт, что поле, косвенно связанное с последовательностью Oracle, разре шено обрабатывать как поле с автоинкрементом и что можно управлять поряд ком выборки новых значений последовательности в объект DataSet Ч подтверж дение мощи, предоставляемой ADO.NET разработчикам. В предыдущих моделях доступа к данным Microsoft такая функциональность отсутствует. Приложение, выбирающее значения автоинкремента Я описал несколько ситуаций, связанных с выборкой новых значений автоинк ремента и последовательности в объект DataSet. Для получения новых значений автоинкремента SQL Server используют пакетные запросы, параметры вывода хранимых процедур и события RoivUpdated объекта DataAdapter, для новых зна чений автоинкремента Access Ч события Ron-Updated объекта DataAdapter, для новых значений последовательности Oracle Ч параметры вывода хранимых про цедур или события RowUpdated объекта DataAdapter. Выбор велик. На прилагаемом к книге компакт-диске записано приложение (на Visual Basic,NET и Visual С*.NET), которое поможет вам разобраться во всем этом, демонст рируя на практике каждую ситуацию и использование каждого варианта выбор ки значений. На рис. 11-3 показана одна из версий этого приложения, Autolncre mentSql. Кроме того, на компакт-диске есть примеры, выбирающие значения ав тоинкремента из БД Access, а также значения последовательности из БД Oracle. * iMriiiving auto-increment waltiss from SQL Server Summary [this sample demonstrate various ways of retrieving newly generated auto-increment values Irom a SQL iServer database When you click any of the buttons below. I he codswil run a test that adds new rows to a DalaTabls |and then submits those new rows to your database The lest will display the contents of the auto-inciement column for each row in the DalaTabteboth ^before and after submitting the new rows to your database. Each lest re-sets the contents of Ihe lest table ki your database and inserts two initial lows As a result, fyoui new rows should have auto-inciement values of 3.4. and 5. Resutts: Relrievng new aulo-hcrement values through stored procedure output parameter ! iBefore submitting new rows to database: Batched QueryFa j First new row ID -- Second new row ID-- Third new row ID = - Alter submitting new tows lo database Fhstnew row ID - Second new row ID = Third new row ID = Рис. 11-3. Приложение AutoIncrementSql 41 8 Часть III Автономная работа с данными: объект DataSet модели ADO.NET Использование ключевого слова NOCOUNT SQL Server Многие администраторы БД добавляют в триггеры и/или хранимые процедуры логику для отслеживания выполняемых приложениями запросов. Хранимая про цедура, вставляющая новую запись, может выглядеть так: CREATE PROCEDURE spQrderlnsert (зOrderID int OUTPUT, SCustomerlD nchar(5), оEmployee3D int, ^OrderDate datetime) AS INSERT INTO Orders (CustomerlD, EmployeelD, OrderDate) VALUES (@CustomerID, йEmployeelD, @OrderDate) SELECT @OrderID = SCOPE_IDENTI7Y() INSERT INTO OrdersLog (TypeOfChange, DateOfChange) VALUES (OrderID + ' added', GetDateO) RETURN Ее задают свойству InsertCommand объекта DataAdapter, чтобы успешно пере дать изменения. Тем не менее передача обновлений средствами такой же проце дуры вызовет проблемы. Что еще хуже, эти проблемы могут проявить себя толь ко после развертывания приложения. Чтобы разобраться в этом, рассмотрим следующую процедуру обновления, включающую аналогичный код ведения журнала: