WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 3 | 4 || 6 |

«том1 а л ь м а н а х программиста Тематический сборник материалов MSDN» Library и MSDN» Magazine Microsoft ADO.NET ...»

-- [ Страница 5 ] --

Name, SqlDbType, Size, Precision, Scale и Direction. Его конструктор пере гружен несколькими вариантами, чтобы вы могли задавать Name, SqlDb Type, Size или комбинацию этих свойств в зависимости от того, в чем именно проявляются различия между параметрами. Если вам нужно ука зать направление (direction), точность (precision) или масштаб (scale) па раметра, вы должны вручную настроить свойства Direction, Precision и Scale. Несколько примеров использования SqlParameterAttribute показано на рис. 7.

Рис. 7. Применение SqiParameterAttribute SqlCofBfflandHethc^(Goffl«andType.StorectProce6ure} } public static OataSet GetCustowrsC E NonCoaiiBand'Paraffleter ] SqlConrtection connection) {... $ ESqlCo™af!dHett№d{CoiranandType.StoredProcedure) ] public static SataSet GetCustomersByStateC t HofiCoRimandPararfteter 3 Sql Connection connection,.

[ SqlParamter(2) 1 string state) {... } i SqlComfftandNethQd( СошапйТуре. Stored? rocedu re ) public static DataSet GetCu atoms rBy!d( E Мол Command-Parameter 3 SqlConrveetion connection, int customerld). StoredProcedure) public static int AddQrderltesK E NonOommandParameter 3 SqlConnection connection, ;

{ SqlParameterC'PartNr", 20) ] string partHuiaber, [ SqlParameterCSqlDbTypa. Decimal, Scale = 9, Precision = 4) ] decimal «sltPrice, Int quantity) {... }. ' ".,- • -r - - 288 Доступ к данным из приложений При проектировании сложного атрибута важно определить, какие свой ства естественнее инициализировать в конструкторе, а какие — устанавли вать явно. Поскольку свойств у SqlParameterAttribute немало, возмож ность инициализации их всех через комбинации перегруженных конструк торов привела бы только к путанице, а исходный код было бы трудно читать. Бот почему я предпочел инициализировать через конструктор лишь самые популярные свойства параметра* — имя, тип данных и раз мер. Помните об этом, разрабатывая собственные атрибуты.

Реализация SqlParameterAttribute требует пояснений. У этого класса име ется шесть свойств только для чтения: IsNameDefined, IsSizeDefined, IsPre cisionDefmed, IsScaleDefined, IsTypeDefined и IsDirectionDefined. SqlPara meterAttribute поддерживает для каждого свойства два состояния: опреде ленное (defined) или неопределенное (undefined). Все состояния по умолчанию инициализируются как неопределенные, а это означает, что имя, размер, точность, масштаб, тип и направление объекта параметра не заданы явным образом и поэтому должны быть установлены на основе контекста. Для SqlCommandGenerator контекстом являются метаданные параметра функции. Неопределенное состояние свойств Name, Size, Preci sion и Scale отражается специфическим значением, допустимым для их типа. Например, имя считается неопределенным, если закрытое поле _name равно null или занято пустой строкой;

в ином случае оно рассмат ривается как определенное. То же правило применяется к размеру, точно сти и масштабу. Однако _рагатТуре и _direction относятся к перечисли мому типу, так что у них нет значения, которое можно было бы безопасно использовать для индикации неопределенного состояния. Поэтому на их состояние указывают отдельные поля: typeDefined и _directionDefined соответственно.

SqlCommandGenerator Именно этот класс в конечном счете принимает метаданные метода, при меняет все переопределения, заданные моими атрибутами, и генерирует готовый к выполнению объект SqlCommand. Его единственный открытый метод GenerateCommand представляет собой более полную реализацию того, что вы уже видели на рис. 5. Как и раньше, второй параметр в Gene rateCommand идентифицирует функцию, на основе метаданных которой следует генерировать команду, — только на этот раз я сделал его необяза тельным. Если вы передаете NULL (или Nothing в Visual Basic.NET), Generate Command автоматически использует метаданные вызвавшей фун кции. Свою работу он начинает с класса StackTrace из пространства имен System.Diagnostics, чтобы инициировать трассировку стека (stack trace).

* Здесь подразумевается объект параметра. — Прим. сост.

Динамическое связывание уровня данных 2S Затем он захватывает метод из предыдущего фрейма стека, передавая его индекс в StackTrace.GetFrame (индекс, равный 0, соответствовал бы само му вызову GenerateCommand):

if (method == null) method = (Methodlnfo) (new StackTrace<).GetFrame(1).GetMethod());

Теперь можно одним махом получить метаданные для вызвавшей функции из фрейма стека. При этом нет никакой необходимости в сложном вызове Type.GetMethod. Черт возьми, даже вызывать MethodBase.GetCurrent Method и то не нужно. Проще некуда! Но два требования вы обязаны со блюдать: вызвавшей функцией должна быть прокеи-функция для коман ды базы данных и она не должна быть конструктором. Последнее требова ние вызвано тремя причинами. Во-первых, это просто бессмысленно, даже если бы было возможно с технической точки зрения. Во-вторых, Attribute Usage в SqlCommandMethodAttribute все равно запрещает применение этого атрибута к конструктору. И в-третьих, хотя StackTrace.GetFrame воз вращает MethodBase (который является надклассом Methodlnfo и абстра гирует методы и конструкторы), GenerateCommand приводит его к Me thodlnfo. Поэтому, если бы конструктор попытался вызвать Generate Command, возникло бы исключение InvalidCastException.

Из-за новой функциональности (распознавания вызвавшей функции), добавленной в генератор, у вас может появиться соблазн всегда передавать NULL во втором параметре, но берегитесь: эта простота в использовании больно бьет по производительности. Прогнав серию тестов на своей маши не, я обнаружил, что максимальное быстродействие дает Type.GetMethod, MethodBase.GetCurrentMethod лишь немного уступает ему, а проход по стеку с помощью StackTrace обойдется вам 12-кратным падением произ водительности. На практике разницей в быстродействии между первыми двумя способами можно пренебречь.

Тем не менее вариант на основе MethodBase.GetCurrentMethod следует выделить как основной — по возможности выбирайте именно его и не пе редавайте NULL. Кстати, если вы подумали, что MethodBase.GetCurrent Method тоже использует какую-то разновидность трассировки стека, вы не ошиблись, но разработчики Framework, похоже, оптимизировали его для частых вызовов. Достаточно посмотреть, сколько раз сама FCL обращает ся к стеку для проверки разрешений (permissions) у вызвавшей функции.

Покончив с этой задачей, GenerateCommand приступает к основной рабо те и проверяет, дополнен ли метод атрибутом SqlCommandMethodAttri bute. (Если нет, проверка заканчивается неудачей, и сообщается, кто вино ват в этом.) Далее GenerateCommand создает объект SqlCommand и ини циализирует его свойства Connection, CommandType и CommandText.

10- 290 Доступ к данным из приложений Настраивая свойство CommandText, он проверяет размер значения в одно именном свойстве атрибута. Если это значение представляет собой пустую строку, в свойство CommandText объекта команды записывается имя ме тода;

в ином случае берется значение из атрибута. Если строка пуста, GenerateCommand делает дополнительную проверку, чтобы убедиться, действительно ли тип команды соответствует хранимой процедуре, так как для параметризованного SQL-запроса бессмысленно использовать имя метода. GenerateCommandParameters, вызываемая следующей, — закрытая функция, предназначенная исключительно для разделения труда. Она от вечает за обработку параметров метода и добавление нужных объектов SqlParameter в объект SqlComraand. Исходный код этой функции содер жит подробные комментарии, поэтому я лишь вкратце опишу алгоритм ее цикла:

• получить следующий параметр метода;

• если у него есть атрибут KonCommandPararaeterAttribute, пропустить его;

• если у него есть атрибут SqlParameterAttribute, использовать значения, определенные в атрибуте, для настройки соответствующих свойств объекта SqlParameter;

• если атрибута SqlParameterAttribute нет, создать временный атрибут через конструктор по умолчанию;

• настроить все аспекты SqlParameter на основе метаданных для этого параметра метода.

Кроме того, в исходном коде присутствуют две контрольные точки (asser tions), срабатывающие, когда число параметров (кроме помеченных атри бутом NonComrnandParameterAttribute), объявленных для метода, не со впадает с количеством значений, переданных в GenerateCommandPara meters. Это полезно в тех случаях, когда объявляешь какой-то параметр метода, но забываешь передать его значение генератору.

Проблема выходных параметров Одно из ограничений, заслуживающих упоминания, связано с направле нием (direction) объекта SqlParameter. Если в атрибуте оно не определено, то выбирается в соответствии с тем, как передается параметр метода. В С# параметр можно передавать по значению (по умолчанию), по ссылке (клю чевое слово ref) или только для возврата (output only) (ключевое слово out), поэтому направление соответственно задается как ParameterDirec tion.Input, ParameterDirection.InputOutput или как ParameterDirection.Out put. В Visual Basic.NET никакой разницы между последними двумя слу чаями нет, и направление задается либо как ParameterDirection.Input (для Динамическое связывание уровня данных параметров с ByVal), либо как ParameterDirection.InputOutput (для пара метров с ByRef).

Задав направление ParameterDirection.Output для параметров, вы сможе те получать нужные значения от хранимой процедуры или параметризо ванного SQL-запроса, но проблема в том, что их нельзя просто так (без дополнительных усилий) передать вызвавшей функции. Это связано с тем, что они возвращаются в GenerateCommand по значению и в итоге не мо гут быть автоматически переданы вызвавшей функции. То же самое (и даже в большей степени) относится к типам значений, которые упаковы ваются (boxed) и копируются из-за того, что массив значений является массивом объектов. Поэтому после выполнения команды вам придется вручную копировать все выходные параметры из объекта SqlParaineter в вызвавшую функцию.

Один пример. Возьмем хранимую процедуру AddAnnouncements из IBuy Spy Portal (рис. 8). Как и любая другая хранимая процедура такого типа, она вставляет строку в таблицу и возвращает в выходном параметре авто матически назначенный идентификатор (auto-assigned identity). На рис, показано, как передать выходной параметр из хранимой процедуры в out параметр вызвавшей Ctt-функции. Хотя SqlCommandGenerator устанавли вает правильное направление для параметров с ключевым словом ref, он не может автоматически передать в них значения, возвращенные храни мой процедурой.

Рис. 8. Вставка строки и возврат identity CREATE PROCEDURE AtWAnnGunceflient nvarchar(IQQ), «Title nvarcHar(150), tHoreLlRk nvarchar{150), @MobileMo relink nvarchar(15Q),.

©ExpireOate OateTime, ^Description nvarchar(20QO), int OUTPUT AS INSERT INTO AnfflOtmceiiBnts ( Module ID, CreatetfByUser, CreatedDate, Title, HoreLink, см. след. стр.

Доступ к данным из приложений Вставка строки и возврат Identity (окинчаше) Рис. 8.

MobileMo relink, ExpireDate, Description ) VALUES @ModuleID, SUserName, aetDeteO, @Mo relink, ©ExpireDate, ^Description SELECT Рис. 9. С#-вызов хранимой процедуры с выходными значениями public static void AddAnnouncefflent( [ NonCoBimandParameter ] SqlConnection connection, int nroduleld* [ SqlPara»eter(1QQ) ] string userName, С SqlParameter(150) ] string title, [ SqlParameter{l50) ] string ntoreLink, С SqiParameter(150) ] string mobileHoreLink, DateTirne expireDate, ' [ SqlParameter(2000) ] string description, out int itemld) = 0;

// запрещает прямое использование локальной, // переменной "itemld" SqlCommand command = SqlGofflfnand6enerator.6enerateCoMand(connection, null, new object{3 { moduleld, userKame, title, raorelink, mobileHoreLink, expireDate, description, itemld });

command. ExecuteNonQueryO;

itemld = (int) command.Parameters["@IteeiID"].Value;

Кстати, та же проблема возникает и при использовании механизма Reflec tion для вызова метода, принимающего параметры по ссылке. На рис. показан метод Swap, вызываемый прокси с применением позднего связы вания (late binding). После того как Swap заканчивает свою работу, Swap Динамическое связывание уровня данных Proxy копирует значения из массива параметров в переменные, выделен ные вызвавшей функцией.

Заодно интересно посмотреть, как с этой ситуацией справляется Visual Basic.NET, потому что он позволяет использовать позднее связывание (в режиме Option Strict Off) по более естественному и элегантному синтак сису, чем С#. Итак, на рис. 11 показана версия того же кода, что и на рис. 10, но написанная на Visual Basic.NET. Функция SwapProxy исчезла, так как все, что нужно для вызова члена по механизму Reflection, Visual Basic.NET делает сам. В конечном счете он вызывает Type.InvokeMember, и это всего лишь еще один способ выполнения Methodlnfо.Invoke.

Рис. 10. Reflection и объекты Parameters, передаваемые по ссылке class Sample public static void Swap(ref int a, ref int tj) int teiap = b;

b = a;

a - temp;

public static void SwapProxyCref int a, ref int b) objectl} parameters = new ob]ect[] { a, b }', typeof(Sample).GetMethod("3wap").InvQke(nuXl, parameters);

a = (int) parameters[0};

Ь = (in-t) paraiaeter&Et];

static void Hain(stringn args) I int a = 1;

Int b * 2;

SwapPraxy(ref a, ref b);

System. Console, WriteLine<"a = IQ>. b = Рис. 1.

1 Применение к Swap позднего связывания в Visual Basic -NET Class Sample Public Sub SwapCByBef a As Integer, Byflef b As Integer) Dim temp As Integer == b, b=a a * temp см. след, стр.

Доступ к данным из приложений (окончание) Рис. 11. Применение к Swap позднего связывания...

End Sub End Class Module Module!

Sub Main<) Difo a As Integer = Dim b As Integer = Dim о As Object * New San*ple<) o.Swap(a, b) System.Console.WriteLine("a = {Q}, b = Ш, a, b) " End Sub End Module Этот код выглядит обманчиво простым, но за кулисами Visual Basic.NET генерирует тот же IL-код, что и С#. Дамп метода Main в том виде, в каком он показывается ILDASM, приведен на рис. 12. Те, кто не хочет лишней головной боли от чтения IL-кода, должны поверить мне на слово: IL-код копирует элементы из временного массива _Vb_X_array_2 обратно в пере менные а и Ь.

Рис. 12. Дамп функции Main в IIDASM.method public static void Hair*() oil managed {.entrypoint.custom instanoe void [mscQrlibJSystem.STAThreadAttribote::.ctor() * ( 01 00 00 00 ) // Code size 114 (0x72) :.naxstack €.locals init ( 0 int32 a, {] [ ] int32 b, [2] object 0, [ ] objeetn _Vto_t_array_2, [4] obJectE] _V&..t_array_1, E5] boolU _Vb_t_array_Q).Шдиаде "f3At2P088-C2SG-HDO-B442-OOA0244A1DD2}'", 4994B45C4-E6E9-11D2-9Q3F-OGC04FA3G2A1J", "100000000-0000-0 000» 0000-OOOOOOOOOOQO)" // Source File "C:\Documents and Settings\atifa\Hy Documents\Visual Studio Projects\vbref\Module1.vb" //000015: Sub MainO |L_0000: пор //000016:

DiiB a As Integer = t //000017:

Idc.i4. IL.0002: StlOG.O см. след. стр.

Динамическое связывание уровня данных (продолжтше) Рис. 12, Дамп функции Main в ILDASM Din b As Integer = //000018:

lde.14. 1ЦКЮЗ:

stloc. IL_GQQ4:

Dim о As Object = SarapleO //00001»:

instance void vbref.Sample: :.ctor<),11.0605: newobj stloc. ILJKKte:

o.Swap(a, b) //000020;

ldloc. ILJJOQb:

Umll IL.OOQc;

idstr "Swap" ILJJOGd:

ldc.14. IL_0012:

newarr IiJH>l3;

Imscorlib]SysterB. Object ILJ)Q18: stloc.s _b_t_array_ IL_001a: Idloc.s _Vb_t_array_ ILJWic : Idc.i4. IL_001d: ldloc. IL_001e: box [msGorlib]System.Int ILJ3C23;

stelern. ref ХЦМ24: Idloc.s _Vb_t_array_ ILJ3026: Idc.i4. ILJ3§27: ldloc. IL_0028: box [mscorlib}System, Int Il_002d;

steleffl. ref IL_002e: Idloc.s _Vb_t_array^ IL_0030: Stloc. : 11.0031: Idloc.s 11.0032: Idnull 11^0033: Idloca. s „Vb^t.array^S IL_0035;

call void [Microsoft.VisualBasiclMierosoft.VisualBasic.Helpers.LateSinding : :UteCall(object, class [mscorllblSystera.Type, string, objectC], striRa[3,boolCl&) IL_003a: пор _Vb_t.array_ IL_003b: Idloc. s It_0t3d: ldc.14. IL_003e;

Idelem.i IL.003f: brfalse.s IL_OQ4a IL_0041 : Idloc. Il_OQ42: ldc.14, IL_0043 : Ideleei. ref tntaa ILJ)Q44: call [Microsoft.Visual&asiclHicrosoft.VisualBasic.Helpers.IntegerType :: FroinObject(object) ILJXMS: stJoc. IL_004a: Idloc.s.Vb IL_.004c: Ide,i4, 1L 004d: Idelem.i см. след. стр.

Доступ к данным из приложений Рис. 12. Дамп функции Main в-ILDASM (окончание} ILJ)Q4e: brfalse.s ILJHffit IL_0050: ldloc. IL_0051: Idc,i4. ILJ)052;

Idelem.ref IL_0053: call int [HierosQft.VisualBasiGJMlerosoft.VIsualBasic,Helpers.IntegerType ::FromQbJect(object) IL_0058: stloc.O s System.Console.WriteLineC'a = {0}, b = {1}", a., b) //000021 :

"a = {0}, b = U)" IL_0059: Idstr It^OOSe: ldloc. CfflscorUb]System.Int ILJ)05f: box 1L,0064: ldloc. tniscorlib3Systent.Int Il_0065: box И„006а: call void [BiscorlibjSystem.Console;

;

WriteLine(string, object, ooject) Il_006f: пор //000022:

//000023: End Sub IL_0070: пор II 0071: ret } // end of method Modulel:;

Nain Вообще говоря, вы могли бы полностью решить проблему выходных пара метров, генерируя код так, как это делает Visual Basic.NET, и используя для этого средства, предлагаемые пространством имен System.Reflec tion.Emit, но это тема для другой статьи.

Обработка NULL-значений О чем я вам еще не рассказал, так это об обработке NULL-значений. Если хранимая процедура допускает NULL-значения в одном или двух парамет рах, в вашем прокси-методе нельзя использовать предопределенные типы вроде int в С# и Integer в Visual Basic.NET. Вместо этого вы должны объя вить свой метод принимающим один из типов значений из пространства имен System.Data.SqlTypes. Допустим, в базе данных Pubs имеется храни мая процедура:

CREATE PROCEDURE [GetEmployeesByJob](rjob_id SMALLINT = NULL) AS SELECT * FROM [employee] WHERE [job_id] = ISNULL(@job_id, [job_id]) Динамическое связывание уровня данных Поскольку параметр @job_id может быть NULL, вам следует объявить свой С#-метод с использованием Sqllntl6 вместо short, как показано в следующем фрагменте кода:

[ SqlCommandMethod (CommandType.StoredProcedure)] public static DataSet GetEmployeesByJob( [ NonCommandParameter ] SqlConnection connection, [ SqlParameter("Job_id") ] Sqllnt16 Jobld} { SqlCommand command = SqlCommandGenerator.GenerateCommand( connection, null, new object[] { jobld });

DataSet dataSet = new DataSetQ;

SqlDataAdapter dataAdapter = new SqlDataAdapter(command);

dataAdapter.Fill(dataSet);

return dataSet;

I А вот как вы должны вызывать метод в тех случаях, когда параметр jobld равен NULL и когда он не равен NULL;

DataSet dataSet;

dataSet = GetEmployeesByJob(connection, new Sqllnt16(5));

dataSet = GetEmployeesByJob(connection, 5);

// неявное приведение // к Sqllnt dataSet = GetEmployeesByJob(connection, SqlInt16.Null);

Второй вызов GetEmployeesByJob возможен только в С#, поскольку Visu al Basic.NET не поддерживает напрямую операторы преобразований. Тем не менее вы вправе сами вызвать Sqllntl6.op_lmplicit, даже если Intelli Sense прячет ее от вас в Visual Studio, правда тогда вызов GetEmployees ByJob получается таким громоздким, что с тем же успехом можно пользо ваться версией конструктора. Вот версия предыдущего кода для Visual Basic.NET:

Dim dataSet As DataSet dataSet = GetEmployeesByJobfconnection, New Sqllnt16{5)) dataSet = GetEmployeesByJob(connection, Sqllnt16.op_lmplicit{5)) dataSet = GetEmployeesByJob(connection, SqlInt16.Hull) Определение пользовательских типов данных АЛЯ параметра через наследование Из всех атрибутов в моей библиотеке только класс Sql Parameter Attribute определен без ключевого слова sealed (или Nonlnheritable в Visual Basic.NET). Это диктуется его структурой — пусть даже в нем нет открытых или защищенных членов, которые могли бы быть переопределены каким нибудь подклассом. Обычно атрибуты предназначены для хранения лишь Доступ к данным из приложений дополнительных метаданных и не несут в себе никакой функциональнос ти, которая выходила бы за рамки того, что определено классом Attribute;

поэтому вы должны запечатывать (seal) их. Помимо всего прочего, запе чатывание еще и ускоряет поиск атрибутов исполняющей средой. В слу чае SqlParameterAttribute я оставил атрибут открытым для специализации через наследование, что позволяет вам создавать собственные пользова тельские типы (user-defined types), уменьшающие вероятность ошибок в программе и упрощающие ее модификацию. Здесь полная аналогия с при менением пользовательских типов в SQL Server.

Допустим, в нескольких таблицах базы данных вы храните адреса элект ронной почты. Если такой адрес определен как NVARCHAR(IOO), то со здание для него пользовательского типа в SQL Server обеспечит согласо ванность определения адресов электронной почты во всех объектах базы данных. Когда вам понадобится изменить длину адресов, вы просто моди фицируете пользовательский тип данных — единственную точку измене ний. Аналогичным образом можно создать подкласс SqlParameterAttribute для централизации определения параметров, используемых во многих ко мандах. Ниже показано, как определить новый атрибут, основанный на SqlParameterAttribute и служащий оболочкой пользовательского типа sysname в SQL Server. Заметьте, что сам SysNameParameterAttribute запе чатан:

sealed class SysNameParameterAttribute : SqlParameterAttribute { public SysNameParameterAttributeC) : base{SqlDbType.NVarChar, 128} {} public Sy$NameParameterAttribute{string name) :

base(name, SqlDbType.NVarChar, 128) {} I Бонус Итак, вы видели, как с помощью атрибутов можно автоматически генери ровать команды в период выполнения, но они же позволяют создавать вспомогательные утилиты и инструменты, полезные при разработке при ложений. Чтобы вы получили представление о том, как создать простой инструмент, посмотрите на программу, приведенную на рис. 13. Если при ее запуске в командной строке указывается какая-нибудь сборка, програм ма просматривает все экспортируемые типы и их методы и сообщает о тех из них, у которых есть атрибут SqlCommandMethodAttribute со свойством CommandType, установленным в CommandType.StoredProcedure. Какой-то десяток строк кода — и вы сможете находить в сборках все прокси-функ ции хранимых процедур!

Динамическое связывание уровня данных Рис. 13. Перечисление всех прокси-функций хранимых процедур using System;

.usinf System. Reflection;

using Sample.Data.Sql;

Sample class static void Hain(string[3 args) Assembly assembly = Assembly.LoadFrom(args[0]);

foreach {Type type in assembly.GetExportedTypesO) foreach

if (attribute != null && attribute. CoiwnandType == System.Data.CormandType.StoredProcedure) Console, Write("{6}.{1}", nethodlnfo.NaBe);

If (attribute.CofflaiandText.Lervjth 1= 0) Console.WriteC -> {0}", attribute.CoiwaandText);

.Writellne();

И последнее. В исходный код, прилагаемый к статье, я включил образец SQL-сценария (рис. 14), который генерирует С#-прокси для вызова хра нимой процедуры, при необходимости устанавливая соответствующие ат рибуты параметров. Этот сценарий особенно полезен, когда вам нужны «тупые» оболочки для хранимых процедур. Только учтите, что он не рас считан на все случаи жизни (в частности, он не решает проблему выход ных параметров). Но если вы имеете дело с наиболее распространенным классом хранимых процедур, сценарий сделает всю работу за вас.

Рис. 14. SQt-сценарий для генерации сигнатуры С#-прокси set noeount on declare @sp varchar(lOO) set esp = '« здесь указывается имя хранимой процедуры »' см. след. стр.

300 Доступ к данным из приложений (продолжение) Рис. 14. SQL-сценарий для генерации сигнатуры...

declare @oid int select eoid = o.id from sysobjects о where о.паяе = @sp declare @last int — function signature select iglast = maxCc.eolid) froffl dtjQ.syscolufftns с where c.id = ®oid select case c.colid r when 1 then [ SqlComiriandMethodCCQBfflandType.StoredProcedure) ]' + char(l3) + 'public static SqlCoifimaRcJ ' + esp + •'(* + char(13)+ t tteiConnnandParaieeter ] ScilConftection ' connection' + спаг(13) * else ' ' end +' '+ case t.name when "char' then С SqlPararaeter('*convert(nvarchar(10}, c.length)*') ' when 'varchar' then [ SqlParameter(f+convert(nvarchar(tO>, c.lengthH') J* 'nchar' then [ SqlParameter('+convert(nvarchar(10)h c.length /2)+')3' when "nvarchar" then '[ SqlParameterC'+cor>vert(nvarchar(10),c.length/2)+')]* else end case t.name when 'char' then 'string' when 'nchar' then 'string' when 'varchar' then 'string' when 'nvarchar' then 'string' when 'bit' then 'bool when 'datetiiae' then 'DateTime' when 'float' then 'double' when 'real' then 'float' when 'int' then 'int' else 'object /* ' + t.name + ' */' end ' + lower(substrlng(c.name, 2, 1)} + su*string(c.name, 3, tOO) case c.colid when 91ast then '' + char(13) + "{' ) else ',' end см. след. стр.

Динамическое связывание уровня данных (окончание) Рие. 14. SQt-сценарий для генерации сигнатуры...

front dbo.syscolumns с left outer join dbo.systypes t on c.xusertype = t.xusertype where c.id = @old order by c.colid — вызов генератора,select case c.colld when 1 then return SqlGo[nmandGenerator.GenerateCofflffland(connection,' + char(13) else " " end +' ' + lowerCsubstrlnsCc.name, 2, 1» + substring c.name. 3, 100) case o.colid when elast ttierv ');

' + char(13) + ',}* else ',' end from dbo.syscolumns с where c.ld = @oid order by c.colic) Чтобы использовать этот сценарий, просто загрузите его в isqlw (SQL Query Analyzer), перейдите в свою базу данных, присвойте переменной @sp имя хранимой процедуры — и вперед! Потом скопируйте выходной код из секции результатов (result pane), включите его в свое решение и при необходимости внесите в него изменения. В сценарии предполагается, что вы будете возвращать из своей функции объект SqlCommand, а это полез но в основном для тех запросов, где выбор метода сбора данных оставля ется на усмотрение вызвавшей функции. В одних ситуациях вызвавшая функция может подключить команду к SqlDataAdapter, а в других — бу дет достаточно SqlDataRcader.

Для получения корректных результатов надо также сообщить isqlw, чтобы тот удалил заголовки и показал результаты в виде текста, а не сетки (grid).

Соответствующие параметры настраиваются на вкладке Results диалого вого окна Options (рис. 15). Кроме того, учтите, что сценарий тестировал ся только в SQL Server 2000, хотя, по идее, он должен нормально работать и в версии 7.0.

Перенастроить сценарий на генерацию кода для Visual Basic.NET не сложно, и эту задачу я оставляю как упражнение для читателей.

Доступ к данным из приложений Default results target:

Results output fornut: ("] [ Maximum characters per column:

Выберите в списке f" Flint column headers «Results (о Text» Г~ crol results a;

received ("] Г" Output queijj Г" Right align numerics ["] Сбросьте флажок Г~ Discard results Print column headers» Г" Щпеп a query batch c:orrplBtes:

Рис. 15. Удаление заголовков и отображение результатов в виде текста Заключение Поддержка создания собственных атрибутов, связывания их с различны ми элементами программы и запроса метаданных через механизм Reflec tion открывает колоссальные возможности в автоматизации и в разработ ке совершенно нового класса динамичных приложений. Я продемонстри ровал использование атрибутов и механизма Reflection на примере решения реальной проблемы (упрощения вызовов хранимых процедур) и надеюсь, что вы теперь понимаете, как применить их на практике в других ситуациях. Моя библиотека годится для любого CLR-совместимого язы ка программирования. В ней много чего можно усовершенствовать. Так, вы могли бы реализовать кэширование, чтобы часто используемые и сложные команды с массой параметров не становились «узким местом» в вашей программе. Однако я не стал бы слишком увлекаться кэшированием без предварительного профилирования кода. В целом, по сравнению с тради ционной настройкой объекта команды мой Sql Command Generator должен работать лишь чуть медленнее.

Атиф Азиэ (Atif Aziz) — главный консультант в Skybow AG и бывший Microsoft'oBeq (ex-Microsoftie). Основное направление его деятельности — помощь заказчикам в переходе на платформу.NET Framework. Регулярно выступает на конференциях Microsoft. С ним можно связаться по адресу atif.aziz@skybow.com.

Майкл Говард и Кит Браун Советы по защите Десять лучших приемов защиты кода, о которых должен знать каждый разработчик Когда дело касается безопасности, есть много способов попасть в неприятно сти: доверять любому коду, выполняемому в вашей сети, предоставлять доступ к важным файлам кому угодно и никогда не проверять, не модифици рован ли код на вашей машине. А еще можно работать без антивирусных программ, не встраивать защиту в собственный код и выдавать чрезмерные привилегии слишком широкому кругу лиц. Или вручить все отмычки для взлома, легкомысленно используя некоторые встроенные функции. Наконец, можно оставить все порты сервера открытыми и не следить за ними.

Очевидно, это далеко не полный список. Какие проблемы защиты важны по настоящему, каких ошибок избегать в первую очередь, чтобы не подвергать опасности данные или систему? Эксперты в области безопасности, Майкл Говард и Кит Браун, дадут вам десять советов, которые помогут уберечься от неприятностей.

Безопасность — проблема многоплановая. Угроза безопасности может ис ходить откуда угодно. От плохо написанного кода для обработки ошибок или от слишком щедро розданных разрешений. От забытых сервисов, ра ботающих на вашем сервере. От неразборчивости в приеме вводимых дан ных... список можно продолжать еще долго. Вот 10 советов, которые дадут вам фору в извечной борьбе щита и меча — помогут в защите компьюте ров, сеги, кода и в выборе более безопасной сетевой стратегии.

Публиковалось в MSDN Magazine/Русская Редакция. 2002. №3 (сентябрь). — Прим. изд.

Доступ к данным из приложений 1. Не доверяйте данным, вводимым пользователями Даже если вы не прочтете больше ни строчки из этой статьи, запомните одно: не доверяйте тому, что вводят пользователи. Если поверить, будто пользователь всегда вводит правильные данные, до неприятностей — один шаг. Большинство уязвимых мест в безопасности открывается взломщику, когда сервер получает заведомо некорректные данные.

Доверие к любой вводимой информации может приводить к переполне нию буфера, атакам с использованием кросс-сайтовых сценариев (cross site scripting attacks) или с внедрением SQL-кода (SQL injection attacks) и т. д.

Рассмотрим каждую из этих потенциально возможных атак детальнее.

2. Защищайтесь от переполнения буфера Переполнение буфера происходит, когда атакующий предоставляет боль ше данных, чем ожидает приложение, в результате чего излишек данных «переливается» во внутреннее пространство памяти. Переполнение буфе ра — проблема, характерная в основном для С/С++-программ. Это, конеч но, угроза, но обычно легко устранимая. Мы сталкивались лишь с двумя случаями, когда переполнение буфера было неочевидным и избавиться от него было непросто.

Переполнение буфера вызывает повреждение других структур данных в памяти, а это зачастую позволяет атакующему запускать вредоносный код.

Бывает, что переполнение буфера или обращение за его нижнюю границу происходит из-за ошибок в индексировании массива, но такие случаи встречаются реже.

Взгляните на фрагмент С++-кода:

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) { char cBuffDest[32];

memcpyCcBuffDest, cBuffSrc, cbBuffSrc);

} В чем здесь проблема? С этим кодом все в порядке, если значения cBuffSrc и cbBuffSrc передаются из доверенного источника, например, от кода, ко торый проверяет правильность структуры и размера вводилшх данных. Но если данные поступят из ненадежного источника, атакующий (собственно источник) может запросто сделать так, чтобы значения cBuffSrc и cbBuff Src были больше значения cBuffDest. Когда memcpy скопирует данные в cBuffDest, она затрет адрес возврата из DoSomething, поскольку cBuffDest размещается во фрейме стека функции сразу за адресом возврата. Таким образом, атакующий заставит код выполнить деструктивные действия.

Десять лучших приемов защиты кода Чтобы избежать этого, нельзя доверять ни данным, вводимым пользовате лем, ни данным, хранящимся в cBuffSrc и cbBuffSrc:

void DoSomethtng(char «cBuffSrc, DWORD cbBuffSrc) { const DWORD cbBuffOest = 32;

char cBuffDest[cbBuffDest];

ttifdef „DEBUG memset(cBuffDest, 0x33, cbBuffSrc);

Sendif memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));

i У этой функции три особенности, которые отличают правильно написан ный код, не допускающий переполнения буфера. Во-первых, она требует, чтобы при вызове ей сообщили размер буфера. Конечно же, этому значе нию слепо верить нельзя! Во-вторых, в отладочной версии код проверяет, достаточна ли емкость буфера, чтобы вместить данные из исходного буфе ра. Если это не так, код скорее всего вызовет нарушение доступа (ошибку защиты памяти), и запустится отладчик. (Просто удивительно, сколько вы при этом обнаружите ошибок!) Наконец;

самое важное: вызов memcpy тоже под охраной, поскольку он копирует данных не больше, чем умеща ется в целевом буфере.

Когда в Microsoft был объявлен месячник по устранению ошибок, связан ных с защитой (под названием Windows Security Push), мы создали для программистов, пишущих на С, список функций, которые безопасно обра батывают строки. Просмотрите его на http://msdn.microsoft.com/library/ en-us/dnsecure/html/strsafe.asp.

3. Не позволяйте запускать кросс-сайтовые сценарии Уязвимость, связанная с возможностью запуска кросс-сайтовых сценари ев, — специфичная для Web проблема, которая может привести к компро метации клиентских данных из-за изъяна на единственной Web-странице.

Вообразите фрагмент кода на ASP.NET:

Подобный код видел почти каждый. Наверное, вы удивитесь, но он кишит ошибками! Обычно пользователь обращается к этому коду по примерно такому URL:

tittp://explorations! г. com/welcome. aspx?name=Michael Код на С# предполагает, что данные всегда корректны и не содержат ни чего, кроме имени. Но атакующие, злоупотребляя его доверчивостью, под ставляют имя сценария и нужный HTML-код. Набрав URL:

306 Доступ к данным из приложений fittp://northwindtraders. com/welcome.aspx?name= вы получили бы Web-страницу, которая отображает диалоговое окно со словом «hi!>>. «Ну и что?» — спросите вы. Представьте, что пользователь щелкнул эту ссылку, — тогда в строке запроса окажется какой-нибудь опасный сценарий и HTML-код, который получает ваш cookie и передает его на сайт атакующего. В результате он получит информацию из вашего cookie или сделает что-нибудь похуже.

Есть два способа избежать этого. Первый — не доверять вводу и проверять строку с именем пользователя. Например, вы могли бы проверять, содер жит ли имя только стандартный набор символов и не слишком ли оно ве лико. Следующий фрагмент кода на СП демонстрирует, как это делается:

Regex г = new Regex(@""[\w]{1,40}$");

if (г.Match(strName).Success) { // Хорошо! Со строкой все в порядке.

} else { // А это не хорошо! Строка неправильна.

} Этот код проверяет строку по выражению, чтобы она содержала от 1 до алфавитно-цифровых символов и чтобы ничего больше в ней не было. Это единственный безопасный способ убедиться в корректности значения, HTML-код или сценарий через такое выражение не пролезет! Но не пола гайтесь на эти выражения в поиске недопустимых символов и не откло няйте запрос, если таковые символы будут найдены, потому что вы обяза тельно что-нибудь упустите.

Второй способ — кодировать в формате HTML всю входную информацию, когда она используется как выходная. Это превратит опасные HTML-тэги в более безопасные escape-символы. С помощью метода HttpServerUtili ty.HtmlEncode (в ASP.NET) или Server.HTMLEncode (в ASP) вы обезопа сите себя от любых потенциально опасных строк.

4. Не требуйте разрешений уровня системного администратора Последний вид атак, основанных на доверии к входной информации, ко торый мы хотели обсудить, — внедрение SQL-кода. Многие разработчики пишут код, принимающий ввод и на его основе формирующий SQL-зап росы к серверному хранилищу данных, например Microsoft SQL Server или Oracle.

Десять лучших приемов защиты кода Взгляните на этот фрагмент кода:

void DoQuery(string Id) { SqlConnection sql= new SqlConnection(@"data source=localhost;

" + "user id=sa;

password=password;

");

sql.OpenO;

sqlstring= "SELECT hasshipped" + 11 P FROM shipping WHERE id= " + Id + ;

SqlCommand cmd = new SqlConimand(sqlstring, sql);

В нем три серьезных изъяна. Во-первых, Web-сервис подключается к SQL Server по учетной записи системного администратора (sa). Чуть позже вы поймете, почему это плохо. Во-вторых, учетной записи sa назначен жутко хитрый пароль «password»!

Но настоящий повод для беспокойства — в конкатенации строк, из кото рых формируется SQL-выражение. Если пользователь вводит идентифи катор 1001, получается следующее SQL-выражение (абсолютно верное и допустимое):

SELECT hasshipped FROM shipping WHERE id = '1001' Но изобретательные атакующие идут дальше и вводят следующий идентифи катор: «'100Г DROP table shipping —•», что приводит к выполнению запроса:

SELECT hasshipped FROM shipping WHERE id = '1001' DROP table shipping - ';

Это изменяет работу запроса. Код не только пытается определить, отгру жен ли некий товар, но и удаляет таблицу shipping! Оператор — обознача ет в SQL комментарий, который облегчает атакующему создание набора допустимых, но от того не менее опасных, SQL-выражений!

Здесь вас, вероятно, заинтересовало, как это любой пользователь может удалить таблицу из базы данных SQL Server. Подобные действия разреше ны только администраторам. Вы правы. Но здесь вы подключаетесь к базе данных по учетной записи sa, а она позволяет делать с базой данных SQL Server что угодно. Так что никогда не подключайтесь к SQL Server из при ложений по sa;

вместо этого используйте Windows-средства аутентифика ции или создайте специальную учетную запись с соответствующими (ог раниченными!) правами.

Устранить проблему с внедрением SQL-кода легко. Следующий код де монстрирует, как это делается (предполагается, что идентификатор отгру женного товара может быть только числом, в котором от четырех до деся ти разрядов):

308 Доступ к данным из приложений Педех г = new Regex(@"~\d{4,10}$");

if {I r.Match(Id).Success) throw new Exception("Invalid ID");

SqlConnection sqlConn= new SqlConnection(strConn);

string str="sp_HasShipped";

SqlCoiwand cmd = new SqlCommand(str, sqlConn);

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("@ID",Id);

Переполнение буфера, запуск кросс-сайтовых сценариев и внедрение SQL-кода — все это примеры атак, возможных из-за доверия к входной информации. От атак этого типа можно защититься, если считать любой ввод злонамеренным, пока не доказано обратное.

5. Избегайте доморощенной криптографии!

Теперь рассмотрим нечто близкое и дорогое нам. Я бы сказал, более 30% кода, который предназначен для защиты и который проходит через нашу экспертизу, содержит ошибки, создающие дыры в этой самой защите. Ве роятно, самая распространенная из них — доморощенный код шифрова ния, обычно весьма ненадежный и уязвимый перед взломом. Никогда не пишите собственный код для шифрования: как надо, у вас все равно не получится. Не воображайте, будто, если вы создали свой криптографический алгоритм, его никто не разгадает. У атакующих есть отладчики и достаточ но знаний, чтобы точно выяснить, как работает ваша система, а зачастую и взломать ее за пару часов. Для \т32-приложений лучше использовать CryptoAPI;

пространство имен System.Security.Cryptography содержит бо гатый набор правильно написанных и тщательно протестированных крип тографических алгоритмов.

6. Сводите вероятность атак к минимуму Если какая-то функциональность не нужна 90% ваших клиентов, не давай те устанавливать ее по умолчанию. Internet Information Services (IIS) 6. следует именно такому плану установки. В основе этого подхода к уста новке лежит следующая идея: если в системе работают неиспользуемые сервисы, они остаются без присмотра, что опасно. А если такая функцио нальность все же устанавливается по умолчанию, она должна работать по принципу наименьшей привилегии.

7. Используйте принцип наименьшей привилегии Политика безопасности нужна операционной системе и общеязыковой исполняющей среде (common language runtime, CLR) по ряду причин.

Многие считают, что главная из них — не дать возможности пользовате лям открывать файлы, не предназначенные для их глаз, перенастраивать Десять лучших приемов защиты кода сеть под свои потребности и совершать прочие нехорошие поступки. Уг роза внутренних атак, несомненно, существует, и гарантированная защи та от них нужна, но есть и другая причина. Политика безопасности при звана оградить код от преднамеренных или непреднамеренных действий пользователей, сеющих хаос в сети. Например, программа, полученная как вложение в почтовом сообщении и запущенная на машине пользователя, получит доступ лишь к тем ресурсам, которые доступны этому пользова телю. Если во вложении окажется троянский конь, хорошая политика бе зопасности резко ограничит возможный ущерб.

При проектировании, кодировании и развертывании серверных приложе ний не полагайтесь на то, что запросы будут приходить только от лояль ных пользователей. Если плохой парень пришлет злонамеренный запрос, который (Боже упаси!) заставит ваш код выполнить деструктивные дей ствия, вы захотите возвести вокруг своего приложения любые укрепления, способные ограничить наносимый ему вред. Суть в том, что политика бе зопасности нужна вашей компании не потому, что она не доверяет вам или вашему коду, а для защиты на тот случай, если этим кодом будет управ лять злая воля извне.

Принцип наименьшей привилегии гласит, что любую привилегию следу ет предоставлять только минимально необходимому коду и только на ми нимально необходимый срок. Иными словами, в любой момент времени нужно возводить вокруг своего кода как можно больше защитных стен.

Когда случится какая-нибудь неприятность (а по закону Мэрфи это неиз бежно), вы будете рады, что эти стены есть. Вот несколько конкретных идей насчет того, как сделать, чтобы код работал с минимально возможной привилегией.

Выбирайте для своего серверного кода такой контекст защиты, который дает доступ только к ресурсам, нужным для его работы, Если какая-то часть вашего кода требует существенно больших привилегий, подумайте об отделении ее от остального кода. Лучший способ безопасно изолиро вать код с другими удостоверениями защиты — выполнять его в отдельном процессе, работающем в более привилегированном контексте защиты. Это значит, что вам понадобится механизм для межпроцессного взаимодей ствия, например СОМ или Microsoft.NET Remoting;

кроме того, придет ся разработать такой интерфейс, который позволит минимизировать об мен данными.

Если вы используете.NET Framework, продумайте необходимый уровень привилегий для каждой сборки. Вполне вероятно, что изолировать код, требующий высоких привилегий, легче в отдельных сборках, которым можно дать больше разрешений, а остальные сборки пусть работают с Доступ к данным из приложений меньшими привилегиями — это позволит окружить код дополнительной защитной стеной. Простой способ ограничить привилегии конкретной сборки — запрашивать разрешения на уровне сборки, как показано на рис. 1. А рис. 2 иллюстрирует, как создавать XML-файлы, используемые при таких запросах. Только не забудьте, что таким способом вы ограничи ваете разрешения не только для своей сборки, но и для любых сборок, вызываемых из нее.

Рис. 1. Запросы разрешений для сборки using. System;

using System.Security.Permissions;

// Объявить минимальные разрешения, необходимые // для загрузки этой сборки [assembly: PenuissionSetAttribui.e( Securi tyAc t i о n.R e q i jest M i n i mu m, - - File-"i»in_per«.xEsl"} // Необязательные разрешения, не критичные для работы // сборки, мо важные для работы некоторых функций [assenbly: Per»issionSetAttr±bute< SecurityAction. ftetjuestOptlonal, File="ept_pe nn.xffll") // Еед» разрешения заданы явт, исполняющая среда не // предоставит других разрешений, даже если от будут // определены лосяв поставки вашего кода Многие создают приложения, к которым можно подключать дополнитель ные компоненты уже после тестирования и поставки продукта. Защищать такие приложения очень трудно, поскольку нельзя протестировать все пути выполнения кода на наличие ошибок и дыр в защите. Однако, если это управляемое приложение, можно использовать удобную CLR-функци ональность, которая позволяет блокировать точки подключения модулей.

Объявив объект или набор разрешений и вызвав PerniitOnly или Deny, вы добавляете в стек маркер, отнимающий все разрешения у любого вызыва емого вами кода. Делая так перед вызовом подключаемого модуля, вы ог раничите операции, которые сможет выполнять этот модуль. Так, подклю чаемому модулю, выполняющему расчет амортизации, вовсе не нужен до ступ к файловой системе. Это просто еще один пример использования принципа наименьшей привилегии, позволяющего заранее обезопасить себя. Обязательно документируйте эти ограничения и помните, что под ключаемые модули с высокими привилегиями способны обойти эти огра ничения через оператор Assert.

Десять лучших приемов защиты кода Рис. 2. Сериализация запросов разрешений using System;

using System.Security;

using System.Security.Permissions;

class App { static void Nain(stringU args) { // Сериалиэация набора разрешений IPermission a » new flvlroRfflentPennis3ion{ Envl ronmerrtPe rmissionAccess. fleat), "HyEnvi ronnrentVar");

ж IPermission b new FileSlalogPeraisslonC FileOialogPermissiQnAccess.Qpen);

PermissionSet ps ~ new Penai$sionSet( Pe riHissionState. Hone);

ps.AcfdPerBiisslon(a);

ps.AddPerBiission);

Console. WrlteLine(ps.ToXBlO);

8. Обращайте внимание на обработку ошибок Признайтесь: вы, как и все, ненавидите писать код для обработки ошибок.

Ведь причин их появления так много, что одна мысль об этом наводит тос ку. Большинство программистов, включая нас самих, намного охотнее уде лили бы внимание нормальному пути выполнения кода, поскольку имен но на этом пути программа делает свою работу. И всем хочется поскорее разделаться с обработкой ошибок, чтобы перейти к настоящему коду.

Печально, но такой настрой — не лучший с точки зрения безопасности.

Потенциальные сбои нужно продумывать гораздо тщательнее. Куски кода, отвечающие за обработку ошибок, часто пишут без особого внимания к деталям и вообще не тестируют. Вот вы, хорошо помните, когда в после дний раз проходили отладчиком весь код в функции, не пропустив ни еди ной строки в этих крошечных обработчиках ошибок?

Непроверенный код часто создает дыры в защите. Для решения этой про блемы можно сделать три вещи. Во-первых, уделяйте обработчикам оши бок не меньше внимания, чем обычному коду. Подумайте, в каком состоя нии находится система при выполнении вашего кода обработки ошибок.

Оставляете ли вы систему в согласованном и безопасном состоянии? Во вторых, написав функцию, несколько раз пройдитесь по ней отладчиком, чтобы попасть в каждый обработчик ошибок. Но даже столь пристрастная проверка может не выявить малозаметные ошибки синхронизации. Воз 312 Доступ н данным из приложений можно, придется передать функции заведомо недопустимые аргументы или как-то изменить состояние системы, чтобы активизировать обработ чики ошибок. Пошаговое выполнение кода в отладчике отнимает много времени, однако не стоит жалеть его, чтобы лишний раз проверить код и состояние системы во время его работы. Таким способом нам удавалось обнаруживать массу изъянов в логике наших программ. Это проверенная временем методика — используйте ее. Наконец, убедитесь, что ваши тес товые комплексы действительно позволяют моделировать неудачное за вершение ваших функций. Старайтесь создавать такие тестовые комплек сы, которые заставят работать каждую строку кода в функции.

И еще одно важное замечание о сбойных ситуациях. Следите, чтобы пос ле возникновения ошибки ваш код оставлял систему в максимально безо пасном состоянии. Вот пример плохого кода:

bool accessGranted = true;

// оптимистичное допущение!

try { II Проверить, есть ли доступ к c:\test.txt new FileStream(§"c:\test.txt", FileMode.Open, FileAccess.Head).Close();

.

catch (SecurityException x) { // В доступе отказано accessGranted = false;

I catch (... ) { // Случилось что-то другое i Допустим, CLR не запрещает доступ к файлу, поэтому исключение Securi tyException не возникает. А что, если доступ к этому файлу запрещен, на пример, сопоставленным с ним списком управления избирательным дос тупом (DACL)? Тогда возникнет исключение другого типа, но оптимис тичное допущение в первой строке кода никогда не позволит нам узнать об этом.

Лучше писать этот код в пессимистичной манере:

bool accessGranted = false;

// пессимистичное допущение!

try { // Проверить, есть ли доступ к c:\test.txt new FileStream(@"c:\test.txt", FileMode.Open, FileAccess.Read).Close();

// Если дошли до этого места, все в порядке!

accessGranted = true;

} catch (...) {} Десять лучших приемов защиты кода Это куда более отказоустойчивый код, поскольку независимо от сбойной ситуации он возвращается в самое безопасное состояние.

9. Олицетворение — опасная штука При написании серверных приложений вы нередко (прямо или косвенно) используете удобную функциональность Windows, называемую олицетво рением (impersonation). Олицетворение позволяет каждому потоку про цесса работать в собственном контексте защиты — как правило, в клиент ском. Например, когда редиректор файловой системы получает через сеть запрос к файлу, он аутентифицирует удаленный клиент, проверяет, не на рушает ли его запрос DACL сетевого ресурса, а затем назначает потоку, который обрабатывает запрос, так называемый маркер олицетворения (impersonation token) клиента. В результате этот поток подменяет клиен та. Далее поток получает доступ к локальной файловой системе на серве ре в контексте зашиты клиента. Локальная файловая система проверяет права с учетом запрошенного типа доступа, DACL файла и маркера оли цетворения, присвоенного потоку, Если эта проверка заканчивается неуда чей, она сообщает об этом редиректору файловой системы, который воз вращает соответствующую ошибку удаленному клиенту. Для редиректора файловой системы это невероятно удобно, поскольку он просто передает эстафету локальной файловой системе и предоставляет ей самой разбирать ся с правами доступа — так, будто запрос поступил от локального клиента.

Все это хорошо для простых шлюзов наподобие редиректоров файловых систем. Однако олицетворение часто применяется и в более сложных слу чаях. Возьмем, к примеру Web-приложение. Если вы пишете классическое неуправляемое ASP-приложение, расширение ISAPI или приложение ASP.NET, у которого в файле Web.config указано:

то получите среду с двумя разными контекстами защиты: процессу при сваивается один маркер, потоку — другой. Для проверки прав доступа обычно используется маркер потока (рис. 3). И вот вы пишете приложе ние ISAPI, выполняемое в процессе Web-сервера. У вашего потока скорее всего будет маркер IUSR_MACHINE — при условии, что большинство запросов выполняется без аутентификации. Но у процесса-то маркер SYSTEM! Допустим, плохому парню удалось скомпрометировать ваш код через переполнение буфера. Думаете, он удовлетворится маркером IUSR_ MACHINE? Да никогда! Атакующий код вызовет метод RevertToSelf, что бы удалить маркер олицетворения в надежде повысить уровень своих при вилегий. И замечательно преуспеет в данном случае. Он может сделать и другое: вызвать CreateProcess. Маркер защиты для нового процесса фор Доступ к данным из приложений мируется не из маркера олицетворения, а из маркера защиты процесса родителя, так что новый процесс будет работать как системный.

ftSPflfT WP-Ш По гв* Рис. 3. Проверка Как решить эту маленькую проблему? Ну, для начала следите, чтобы не было переполнения буфера, и помните о принципе наименьшей привиле гии. Если ваш код не нуждается в божественных полномочиях, предостав ляемых SYSTEM, не настраивайте Web-приложение на выполнение в про цессе Web-сервера. Если вы просто сконфигурируете его на выполнение со средним или высоким уровнем изоляции, ваш процесс получит маркер IWAM_MACHINE, вообще не дающий никаких привилегий, — в этом слу чае атаки будут далеко не так эффективны, как в предыдущем. Заметьте, что по умолчанию в IIS 6.0, который станет компонентом Windows.NET Server, никакой пользовательский код не может работать как SYSTEM.

Это решение основано на понимании того, что разработчики время от вре мени ошибаются и что любое содействие со стороны Web-сервера в сни жении привилегий кода пойдет им на пользу.

СОМ-программисты должны учитывать один прокол. Если вызвать внут рипроцессный СОМ-сервер (in-process COM server), чья модель потоков отличается от таковой для вызывающего потока, СОМ выполнит вызов в другом потоке. При этом СОМ не станет использовать маркер олицетво рения вызывающего потока, из-за чего вызов будет выполнен в контексте защиты процесса. Какой сюрприз!

Еще один сценарий, где олицетворение может подставить вас под удар.

Пусть ваш сервер принимает запросы через именованные каналы, DCOM или RPC. Вы аутентифицируете клиенты, олицетворяете их и открываете объекты ядра от имени этих клиентов. Теперь допустим, что вы забыли закрыть один из таких объектов (например, файл) после отключения кли ента. Затем подключается следующий клиент, вы снова его аутентифици руете, олицетворяете и... угадайте, что будет? У вас по-прежнему сохраня ется доступ к тому файлу, и, даже если у нового клиента нет прав на дос Десять лучших приемов защиты кода гуп к нему, он сможет обращаться к этому ресурсу. А все дело в том, что для большего быстродействия ядро проверяет права доступа к объектам, только когда вы впервые открываете их. Даже если ваш контекст защиты потом изменится (из-за олицетворения очередного клиента), вы все рав но сможете обращаться к этому файлу.

Все упомянутые нами сценарии напоминают, что олицетворение — штука, хоть и удобная разработчикам серверного ПО, но небезопасная. Если ваш код работает с маркером олицетворения, обратите на него самое присталь ное внимание.

10. Пишите приложения, с которыми смогут нормально работать не только администраторы На самом деле это следствие принципа наименьшей привилегии. Если программисты будут и дальше писать код, нормально работающий в Win dows, только когда его пользователем является администратор, то как же, черт возьми, мы избавим эту систему от клейма «небезопасной»? Windows предоставляет внушительный набор средств защиты, но, если приложение требует от пользователей прав администратора, а иначе отказывается что либо делать, то какой толк от всех этих средств?

Что здесь посоветовать? Прежде всего, не берите на себя слишком много.

Перестаньте работать по учетной записи администратора, и вы очень ско ро поймете, сколько мучений доставляет использование программ, чьи разработчики не учитывали требования защиты. Однажды я (Кит) устано вил одну программу, которая синхронизирует данные между карманным устройством и настольным компьютером (она шла в комплекте с устрой ством), Так вот, я, как всегда, вышел из системы, где работал по обычной учетной записи пользователя, и, снова войдя в систему, но уже по встро енной учетной записи администратора, установил эту программу. Затем, вернувшись к обычной учетной записи, попытался запустить установлен ную программу. Она тут же показала мне окно с сообщением, что не мо жет получить доступ к нужным файлам данных, а потом все кончилось сообщением об ошибке защиты памяти (access violation). Ребята, но это же была программа от одного из крупнейших в мире производителей карман ных устройств. Такому просто нет оправдания!

Запустив утилиту FILEMON (http://sysinternals.com), я быстро обнару жил, что злополучная программа пытается открыть для записи файл дан ных, установленный в один каталог с ее исполняемыми файлами. Когда приложения устанавливаются в каталог Program Files (как и должно быть), они ни в коем случае не должны записывать в этот каталог свои данные. Дело в том, что на каталог Program Files распространяется огра Доступ к данным иэ приложений ничение доступа. Мы не хотим, чтобы пользователи что-то записывали в этот каталог, так как иначе один из них может оставить там троянского коня, а другой — запустить его. Поддержка такой политики — фактически одно из базовых требований к приложениям с эмблемой Windows XP (http://www.microsoit.com/vvinlogo).

Слишком часто приходится слышать от программистов оправдания за раз работку программ под администраторской учетной записью. Если мы бу дем по-прежнему игнорировать проблему, это лишь усугубит ее. Ребята, для редактирования текстового файла не нужны привилегии администра тора — равно как и для компиляции или отладки ваших программ. А в тех случаях, когда такие привилегии действительно нужны, запускайте соот ветствующие программы с повышенными привилегиями через RunAs (см.

колонку «Security Briefs» за ноябрь 2001 г. по ссылке http://msdn.micro soft.com/msdnmag/issues/01/ll/security/security0111.asp). Если выпише те инструменты для разработчиков, на вас ложится дополнительная ответ ственность. И нужно разорвать порочный круг, когда разработчики пишут код, который могут запускать только администраторы, — но добиться этого можно, только если инициативу поддержат широкие массы разработчиков.

За дополнительной информацией на этот счет обращайтесь на Web-сайт Кита Брауна (http://www.develop.com/kbrown);

и непременно прочтите книгу Майкла Говарда «Writing Secure Code» (Microsoft Press, 2001)*, в которой даны рекомендации по написанию приложений, корректно рабо тающих без администраторских, привилегий.

Майкл Говард (Michael Howard) — менеджер программы Security Program группы Secure Windows Initiative в Microsoft. Соавтор книги «Writing Secure Code» и автор книги "Design Secure Web-based Applications for Microsoft Windows 2000» (обе опубликованы Microsoft Press).

Кит Браун (Keith Brown) работает в DevelopMentor как исследователь, технический писатель и преподаватель. Разъясняет программистам концеп ции безопасного кода. Автор книги "Programming Windows Security» {Addison Wesley, 2000), соавтор «Effective COM». Сейчас работает над книгой по безопасности в.NET. С ним можно связаться по адресу http:// www. deve I op. со m/ k b rown.

Издательство «Русская Редакция* готовит к выпуску перевод второго издания этой кни ги (ориентировочно в третьем квартале 2003 г.).

Джонни Папа Доступ к данным Объекты DataRelation в ADO.NET Одно из главных отличий ADO.NET от традиционной ADO в том, что новая технология позволяет использовать истинно реляционные наборы записей.

Здесь рассматриваются все за и против представления данных в ADO.NET как многоуровневой реляционной структуры по сравнению с отдельным набором записей, получаемым через INNER JOIN. Автор поясняет, как усовершенство вать приложение с помощью объектов DataRelation и какие решения приходится принимать при работе с этими объектами. В заключение демонст рируется создание группы объектов DataTable. связанных через DataRelation, выборка родительских и дочерних наборов записей, а также выполнение каскадных обновлений DataSet.

Одно из главных отличий ADO.NET от традиционной ADO в том, что новая технология позволяет использовать истинно реляционные наборы записей (rowsets). Допустим, в DataSet имеется объект DataTable, содер жащий информацию о клиентах, и еще один объект DataTable, в котором содержатся сведения о заказах клиентов. В ADO.NET эти DataTable мож но связать друг с другом, воспроизведя отношение между ними, существу ющее в реляционной базе данных. Считав два набора записей (называе мых родительским и дочерним) и связав их между собой, можно извлечь все дочерние записи для данной родительской, отобразить любой из Data Table в сетке (grid) или модифицировать объекты EjataTable и записать все изменения в базу данных за одно пакетное обновление. Все эти возможно сти реализуются объектами DataRelation — неотъемлемой частью прило жений ADO.NET.

* Публиковалось в MSDN Magazine/Русская Редакция. 2002. №5 (ноябрь). — Прим. изд.

318 Доступ н данным из приложений В этой статье рассматриваются все за и против представления данных в ADO.NET как многоуровневой реляционной структуры по сравнению с отдельным набором записей, получаемым через INNER JOIN. Я поясню, как усовершенствовать приложение с помощью объектов DataRelation и какие решения приходится принимать при работе с этими объектами. В заключение я продемонстрирую, как создавать группы объектов DataTable, связанных через DataRelation, как извлекать родительские и дочерние на боры записей, а также как выполнять каскадные обновления DataSet, Зачем нужны DataRelation?

Прежде чем рассматривать примеры кода, важно понять, что дают взаимо связи между данными и что нужно учесть перед их связыванием. К неко торым из основных факторов, определяющих, стоит ли использовать вза имосвязи и как именно, относятся проблема избыточности данных, способ их отображения и изменения. Начнем с этих вопросов, а затем попробуем на практике реализовать такие взаимосвязи.

В свой февральской колонке за этот год я продемонстрировал, как расши рить приложение так, чтобы сохранять изменения в нескольких иерархи ях данных с применением ADO.NET-объектов DataRelation (http://msdn.

microsoft.com/msdnmag/issues/02/02/data/data0202.asp)*. Пример, кото рый я тогда показывал, — стандартное приложение, отображающее и со храняющее данные в элементе управления DataGrid на Web-форме. (Data Grid идеален для отображения связанных объектов DataTable, так как в него встроена поддержка отношений между объектами.) Сегодня я уделю основное внимание объектам DataRelation и поясню преимущества и не достатки этих объектов в сравнении с традиционными наборами записей, возвращаемыми SQL-операторами.

В примерах я буду ссылаться на иерархическую структуру «клиент (custo mer), заказы (orders), позиции заказа (order details)» в базе данных Nor thwind. Решая, использовать один объект DataTable, содержащий объеди ненный (joined) набор записей, или группу связанных объектов DataTable, учитывайте, что две основные функции управления данными заключают ся в отображении информации и контроле изменений (как в базе данных, гак и в самой структуре данных).

Если вы хотите просто отображать информацию, ничего в ней не изменяя, выбор прежде всего определяется тем, как именно нужно показывать дан ные — в одной сетке или в нескольких. Если в одной — лучше использо вать объединенный набор записей, хранящийся в DataTable. Возможность См. Папа Дж. Доступ к данным. ADO.NET: концепции и реализация — С. 77.

Объекты DataRefation в ADO.NET хранения нескольких связанных объектов DataTable не означает, что раз работчики обязательно должны ее использовать. Здесь полная аналогия с отверткой со сменными насадками: если нужно открутить всего один винт, она менее удобна, чем обычная отвертка. Точно так же и группа связанных объектов DataTable может оказаться не самым эффективным решением, если программа лишь отображает данные в одной сетке. Если же програм ма отображает данные, но не в одной, а в нескольких сетках, правильнее хранить данные в связанных объектах DataTable.

Приложения, просто показывающие данные, весьма распространены. Од нако не менее распространены и приложения, которые позволяют не толь ко отображать, но и модифицировать данные. В таких приложениях важно сбалансировать средства удобного и эффективного отображения данных с возможностью управления изменением данных. Здесь-то и проявляется гибкость ADO.NET. При хранении информации о клиентах, заказах и по зициях заказов в трех раздельных объектах DataTable, связанных друг с другом, вы сможете легко показывать эти объекты в нескольких сетках и управлять изменениями в любом из них. Объекты DataRelation, связыва ющие эти наборы записей, позволяют фильтровать данные (с использова нием метода GetChanges) перед отправкой изменений в базу данных. И тог да источнику данных передается лишь модифицированная информация.

Отдельные запросы и запросы с объединением Связанные наборы записей, использующие объекты DataTable и Data Relation, помогают избавиться от избыточности данных, неизбежной в единственном объединенном наборе записей. Так, объединение информа ции о клиентах, заказах и позициях заказов в единый набор записей при водит к дублированию данных, относящихся к клиенту (например, фами лии представителя клиента и названия компании), в каждом заказе и каж дой позиции заказа. Если клиент делает 5 заказов, а в каждом заказе по позиций, то информация о клиенте повторяется во всех 25 записях. Это давняя проблема разработчиков на SQL. Однако есть отличный способ избавиться от такой избыточности данных: используйте иерархические структуры данных вроде связанных объектов DataTable.

Обычно наборы записей формируются в результате запросов с объедине нием (join queries). Например, стандартный SQL-запрос для выборки кли ентов и их заказов возвращает один набор записей, в котором данные о клиенте дублируются в каждом заказе, т. е. возникает избыточность дан ных. А если вы запрашиваете еще и позиции заказов, избыточность данных увеличивается еще больше (рис. 1).

Изменение единого набора записей, полученного из нескольких таблиц, — операция весьма обременительная, если приходится обновлять родитель Доступ к данным из приложений ские данные. Чтобы модифицировать информацию о клиенте в наборе за писей «клиент, заказ, позиции заказа» и распространить изменения на весь набор записей, вам придется заново запрашивать из базы большой объем избыточных данных. Однако с помощью объектов DataRelation набор за писей можно разделить на связанные друг с другом наборы, представляю щие исходные таблицы. Благодаря этому получаемый в результате DataSet в большей мере соответствует реальной структуре данных. Это же способ ствует уменьшению избыточности данных. Обновление данных значи тельно упрощается, а отображение информации о клиентах и их заказах вообще становится элементарной задачей.

Рис. 1. Объединение информации о клиентах, заказах и позициях заказов SELECT c.Gustoflierlu, с.СошрапуЫате, e.ContactName, o.QrderlD, o.QrderDate, Qd.ProdtietlD, p.PrcKHictNaine, od.UnitPrice, od.Quantity FRQH Customers с ШЕЙ JOIN Orders о ON c.CustQinerlD = o.CustoraerlD ШЕЯ JOIN [Order Details] od ON o.OrderlD = od.QrderlD INNER JOIN Products p ON od.ProductID = p.ProduotlD Еще одно преимущество связанных объектов DataTable — увеличение гиб кости в обновлении базы данных. Когда данные разделены на три объекта DataTable (клиенты, заказы, позиции заказов), связанные объектами Data Relation, данные каждого DataTable можно обновлять с помощью команд, относящихся только к этому DataTable. Например, сохраняя изменения в DataTable, содержащем набор записей по клиентам, можно использовать объект SqlCommand, специфичный для таблицы Customers в базе данных Northwind. Объект SqlCommand может ссылаться на SQL-оператор или хранимую процедуру, сопоставленную с объектом Sql Data Adapter, кото рый и записывает изменения DataTable в источник данных.

Формирование данных XML-программисты уже пару лет используют иерархические структуры данных, и с появлением ADO.NET эти структуры стало возможным легко реализовать в.NET-приложениях. В предыдущих версиях ADO для рабо ты с иерархическими структурами применялась технология, называемая формированием данных (data shaping). Хотя формирование данных позво Объекты DataRelation в ADO.NET ляло использовать иерархические структуры, у этой технологии были до статочно серьезные недостатки, помешавшие ее широкому внедрению.

Кроме того, она требовала изучения синтаксиса SHAPE.

Если при формировании данных нужен специальный провайдер данных и особый синтаксис, то ADO.NET позволяет создавать иерархические струк туры данных с использованием стандартного провайдера SQL и стандарт ного синтаксиса SQL, который мы все знаем и любим. По мере роста по пулярности XML в традиционную ADO все чаще и чаще добавлялись и другие XML-технологии. Но технология ADO не основана на XML — в этом ее принципиальный недостаток, который и был устранен в ADO.NET.

XML-функциональность традиционной ADO, например формирование данных и сохранение в формате XML, конечно, полезна, но в ADO.NET такая функциональность впечатляет куда больше. Ни разу не слышал, что бы кто-то сожалел об отказе от синтаксиса формирования данных.

Определение объектов DataRelation Как определить иерархический набор записей с помощью DataRelation?

Рассмотрим DataSet, содержащий три раздельных объекта DataTable, в которых хранится информация о клиентах, их заказах и о позициях зака зов. Эти DataTable можно связать друг с другом через объекты DataRela tion. Объект DataSet с DataTable, связанными через DataRelation, позволя ет выполнять каскадное обновление, перемещаться по родительским запи сям, объединять записи из разных источников данных и даже реализовать агрегацию данных, не делая того же в самой базе. Для начала вы должны создать объект DataSet с тремя наборами записей, полученными в резуль тате трех отдельных запросов (рис. 2).

В этом коде создается DataSet с тремя объектами DataTable: первый Data Table содержит информацию обо всех клиентах, второй — о заказах, тре тий — о позициях заказов. На данный момент мы определили DataSet, содержащий три набора записей, но пока не установили отношения. Я вос пользуюсь двумя объектами DataRelation: один свяжет клиентов с заказа ми, другой — заказы с позициями. Создание этих объектов DataRelation показано на рис. 3 (данный код продолжает код, представленный на рис. 2).

В результате создания объектов DataRelation и их добавления в набор Relations объекта DataSet три набора записей DataTable связываются друг с другом по указанным полям. У объектов DataRelation, как и у большин ства других объектов ADO.NET, несколько разных конструкторов. Я за действовал конструктор, параметрами которого являются имя отношения, а также поля родительской и дочерней таблиц. Если бы отношение уста навливалось по нескольким полям, можно было бы передать массивы по 11- Доступ к данным из приложений лей родительской и дочерней таблиц. Еще один вариант — указать те же три аргумента, что и на рис. 3, но добавить четвертый аргумент (значение типа Boolean), определяющий, следует ли автоматически создавать огра ничения (constraints). Об этих ограничениях я расскажу чуть позже.

Рис. 2. Создание DataSet //- Создаем соединение string sCn = "Data Source=(local);

Initial Gatalog=northwind;

User ID=sa;

Password*yourpassword";

SqlGonnection oCn = new SqlConnection (sCn);

DataSet oDs = new BataSetO;

/ - Заполняем DataTable клиентов / string sSqlGustoraer = "SEIECT CustoBierlD, СснпрапуНаше, ContactName FROM Customers";

SqlDataAdapter oOaCustomer = new SqlDataAdapter(sSqlCustQmer, oCn);

oDaCustofflB г. FH-KoDs, "Customer");

//- Заполняем OataTable заказов string sSqlOrder = "SELECT CustoroerlD, OrderlD, QrderDate FROM Orders";

SqlDataAdapter oDaOrder = new SqlDataAdapter(sSqlOrder, oCn);

oDaQrder.Fill(oOs, "0rder");

//- Заполняем OataTable позиций заказов string sSqlOrderQetail = "SiLECT od.OrderlO, od.ProductIO, p. ProductNaiee, " + " od.UnitPrice, od.Quantity FROM [Order Details] oel ""+ " INNER JOIN Products p 0*1 od.ProductIO = p.ProductXS";

SqlOataAdapter oOaQrderDetail = new SqlDataAdapter( sSqlOrderDetail, oCn);

;

.

oDaOrderDetail.fill(oDs. "OrderDetail");

Рис. 3. Создание объектов DataReiation //- Создаем DataReiation и связываем клиентов с заказами Dataftelation oDr_Customer20rder = new DataRelatiGn("Customer20rder" o&s.Tablest"Customer"].Columns["CustoflierID"1, oDs.Tables["Order"].Columns["CustomerID"]};

oDs. Relations. Add(oDr_Custoitier20 rder);

//' Создаем DataReiation и связываем заказы с позициями заказов DataReiation oBr_Grder20rderCtetail = new DataRelatian("Order20rderDetail", oOs.Tables["Order"].GoluHins["OrderID"], oOs.Tables["OrderDetail"].ColuinnsE"OrderIO"J);

oDs-Relations,Add(oDr_Order20rderDetail);

Объекты DataRelation в ADO.NET После заполнения DataSet тремя наборами записей и установления отно шений, связывающих объекты DataTable, DataSet можно показать в эле менте управления DataGrid на Web-форме, настроив свойство DataSource:

dataGridl.DataSource = oDs;

DataGrid достаточно «интеллектуален», чтобы понять: вы выводите не сколько объектов DataTable и нужно перемещаться по наборам записей в порядке, заданном объектами DataRelation.

Ограничения и каскадные операции Как и в базах данных, ограничения внешнего ключа (foreign key const raints) в объектах DataSet помогают обеспечивать целостность данных.

Когда я создаю DataRelation-объект Customer2Order (рис. 3), автомати чески создаются ограничение уникальности (unique constraint) для ключа родительской таблицы (customer.customerid) и ограничение внешнего ключа для дочерней таблицы (order.customerid). Ограничение внешнего ключа автоматически вступает в силу, когда DataSet отображается в Data Grid на Web-форме. Поэтому, если вы попытаетесь изменить значение внешнего ключа дочерней записи на значение, отсутствующее в родитель ском объекте DataTable, возникнет исключение, которое связано с наруше нием целостности данных и которое можно перехватить. Чтобы убедить ся в том, что это происходит благодаря ограничениям, можно отключить ограничения, присвоив false свойству EnforceConstraints объекта DataSet.

Но не советую делать это в других целях, кроме демонстрационных: от ключение ограничений позволит легко нарушить целостность данных и приложения.

Одно из новшеств SQL Server 2000 — поддержка каскадного обновления и удаления. Например, включив эту функцию в SQL Server 2000 и удалив родительскую запись, вы автоматически удалите и дочерние записи. В ADO.NET имеется аналогичная функциональность, контролируемая через свойства DeleteRuIe и UpdateRule объекта ForeignKeyConstraint. По умол чанию эти правила (rules) разрешают каскадное изменение связанных дан ных. Так, если вы измените значение CustomerlD в DataTable клиентов, соответствующим образом изменится и CustomerlD в DataTable заказов.

Это поведение можно изменить, присваивая свойствам одно из значений Rule перечислимого типа: Cascade (по умолчанию), None, SetDefault или SetNull.

Чтобы создать собственное ограничение внешнего ключа, определите объект ForeignKeyConstraint. Он позволяет указывать поля и правила пе ред применением ограничения. Вот как создать ForeignKeyConstraint и сопоставить его с DataRelation:

324 Доступ к данным из приложений ForeignKeyConstraint oFKey;

oFKey = new ForeignKeyConstraintC'CustomerForeignkey", oDs.Tables["Customer"].Columns["CustomerID"], oDs.Tables[ "Order"]. Columns["CustonierID"]);

oFKey.DeleteRule = Rule.Cascade;

oFKey.UpdateRule = Rule.Cascade;

oDs.Tables["Customer"].Const гаtnts.Add(oFKey);

oDs.EnforceConstralnts = true;

Родительские и дочерние наборы записей Работая с DataRelation, важно знать, как перемещаться вверх/вниз по ро дительским и дочерним наборам записей. Например, если вы хотите пере числить все заказы от определенного клиента, то реализовать это очень легко, потому что мы уже создали DataRelation, устанавливающий требу емое отношение. Как получить все дочерние записи и перечислить их, по казано на рис. 4.

Рис. 4. Перечисление дочерних записей //- Получаем дочерние записи для первого клиента Dataf)ow[] oRows = oDs.Tables["Custoroer"3.Rows[0],GetChildRows( oDr_Custoiner2Qrder);

string sHsg = "The orders for the first custoiaer are: \й";

//- Е1еребира0н дочерние записи для первого клиента for (int 1=0;

1 < ofiows.Length;

i++) i //- Добавляем в строку значения каадой из дочерних записей DataRow oRow = oRowstij;

sMsg +- "\t" + oSow"CustoiBerID"].ToString(} + " " + oRow["l>rderID"],ToStrinfl() + " " + oRow{"OrderDate"].ToStrlftg() + "\n";

} //- Показываем эти значения MessageBox.Snow(sNsg);

А как быть, если вам нужно найти одну или несколько родительских за писей DataTable? Если родительских записей несколько, применяется ме тод GetParentRows. Однако в нашем примере для каждого заказа может быть лишь одна родительская запись, поэтому я воспользовался методом Get Parent Row. В следующем фрагменте кода показано, как получить зна чения из родительского объекта DataTable клиентов по ссылке на первую запись в DataTable заказов:

DataRow oRow = oDs.Tables["Order"].Rows[0].GetParentRowC oDr_Customer20rder);

HessageBox.Show(oRow["CompanyName"].ToSt ring());

Объекты DataReiatio» в ADO.NET Заключение Еще одно преимущество ADO.NET — возможность представления данных в формате XML. Дело в том, что XML лежит в основе ADO.NET, поэтому DataSet легко преобразовать в XML. Используя методы WriteXml, Write XmlSchema, GetXml или GetXmlSchema объекта DataSet, вы получите XML-представление его внутренней структуры. Вызов метода GetXml для DataSet, используемого в моих примерах, вернет вам строку с XML-пред ставлением этого объекта. Метод GetXmlSchema возвращает структуру данных (их схему), а метод GetXml — и схему, и данные:

MessageBox.Show(oDs.GetXml(>};

Вы также можете создать DataSet, считав его из XML-строки или файла методами ReadXml и ReadXmlSchema. Например, сохранив структуру и данные DataSet в XML-файле методом WriteXml, вы сможете в дальней шем воссоздать DataSet из этого XML-файла методом ReadXml.

Ключевой момент, о котором следует помнить при использовании отноше ний в ADO.NET, заключается в том, что сначала вы должны определить, а нужны ли они вам. Отношения идеально подходят для приложений с иерархическим представлением данных. Однако, если вы хотите лишь по казать данные в одной сетке, несколько связанных наборов записей не дадут существенного выигрыша по сравнению с одним объединенным набором.

Джонни Папа {Johnny Papa) — вице-президент компании MJM Investigations по информационным технологиям {Роли, штат Северная Каролина). Автор нескольких книг по ADO, XML и SQL Server. Часто выступает на различных конференциях, в том числе на VSLive. С ним можно связаться по адресу datapoints@1ancelotweb.com.

Прийя Дхаван Разработка распределенных Операции над данными с иерархической структурой* В этой статье рассматриваются операции над иерархическими наборами строк с помощью ADO.NET. Исходный код к этой статье можно скачать с сайта MSDN Online Code Center по ссылке http://msdn.microsoft.com/code/ default.asp?URL=/code/sample.asp?url=/msdn-files/026/002/313/ msdncompositedoc.xml.

Введение Эта статья демонстрирует методику чтения и записи иерархических набо ров строк в источнике данных. В примерах кода, приведенных в этой ста тье, для соединения с базой данных Microsoft SQL Server или Microsoft Desktop Engine (MSDE) используется управляемый провайдер SQL (SQL managed provider). Для соединения с другими OLEDB-совместимыми ис точниками данных следует применять управляемый провайдер ADO (ADO managed provider).

Для доступа к иерархическим строкам, возвращаемым источником дан ных, в ADO,NET используются объекты DataReader и DataSet. Объект DataReader обеспечивает простой и быстрый доступ к данным только для чтения. С помощью этого объекта можно обращаться либо к иерархичес Priya Dhawan Building Distributed Applications with.NET. Data Operations on Hierarchical Row Daca//MSDN Library. 2002. February. - Прим. илд.

Операции над данными с иерархической структурой ким строкам данных, полученным в результате выполнения нескольких операторов SELECT, либо к XML-данным, возвращаемым SQL Server 2000. Объект DataReader позволяет читать данные только в направлении вперед (forward-only) и остается соединенным с базой данных, пока при ложение читает данные.

Объект DataSet, напротив, представляет отсоединенный реляционный кэш данных в памяти. Он может содержать несколько таблиц, рассматри ваемых как отдельные или связанные таблицы. Особенности этого объек та облегчают навигацию по связанным данным и их модификацию.

Кроме того, SQL Server.NET Data Provider позволяет получить XML-по ток напрямую от SQL Server 2000. Для этого предоставляется специальная API-функция, ExecuteXmlReader, доступная через объект SQLCommand.

Метод ExecuteXml Reader выполняет SQL-запрос применительно к SQL соединению и на основе XML, возвращенного запросом, создает объект XmlReader. ExecuteXmlReader используется только в выражениях, резуль татом которых являются XML-данные, и эффективен в запросах, где в выражениях с SELECT присутствует блок FOR XML.

Операции чтения Иерархические строки часто структурированы как несколько наборов свя занных строк. Для большей эффективности зачастую лучше извлекать несколько наборов строк за одно обращение к базе данных, чем по отдель ности запрашивать каждый из них. Обычно для этого выполняется пакет SQL-выражений (batch of SQL expressions) или хранимая процедура с не сколькими выражениями SELECT. Кроме того, если вы используете в опе раторе SELECT блок FOR XML, SQL Server 2000 возвращает иерархичес кие строки в виде XML.

Применение ADO.NET-объекта DataReader В следующем примере исполняется пакет из двух SQL-выражений SE LECT и соответствующие наборы результатов (result sets) извлекаются через объект DataReader. Этот объект обеспечивает навигацию по данным в направлении только вперед, так что для дальнейшей работы данные ско рее всего следует перенести в другой контейнер. Так как DataReader остав ляет соединение с базой данных открытым, с точки зрения масштабируе мости данные надо переносить быстро и как можно быстрее закрывать соединение. Передавать объект DataReader между уровнями распределен ного приложения нельзя.

Для перебора строки в наборах результатов применяются методы Next Result и Read объекта DataReader.

Доступ к данным из приложений Dim sqlCmd As SOLCommand Dim sqlDataRdr As SQLDataReader Dim fid As Integer Dim rptLine As String Try ' Подготовить объект команды, исполняющий хранимую процедуру, ' которая возвращает Orders и OrderDetails sqlCmd = New SQLCommand(} With sqlCmd.CommandType = CommandType.StoredProcedure,CommandText = "GetOrders".Connection - New SqlConnection(myConnString) End With ' Открыть соединение sqlCmd.Connection.Open(} ' Выполнить команду;

результат передается в DataReader sqlDataRdr = sqlCmd.ExecuteReaderQ ' Перебирать строки в первом наборе результатов Do While (sqlDataRdr.Read) ' Здесь со строкой можно выполнить какие-либо действия rptLine = For fid = 0 То sqlDataRdr.FieldCount - rptLine = rptLine & sqlDataRdr.Item(fld).ToString If fid < sqlDataRdr.FieldCount - 1 Then rptLine = rptLine & ", " End If Next End While Переместить указатель на следующий набор результатов If Not (sqlDataRdr.NextResult) Then Exit Do End If Loop Catch E As Exception Finally Закрыть DataReader sqlDataRdr.Close() End Try Примечание См. ''Example 1» в исходном коде BDAdotNetData4.vb {ссылка для скачивания указана в самом начале статьи).

Применение XmlReader и ADO.NET-объекта SqlCommand В Microsoft SQL Server 2000 встроена поддержка XML. Чтобы результаты выражений SELECT возвращались в виде XML, в этих выражениях нуж но указывать блок FOR XML. Метол ExecuteXmlReader объекта SQLCom mand позволяет получать XML-данные напрямую от SQL Server 2000.

Операции над данными с иерархической структурой ExecuteXmlReader возвращает объект System,XmLXmlReader, содержащий XML-данные, полученные от SQL Server 2000.

В следующем примере хранимая процедура, использующая FOR XML, запускается вызовом метода ExecuteXmlReader объекта SQLCommand.

Dim sqlConn As SqlConnection Dim sqlCmd As SqlCommand Dim xmlRdr As XmlReader Try Создать новый объект соединения sqlConn = New SqlConnection(myConnString) ' Создать новый объект команды sqlCmd = New SqlCommandQ Указать исполняемую команду With sqlCmd.ConwandType = CommandType.StoredProcedure.CoromandText = "GetOrdersXHL".Connection = sqlConn End With Открыть соединение sqlConn. OpenO ' Выполнить команду и извлечь строку в DataReader xmlRdr = sqlCmd.ExecuteXmlReaderC) ' Перейти к корневому элементу xmlRd г.MoveToContent() ' Что-то делаем с данными outXML = xmlRdr.ReadOuterXml ' Перейти к следующему элементу xmlRd r.MoveToElement() Считать атрибут Orderld текущего элемента xmlRd г.MoveToAtt ribute("Orde rid") Catch e As Exception ' Обработать исключение Finally sqlConn.Closet) End Try Примечание См. «Example 2» в исходном коде BDAdotNetData4.vb (ссылка для скачивания указана в самом начале статьи).

Применение ADO.NET-объекта DataSet Объект DataAdapter извлекает данные из источника и заполняет объекты DataTable внутри DataSet. Для выполнения запросов к базе данных объек ту DataAdapter требуется объект Connection.

330 Доступ к данным из приложений Если запрос возвращает несколько наборов результатов, DataSet сохраня ет каждый из них в отдельной таблице. Между таблицами может суще ствовать отношение (relationship).

Отношение между таблицами Как только вы связываете две таблицы в DataSet через отношение (с по мощью объекта DataRelation), навигация по ним упрощается. Кроме того, связывание облегчает выборку всех дочерних строк (объектов DataRow) одной таблицы для родительской строки в другой таблице (объекте Data Table). Для выборки дочерних строк используется перегруженный метод GetChildRows объекта DataRow.

Отношения устанавливаются созданием объекта DataRelation, который сопоставляет строки одной таблицы со строками другой. Эти отношения хранятся в объекте DataRelationCollection, который содержится в объек те DataSet.

Связывать таблицы в объекте DataSet не обязательно. Их можно оставить несвязанными. Однако, если между двумя таблицами существуют какие либо отношения, например через внешний ключ (foreign key relation), то связывание таблиц упростит доступ к дочерним строкам в одном объекте DataTable из родительской строки в другом объекте DataTable.

В следующем примере кода демонстрируется выборка заказов и их пози ций (order details) из таблиц Orders и OrderDetails. Объект DataSet содер жит таблицы Orders и Details, соответствующие таблицам в базе данных, Связью между двумя таблицами служит столбец Orderld, присутствую щий в обоих объектах DataTables.

Dim sqlDA As SqlDataAdapter Dim hierDS As DataSet Dim orderfiow As DataRow Dim detailflow As DataRow Dim detailRowsO As DataRow Dim 1 As Integer Try ' Создать новый объект DataAdapter sqlDA = New SqlDataAdapterQ ' Создать новый объект DataSet hierDS = New DataSetО ' Задать сопоставления таблиц sqlDA.TableMappings.Addf"Orders", "Orders") sqlDA.TableMappings.AddC'Orderst", "OrderDetails") With sqlDA Добавить объект SelectCommand.SelectCommand = New SqlCommandO Операции над данными с иерархической структурой ' Указать команду объекта SelectCommand With. SelectCominand.CommandType = CommandType.StoredProcedure. CormnandText = "GetOrders".Connection = New SqlConnection(myConnString) End With ' Заполнить DataSet возвращаемыми данными.FilKhterDS, "Orders") End With Так как до вызова метода Fill в объекте DataSet нет таблиц, объект SQL DataAdapter автоматически создает таблицы для DataSet и заполняет их возвращаемыми данными. Если таблицы созданы до вызова Fill, объект SQL Data Adapter просто заполняет существующие таблицы.

' Указать первичный ключ для таблиц luerDS.Tables("Orders"). PrimaryKey = New DataColumnQ { hierDS.Tables("Qrders").Coltimns("Orderld")} hierDS.Tables("OrderDetails").PrimaryKey = New DataColumn{) { hierDS.Tables("OrderDetails"),Columns("OrderDetailId")} ' Установить между двумя таблицами отношение через внешний ключ hierDS.Relations.Add("Order_Detail", hierDS.Tables("Orders").Columns("OrderId"), hierDS.Tables("OrderDetails").Columns("Orderld")) ' Выбрать один заказ из таблицы orderRow = hierDS.Tables("Orders").Rows(0) Выбрать соответствующие ему дочерние строки detailRows = orderRow.GetChildRows("Order_Detail") ' Работа с набором дочерних строк For i = 0 То detailRows.Length - detailRow = detailflows(i) Что-то делаем со строкой табпицы OrderDetails strDetail = detailRow("Order!d").ToString & ", " & detailRow("OrderDetailId").ToString & ", " & _

332 Доступ к данным из приложений Операции записи Фиксация (committing) изменений в иерархических данных, содержащих несколько наборов результатов из двух или более связанных таблиц, тре бует сохранения целостности данных. Например, ссылочная целостность означает, что внешний ключ в любой ссылающейся таблице (referencing table) должен указывать на существующую строку в таблице, на которую делается ссылка (referenced table). Следовательно, родительскую строку в этой таблице нельзя удалять до тех пор, пока на нее есть ссылка в другой таблице. Точно так же в ссылающуюся таблицу нельзя вставлять строки, если нет соответствующих строк в таблице, на которую она ссылается.

Так как ADO.NET-объект DataSet позволяет извлекать, обрабатывать и модифицировать данные в базе, он гарантирует ссылочную целостность таблиц при добавлении, изменении и удалении строк. Кроме того, этот объект позволяет выполнять каскадные обновления и удаления с сохране нием целостности данных.

Применение ADO.NET-объекта DataAdapter Метод Update объекта DataAdapter передает изменения, кэшированные в объекте DataSet, источнику данных. Для добавления новых строк Data Adapter использует InsertComrnand, для изменения строк — UpdateCom mand, а для удаления строк из базы данных — DeleteCommand. Когда вы вызываете метод Update, DataAdapter анализирует измененные строки и определяет, какой из объектов Command нужно выполнить для передачи отложенных изменений в каждой строке.

Прежде чем вызывать Update, вы должны настроить свойства InsertCom mand, UpdateCommand или DeleteCommand — в зависимости от того, ка кие изменения были внесены в данные в DataSet. Например, если из DataSet удалялись строки, следует установить свойство DeleteCommand.

Для автоматического формирования команд Insert, Update и Delete мож но задействовать преимущества объекта Command Builder. Если вы указы ваете DataAdapter-свойства InsertCommand, UpdateCommand или Delete Command, метод Update соответственно выполняет команды insert, update или delete для каждой вставленной, обновленной или удаленной строки в DataSet. В ином случае CommandBuilder — в зависимости от значения свойства SelectCommand объекта DataAdapter — генерирует SQL-коман ды, необходимые для внесения изменений в базу данных. Поэтому, чтобы CommandBuilder генерировал команды Insert, Update и Delete, вы долж ны соответственно настроить свойство SelectCommand.

Лучше всего самостоятельно указывать InsertCommand, DeleteCommand или UpdateCommand, поскольку это позволяет явно контролировать, как Операции над данными с иерархической структурой выполняется обновление, и повышает производительность по сравнению с тем случаем, когда команды генерируются автоматически. Это особенно важно, если ваш код за одно обращение к источнику данных выполняет операции над несколькими строками, чтобы сократить частоту обмена дан ными с сервером.

Для свойств InsertCommand. UpdateCommand и DeleteCommand объекта DataAdapter можно указывать параметризованные запросы или хранимые процедуры. Параметры в параметризованных запросах или процедурах соответствуют столбцам в объекте DataTable. Таким образом, один объект DataAdapter поддерживает обновления только одной таблицы в вашей базе данных. Поэтому при обновлении базы данных для каждой таблицы в объекте DataSet потребуется отдельный объект DataAdapter.

Добавление строк В дочернюю таблицу строки можно добавлять, только если они соответ ствуют допустимым (уже существующим) строкам родительской таблицы.

Строки, не соответствующие допустимым строкам родительской таблицы, добавлять нельзя, Новую строку нужно вставить сначала в родительскую таблицу;

только после этого в дочернюю таблицу можно добавлять соответствующие стро ки. По той же причине в процессе синхронизации изменений с базой дан ных метод Update объекта DataAdapter, соответствующего родительской таблице, следует вызывать первым.

Автоматическое формирование команды Insert Проблема с автоматически генерируемыми командами вставки в том, что объекту DataSet не возвращается первичный ключ Id для столбца Identity.

Для решения этой проблемы мы используем хранимую процедуру, которая возвращает первичный ключ для родительской строки, Тогда появляется возможность применять автоматически формируемые команды вставки для дочерних строк. В следующем примере мы создаем два объекта Data Adapter, заполняющие две таблицы в одном объекте DataSet. Мы задаем отношение между этими двумя таблицами и вставляем в них новые стро ки. Метод Update объекта DataAdapter, соответствующего родительской таблице (в нашем случае — Order), вызывается первым. Затем вызывает ся метод Update объекта DataAdapter, соответствующего дочерней табли це (в нашем случае — OrderDetails).

Dim sqlConn As SQLConnection Dim sqlDAOrder As SqlDataAdapter Dim sqlDADetail As SqlDataAdapter 334 Доступ к данным из приложений Dim hierDS As DataSet Dim sqlCmdBldrDetail As SqlCommandBuilder Dim orderRow As DataRow Dim detailflow As DataRow Try ' Создать новый объект SQLConnection sqlConn = New SqlConnection(Common.getConnectionString) ' Создать новый объект SQLDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapterQ ' Создать новый объект SqlDataAdapter для таблицы OrderDetails sqlDAOetail = New SqlDataAdapterO ' Создать новый DataSet hierDS = New DataSetO ' Создать новый объект SQLCommandBuilder для автоматической генерации ' выражений Update sqlCmdBldrDetail = New SqlCommandBuilder(sqlDADetail) With sqlDAOrder ' Добавить объект SelectCommand.SelectCommand = New SqlComraandf) Указать команду Select With.SelectCommand.CommandType = CommandType.Text.CommandText = "Exec GetOrderHeader @0rderld=-1".Connection = sqlConn End With ' Добавить объект InsertCommand.InsertCommand = New SqlCommand() ' Указать команду Insert With.InsertCommand.CommandType = CommandType.StoredProcedure.CommandText = "InsertQrderHeader".Connection = sqlConn ' Определить параметры параметризованного запроса Insert.Parameters.Add (New SqlParameter("@CustomerId", SqlDbType.Int)) ' Задать свойство Direction.Parameters("@CustomerId").Direction = ParameterDirection.Input Задать свойство SourceColumn.Paranteters("@CustomerId").SourceColunm = "Customerld".Parameters,Add (New SqlParameter("@OrderDate", SqlDbType.DateTime)} ' Задать свойство Direction.Parameters("@OrderDate").Direction = ParameterDirection.Input ' Задать свойство SourceColumn,Parameters("§OrderDate").SourceColumn = "OrderDate".Parameters.Add (New SqlParameter("@OrderId", SqlDbType.Int)) Задать свойство Direction.Parameters("@0rderld").Direction = ParameterDirection.Output ' Задать свойство SourceColumn Операции над данными с иерархической структурой.Parameters("eOrderId").SourceColLimn = "Orderld" End With ' Заполнить таблицу Orders данными.Fill(hierDS, "Orders") End With With sqlDADetail Добавить объект SelectCommand.SelectCommand = New SqlCommand() Указать команду Select With.SelectCommand,CommandType = CommandType.Text.CommandText = "Exec GetOrderDetails @0rderld=-1".Connection = sqlConn End With Заполнить таблицу Details данными,Fill(hierDS, "Details") End With ' Установить связь между таблицами hierDS.Relations.AddC'Qrderjtetail", _ hierDS.Tables("Orders").Columns("OrderId"), hierDS.Tables("Details"}.Colunms("Orderld")) ' Создать новую строку для таблицы Orders orderRow = hierDS.Tables("Orders").NewRow() Указать значения каждого столбца в таблице Orders orderRow.Item("CustomerId") = orderRow.ItemC'OrderStatus") = orderRow.Item("0rderdate"} = Now() ' Добавить строку к OataSet hierDS.Tablesf"Orders").Rows.Add(orderRow) Синхронизировать изменения с источником данных sq!DAOrder.Update(hierDS, "Orders") Создать новую строку для таблицы Details detailRow = hierDS.Tables("Details").NewRow() detailRow.ItemC'Orderld") = orderRow.Item("0rderld") detailRow.Item("ltemld") = Добавить строку к DataSet hierDS.Tables{"Details").Rows.Add(detailRow) ' Синхронизировать изменения с источником данных sqlDADetail.Update(hierDS, "Details") Catch e As Exception ' Обработать исключение Finally Выполнить очистку End Try Примечание См. «Example 4- в исходном коде BDAdotNetData4.vb {ссылка для скачивания указана в самом начале статьи).

Доступ к данным из приложений Обратите внимание: если новые строки, добавленные к дочерней таблице, соответствуют новой строке, добавленной к родительской таблице, то для сохранения целостности данных сначала обновляется родительская табли ца. Дочерняя строка всегда должна ссылаться на допустимую строку в родительской таблице.

Использование свойства InsertCommand В этом примере заголовок заказа и его позиции передаются хранимой про цедуре в виде XML, что позволяет выполнить транзакцию за одно обраще ние к базе данных.

Чтобы указать собственное выражение INSERT, выполняемое при вызове метода Update применительно к Data Adapter, следует задать свойство InsertCommand. Значением этого свойства может быть параметризирован ный запрос или хранимая процедура. Параметры InsertCommand опреде ляются так же, как и параметры объекта Command. Управляемый провай дер SQL поддерживает именованные параметры.

Для каждого параметра нужно установить свойство SourceCoIumn. Оно сообщает объекту DataAdapter, в каком столбце таблицы содержится зна чение параметра.

Dim sqlConn As SqlConnection Dim sqlDAOrder As SqlDataAdapter Dim sqlDADetail As SqlDataAdapter Dim hierDS As DataSet Try ' Создать новое соединение sqlConn = New SqlConnection(Common.getConnectionString} ' Создать новый объект SqlDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapter() Создать новый объект SqlDataAdapter для таблицы OrderDetails sqlDADetail = New SqlDataAdapterf) Создать новый DataSet hierDS = New DataSetO With sqlDAOrder ' Добавить объект SelectCommand. SelectCommand = New SqlCommandO ' Указать команду Select With.SelectCommand.CommandType = CommandType.Text.CommandText = "Exec GetOrderHeafler §0rderld=-1".Connection = sqlConn End With Добавить объект InsertCommand.InsertCommand = New SqlCommandO ' Указать команду Insert Операции над данными с иерархической структурой With.InsertCommand.CommandType = CommandType.StoredProcecJure.CommandText = "InsertOrder".Connection = sqlConn ' Задать параметры параметризованного выражения Insert.Parameters.Add (New SqlParameter("@Order", SqlDbType.NVarChar, 4000)) Установить свойство Direction.Parameters("@0rder").Direction = ParameterDirection.Input.Parameters.Add (New SqlParameter("@OrderId", SqlDbType.Int)) Установить свойство Direction,Parameters("@OrderId").Direction = ParameterDirection.Output ' Установить свойство SourceColumn,Parameters("@QrderId").SourceColumn = "Orderld" End With ' Заполнить DataSet возвращенными данными.Fill(hierDS, "Orders") End With With sqlDADetail ' Добавить объект SelectCommand.SelectCommand = New SqlCommandO ' Указать команду Select для объекта sqlDADetail With.SelectCommand.CommandType = CommandType.Text.CommandText = "Exec GetOrderDetails @OrderId=-T.Connection = sqlConn End With Заполнить DataSet возвращенными данными.FllKhlerDS. "Details") End With Установить между двумя таблицами отношение через внешний ключ hierDS.Relations.Add("Order_Detall", hierDS.TablesC"Orders").Columns("Orderld"), _ hierDS.Tables("Details").Columns("Orderld")) ' Создать новую строку в таблице Orders orderRow = hierDS.Tables("Orders"),NewRow() Задать значение каждого столбца в таблице Orders orderRow.Item("Orderld") = - orderRow.ItemC'Customerld") = orderRow.Item("OrderStatus") = ' Добавить строку к DataSet hierDataSet.Tables("Orders").Rows.Add(orderRow) ' Создать новую строку в таблице Details detailRow = hierDataSet.Tables("Details"). NewRow() detailRow.Item("0rderld") = orderRow.Item("0rderld") detailRow.Item("ltemld") = ' Добавить строку к DataSet 338 Доступ к данным из приложений hierDataSet.Tables("Details").Rows.Add(detailRow) ' Создать новую строку в таблице Details detailRow = hierDataSet.Tables("Details").Newflow() detailRow.Item("OrderId") = orderRow.Item("0rderld") detaiinow.ltem("ltemld") = Добавить строку к DataSet hierDataSet.Tables("Details").Rows.Add(detailRow) sqlOAOrder.InsertCommand.Paraiiieters("@Order"). Value = _ hlerDataSet.QetXnlO ' Синхронизировать изменения с источником данных sqlDAOrder.Update(hierDataSet, "Orders") Catch E As Exception ' Обработать исключения Finally ' Выполнить очистку End Try Примечание См. «Example 5» в исходном коде BDAdotNetData4.vb (ссылка для скачивания указана в самом начале статьи).

Обновления При обновлениях строк в связанных таблицах должна сохраняться целос тность данных. Для обеспечения ссылочной целостности дочерняя стро ка должна ссылаться на допустимую строку в родительской таблице. При этом можно использовать каскадное обновление.

Автоматическая генерация команды Update Изменения передаются источнику данных после обновления строки в таб лице объекта DataSet и вызова метода Update объекта DataAdapter. Пос ледний автоматически генерирует команду Update на основе предостав ленной вами команды Select.

В следующем примере кода мы переносим позиции из одного заказа в дру гой, демонстрируя, как осуществляется каскадное обновление.

Dim sqlConn As SqlConnection Dim sqlDAOrder As SqlOataAdapter Dim sqlDADetail As SqlDataAdapter Dim sqlCmdBldrDetail As SqlCommandBuilder Dim hierDS As DataSet Try Создать новое соединение sqlConn = New SqlConnection(Common.getConnectionString) Создать новый объект SqlDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapter() ' Создать новый объект SqlDataAdapter для таблицы OrderDetails Операции над данными с иерархической структурой sqlDADetail = New SqlDataAdapter() • Создать новый DataSet hierDS = New DataSet() ' Создать новый объект SQLCommandBuilder, автоматически формирующий ' команды Update sqlCmdBldrDetail = New SqlComrnandBuilder(sqlDADetail) With sqlDAOrder Добавить объект SelectComntand.SelectCommand = New SqlCommandO ' Указать команду Select With.SelectCommand,CommandType = CommandType.Text.CommandText = "Exec GetQrderHeader @0rderld=2".Connection = sqlConn End With ' Заполнить DataSet возвращенными данными.FilKhierOS. "Orders") End With With sqlDADetail • Добавить объект SelectCommand,SelectCommand = New SqlCommandO ' Указать команду Select With.SelectCommand.CommandType = CommandType.Text.CommandText = "Exec GetOrderDetails e0rderld=2".Connection = sqlConn End With ' Заполнить DataSet возвращенными данными.Fill(hierDS, "Details") End With Установить между двумя таблицами отношение через внешний ключ hierDS.Relations.Add("0rder_Detail", _ hierDS.Tables("Orders").Columns("OrderId"), _ hierDS.Tables("Details").Columns("OrderId")) hierDS.Tables("Orders").Columns("OrderId").Readonly = False ' Перенести позиции из одного заказа в другой orderRow = hierDataSet.Tables("Orders").Rows(O) orderRow("OrderId") = ' Синхронизировать изменения sqlDADetail.Update(hierDS, "Details") Catch E As Exception Обработать исключение Finally Выполнить очистку End Try Примечание См. «Example 6» в исходном коде BDAdotNetData4.vb (ссылка для скачивания указана в самом начале статьи).

Доступ к данным из приложений Использование свойства UpdateCommand Автоматически сформированная команда перемещала каждую строку за одно обращение к базе данных, а хранимая процедура могла бы перемес тить за одно обращение все строки. Чтобы указать собственное выражение Update, исполняемое при вызове метода Update применительно к Data Adapter, задайте свойство UpdateCommand. Его значением может быть параметризированный запрос или хранимая процедура. Параметры Up dateCommand определяются так же, как и параметры объекта Command.

Dim sqlConn As SqlConnection Dim sqlDAOrder As SqlDataAdapter Dim sqlDADetail As SqlDataAdapter Dim hierDS As DataSet Try Создать новое соединение sqlConn = New SqlConnection(Common.getConnectionString) Создать новый объект SqlDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapterf) ' Создать новый объект SqlDataAdapter для таблицы QrderDetails sqlDADetail = New SqlDataAdapterf) ' Создать новый DataSet hierDS = New DataSetQ With sqlDAOrder ' Добавить объект SelectCommand. SelectCommand = New SqlCommand() ' Указать команду Select With.SelectCommand.CommandType = CommandType.Text.CommandText = "Exec GetOrderHeader @0rderld=1".Connection = sqlConn End With ' Добавить объект UpdateCommand.UpdateCommand = New SqlCommandO ' Указать команду Update With.UpdateCommand.CommandType = CommandType.StoredProcedure.CommandText = "HoveOrderDetails" ' Указать параметры.Parameters.Add{New SqlParameter("§FromOrderId", SqlDbType.Int».Parameters("@FromOrderId").Direction = ParameterDirection.Input.Parameters("eFromOrderIci").SourceColumn = "Orderld". Parameters("@FromOrderId").SourceVerslon = DataRowVersion.Original.Parameters.Add(New SqlParameter("@ToOrderId", SqlDbType.Int».Parameters("@ToOrderid").Direction = ParameterDirection.Input.Parameters("@ToOrderId").SourceColuinn = "Orderld".Parameters("@ToOrderId").SourceVersion = DataRowVersion.Current.Connection = sqlConn Операции над данными с иерархической структурой End With ' Заполнить DataSet возвращенными данными,Fill(hierDS, "Orders") End With With sqlDADetail ' Добавить объект SelectCoromand sqlDADetail.SelectCommand a New SqlCommandO Указать команду Select With.SelectCommand.CommandType = CommandType.Text.ComrnandText = "Exec GetOrderDetails @0rderld=1".Connection = sqlConn End With Заполнить DataSet возвращенными данными.FilKhierDS, "Details") End With Установить между двумя таблицами отношение через внешний ключ hierDataSet.Relations.Add("Order_Detail", hierDS,Tables("Orders").Columns("OrderId"), hierDS.Tables("Details").ColumnsC"OrderId")) hierDS.Tables("Qrders").Columns("OrderId").Readonly = False ' Перенести позиции из одного заказа в другой orderRow = hierDS.Tables("Orders").Rows(Q) orderRowC'Orderld") = ' Синхронизировать изменения sqlDAOrder.Update(hierDS, "Orders") Catch E Аз Exception Обработать исключение Finally Выполнить очистку End Try Примечание См. «Example 7» в исходном коде BDAdotNetData4.vb (ссылка для скачивания указана в самом начале статьи).

Свойство SourceVersion позволяет передавать исходное и текущее значе ние Orderld в соответствующие параметры хранимой процедуры.

Удаление Удаляя иерархические данные, вы должны позаботиться о сохранении це лостности базы данных. Каждая дочерняя строка должна ссылаться на допустимую строку в родительской таблице. Поэтому удалять строку из родительской таблицы нельзя до тех пор, пока существует хотя бы одна связанная с ней строка в дочерней таблице.

В ADO.NET объект DataSet поддерживает каскадное удаление, которое позволяет удалять дочерние строки одновременно с соответствующими родительскими строками. Чтобы определить действия, необходимые при 342 Доступ к данным из приложений каких-либо изменениях строк в таблицах, вы должны указать ограничения внешнего ключа для двух таблиц в DataSet. Настройте метод DeletePro perty свойства ForeignKeyConstraint на один из подходящих режимов ра боты (по умолчанию операции выполняются в каскадном режиме).

Dim sqlConn As SqlConnection Dim sqlDAOrder As SqlDataAdapter Dim sqlDADetail As SqlDataAdapter Dim hierDS As DataSet Dim sqlCmdBldrOrder As SqlCommandBuilder Dim sqlCmdBldrDetail As SqlCommancfBuilder Try ' Создать новый SQLConnection sqlConn = New SqlConnection(Common.getConnectionString) ' Создать новый объект SqlDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapterO ' Создать новый объект SqlDataAdapter для таблицы OrderDetails sqlDADetail = New SqlDataAdapterO ' Создать новый объект DataSet hierDS = New DataSetO ' Создать объекты CommandBuilder, автоматически формирующие команды sqlCmdBldrOrder = New SqlCommandBuilder(sqlDAOrder) sqlCmdBldrDetail = New SqlCommandBuilder(sqlDADetail) With sqlDAOrder Добавить объект SelectCommand sqlDAOrder.SelectContmand = New SqlCommandO ' Указать команду Select With.SelectCommand.CommandType = CommandType.StoredProcedure.CommandText = "GetOrderHeaders".Connection = sqlConn End With Заполнить DataSet возвращенными данными.Flll(hierDS, "Orders") End With With sqlDADetail ' Добавить объект SelectCommand sqlDADetail.SelectCommand = New SqlCommandO ' Указать команду Select для объекта sqlDADetail With.SelectCommand.CommandText = "Select * from OrderDetails".Connection = sqlConn End With Заполнить DataSet возвращенными данными.FllKhierDS, "Details") End With Установить связь между таблицами hierDS.Relations.Add("Order_Detall", hierDS.Tables("Orders").Columns("OrderId"), Операции над данными с иерархической структурой hierDS.Tables("Details"J.ColumnsC'Orderld")) ' Найти последнюю строку в таблице Orders orderRow = hierDS.Tables("Orders").Rows.

Item(hierDS.Tables("Orders").Rows.Count - 1) ' Удалить строку из таблицы Orders'. В результате этого автоматически удаляются соответствующие дочерние строки ' из таблицы Details в DataSet.

orderRow. DeleteO ' Синхронизировать изменения с источником данных sqlDADetail.Update(hierDS, "Details") sqlDAOrder.Update(hierDS, "Orders") Catch E As Exception ' Обработать исключение Finally ' Выполнить очистку End Try Примечание См. «Example 8» в исходном коде BDAdotNetData4.vb (ссылка для скачивания указана в самом начале статьи).

Для поддержания целостности данных дочерние строки должны удалять ся первыми. Удалить родительские строки до удаления соответствующих дочерних строк нельзя. Поэтому обновление Data Adapter-объекта sqlDA Detail для таблицы Details выполняется до обновления DataAdapter объекта sqlDAOrder.

Использование свойства DeleteCommand Чтобы указать собственное выражение Delete, выполняемое при вызове метода Update применительно к DataAdapter, используйте свойство Dele teCommand. Его значением является параметризированный запрос или хранимая процедура. Параметры для DeleteCommand определяются так же, как и для объекта Command.

Для каждого параметра нужно задать свойство SourceColumn. Оно сооб щает DataAdapter, в каком столбце содержится значение параметра.

Dim sqlConn As SqlConnection Dim sqlDAOrder As SqlDataAdapter Dim sqlDaOetail As SqlDataAdapter Dim hierDS As DataSet Try ' Создать новый SQLConnection sqlConn = New SqlConnection(Cominon.getConnectionString) Создать новый объект SqlDataAdapter для таблицы Order sqlDAOrder = New SqlDataAdapterO ' Создать новый объект SqlDataAdapter для таблицы OrderOetails 344 Доступ к данным из приложений sqlDaDetail = New SqlDataAdapterf) ' Создать новый DataSet hierDS = New DataSetO With sqlDAOrder ' Добавить объект SelectCommand.SelectCommand = New SqlCommandO Указать команду Select With.SelectCommand.CommandType = CommandType.StoredProcedure.CommandText = "GetOrderHeaders".Connection = sqlConn End With ' Добавить объект DeleteCommand.DeleteCommand = New SqlCommandO ' Задать свойства DeleteCommand With.DeleteCommand,CommandType = CommandType.StoredProcedure.CommandText = "DeleteOrder".Connection = sqlConn ' Определить параметры хранимой процедуры.Parameters.Add(New SqlParameter("@OrderId", SqlDbType,Int)) ' Настроить свойство SourceColumn.Parameters("@OrderId").SourceColumn = "Orderld" End with ' Заполнить DataSet возвращенными данными.Fill(hierDS, "Orders") End With With sqlDaDetail ' Добавить объект SelectCommand.SelectCommand = New SqlCommandO Указать команду Select With.SelectCommand.CommandType = CommandType.Text.CommandText = "Select * from QrderDetails".Connection = sqlConn End With ' Заполнить DataSet возвращенными данными.Fill(hierDS, "Details") End With ' Установить связь между двумя таблицами Найти последнюю строку в таблице Orders orderRow = hierDS.Tables("0rders").Rows.

Item(hierDS.Tables("Orders").Rows.Count - 1) ' Удалить строку из таблицы Orders. В результате автоматически ' удаляются соответствующие дочерние строки из таблицы ' Details в DataSet.

orderRow. DeleteO Операции над данными с иерархической структурой ' Синхронизировать изменения с источником данных sqlDAOrder.Update(hierDS, "Orders") Catch E As Exception ' Обработать исключение Finally ' Выполнить очистку End Try Примечание См. «Example 9» в исходном коде BDAdotNetData4.vb {ссылка для скачивания указана в самом начале статьи).

Заключение При доступе к данным только для чтения объекты DataReader и Xml Reader просты в использовании и работают быстро, хотя соединение с ба зой данных поддерживается до тех пор, пока приложение читает данные.

Если приложение достаточно долго держит эти объекты, может возник нуть конкуренция (contention), что ограничит масштабируемость такого приложения.

Объект DataSet представляет отсоединенный реляционный кэш, а также упрощает навигацию по иерархическим данным и их модификацию. Кас кадная запись облегчает фиксацию изменений в базе данных, но автома тически генерируемые выражения Insert, Update и Delete менее эффектив ны по сравнению с теми, которые пишутся вручную, — особенно с точки зрения уменьшения частоты обращений к базе данных и кэширования зап росов. Сокращение числа обращений к базе данных станет еще актуальнее, когда XML-средства SQL Server 2000 будут полнее использовать возмож ности ADO.NET.

Джонни Папа Доступ к данным Модификация приложения для отображения данных в Web Автор описывает, как создать универсальный инструмент для отображения данных в Web с поддержкой сортировки, управления страницами и несколь ких других функций на основе ADO. Все это может быть реализовано в среде СОМч или Microsoft Transaction Services (MTS).

От однообразия тупеешь. Меня раздражает без конца писать одно и то же, зная что код можно было бы использовать повторно. Например, на Web-сайтах нужно отображать результаты поиска, данные отчетов и дру гие списки. Хорошо бы упростить разработку всех этих страниц! Сегодня я покажу, как ускорить создание Web-страниц для отображения данных с использованием ASP, Visual Basic, COM+ и JScript. Так что сядьте в крес ло и откиньтесь - мы начинаем.

В декабрьском издании «Microsoft Internet Developer» за 1998 г. Чарльз Кейсон (Charles Caison) и я рассказали о методике отображения любых данных SQL на Web-странице. Читатели просили меня обновить код с учетом более современных технологий создания масштабируемых корпо ративных Web-приложений. (Да, я слышал.) Поэтому в своей первой ста тье в «MSDN Magazine» я опишу, как создать универсальный инструмент для отображения данных в Web с поддержкой сортировки, управления страницами и нескольких других, новых функций. Все это реализуется в среде СОМ+ или Microsoft Transaction Services (MTS).

* Публиковалось в MSDN Magazine. 2001. X°6 (июнь). — Прим. изд.

Модификация приложения для отображения данных в Web Начнем с результатов — о коде поговорим потом (его, кстати, можно ска чать с сайта MSDN Magazine по ссылке http://msdn.microsoft.com/msdn mag/codeOl.asp в разделе за июнь). Мое решение рассчитано на поддерж ку разных браузеров и работает с последними версиями Internet Explorer и Netscape Navigator.

Страница На рис. 1 показан пример универсальной Web-страницы, использующей ADO-функции управления страницами (paging) и сортировки, а также многие средства JScript и DHTML. Обратите внимание: на каждой стра нице отображается лишь пять записей. Пользователи могут листать стра ницы при помощи ссылок Previous и Next или вводом номера страницы в поле со списком, которое находится в верхнем правом углу страницы.

Функция управления страницами позволяет настраивать число единовре менно отображаемых записей и перемещаться между страницами в любом направлении. Так, если на первой странице с данными (рис. 1) пользова тель щелкнет ссылку Next, браузер выведет вторую страницу (рис. 2).

jJAulhiiri D.suldji Miuiusi.H fnterrml EH|I|OIHI Authors Display Сортировка 1409-56-7006 Abraham Sennet Berime) О Oft J648-9?-1372 Reginald BlotcriBt-Halls Corvallis J23B-9S-7766 Cheryl Carion Berksl*) J72?-S1-54E4 Michel DeFrance Gary 1713-45-1667 Innes del Castillo йпп Arbor Управление страницами Рис. 1. Отображение данных На рис. 1 показаны первые пять записей из отчета, на рис. 2 — следующие пять. Можно перейти на определенную страницу, выбрав ее номер из спис ка Go to page (рис. 3).

Число записей на одной странице указывается пользователем в текстовом поле Records per page. Чтобы изменить его, введите новое число и щелк ните ссылку Refresh в верхнем левом углу страницы. Это приводит к из менению числа записей на странице;

кроме того, запоминается текущий Доступ к данным из приложений 346-92-7186 Sheryl Hunter Pski Alto СД Рис. 2. Вторая страница Рис. З. Переход по номеру страницы номер отображаемой страницы данных. Например, пользователь находил ся на второй странице отчета из 25 записей, просматривая по пять запи сей. Затем он увеличил число записей на одной странице с пяти до семи и щелкнул Refresh. Теперь на каждой странице отображается по семь запи Модификация приложения для отображения данных в Web сей (рис. 4), и весь отчет занимает четыре страницы, но пользователь по прежнему находится на второй странице отчета. Однако, если пользова тель увеличивает число записей при просмотре последней страницы (ска жем, пятой из пяти), то отображается уже не пятая, а последняя страница отчета. Но вы, как разработчик, можете изменить эту схему в соответствии с тем, что нужно вам.

Mir кипи lnlfii«-t 1 wtoi 3 AulhiiK Dispidy Nasfw 5Z7-?2-3M6 Monvngstar Greene Purr 5J6-92-7106 Sheryl Hunter Paid Alto CA 756-30-7391 Livia i-arsen Oaklird Си 486-29-1766 Charlene Lockslev San Francisco СЙ MacFssther Oakland Рис. 4. Семь записей на странице Сортировка данных Механизм сортировки ADO позволяет упорядочивать информацию по возрастанию, когда пользователь щелкает гиперссылку заголовка колонки с данными. Повторный щелчок упорядочивает данные по убыванию. На пример, на рис. 5 информация отсортирована по фамилиям авторов в ал фавитном порядке по убыванию.

Ссылки на основе строк Если вам нужно отредактировать данные, просто щелкните кнопку Edit, чтобы перейти на другую страницу, где эти данные можно модифициро вать. В каждой строке можно указать ссылки для перехода в окно редак тирования, вывода детальных сведений, заполнения формы для отправки по электронной почте и выполнения любых других нужных вам действий.

Например, если на рис. 6 щелкнуть кнопку Edit в записи Akiko Yokomoto, появится сообщение, показывающее первичный ключ этой записи. Други 350 Доступ к данным из приложений ми словами, вы можете извлечь первичный ключ записи и передать его в другое окно для отображения или изменения записи и т. д.

;

'-* •^•'.з ^ - ;

^.- :..

1лУ «Л -У '....

Authors Display,j Рис. 5. Сортировка по фамилиям Л AutNut Ш«4в« Authors Display Рис. 6. Получение первичного ключа Модификация приложения для отображения данных в Web Помните, что все эти функции адаптируемы. А теперь, когда вы увидели, что позволяет делать моя программа, разберемся, как она работает.

За кулисами У кода многоуровневая архитектура: база данных (я использовал Pubs из комплекта поставки Microsoft SQL Server 7.0 и SQL Server 2000), бизнес объекты (ActiveX-сервер на Visual Basic под MTS), ASP- и клиентский код (рис. 7). ASP- и HTML-код, а также сценарии помещены в одну Web-стра ницу. Но ASP-страницы логически разделяют презентационные и бизнес сервисы. (В ASP.NET подход несколько иной, но это тема для отдельного разговора.) DHTMUJScript t Интернет/LAN WTS/COM* Бизнес-правила Доступ к данным Уровень данных :Бзза данных Рис. 7. Многоуровневая архитектура ASP- и клиентский код Рассмотрим основные компоненты приложения, начиная с верхнего уров ня (ASP/HTML). Проект состоит из файлов View_Authors.asp, Generic Display ToolPartlofS.asp, GenericDisplayToolPart2of3.asp, GenericDisplay ToolPartSofS.asp и Styles.css. Файл View_Authors.asp — Web-страница, где пользователи вводят URL для запуска программы.

Доступ к данным из приложений Это единственная страница приложения, требующая настройки. (Осталь ные страницы универсальны, и их можно использовать повторно для лю бых наборов данных без всяких изменений.) Как же ее настроить? Для этого укажите заголовок страницы и картинки, которые надо выводить в каждой строке, а также определите обработчики событий, инициируемых щелчком этих картинок. Потом напишите код для выборки данных из сво его СОМ-объекта.

Базовые настройки для заголовка и картинок показаны на рис. 8. При щелчке первой картинки вызывается клиентская функция Edit, а при щел чке второй — функция Browse. Создайте код для этих функций и включи те в страницу. Пример таких функций приведен на рис. 9. В данном слу чае выводится сообщение с указанием первичного ключа выбранной запи си. Значение первичного ключа устанавливается автоматически другими страницами, о чем я расскажу чуть позже. Просто замените мой код для функций Edit и Browse на нужный вам. А изменив код в обработчике со бытия onclick (рис. 8), вы сможете вызывать любую другую клиентскую функцию.

Рис, 8. Настройка картинок '- Заголовок страницы sTitle = "Authors Display" '- Страница для вывода sFormAction = "Vlew_Authors.asp" '- JScrlpt-функция., запускаемая при щелчке в строке sOnRowClick.Prlitiary = "Edit(>;

" *- Картинка в строке sfiowImageSsurce^Primary = "Edit.glf" - JSeript-функция, запускаеная при щелчке в строке sQnRowClick_Secondary "BrowseО;

" '- Картинка в строке sflow!mageSotjrc9SecQndary = "Browse.gif" Рис. 9. Клиентские обработчики щелчков в строке

© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.