Транзакции только помогают сохранить непротиворечивость базы данных. Если вы переводите деньги со сберегательного счета на текущий для оплаты счета за телефон, транзакции помогут гарантировать, что деньги будут сняты с одного счета и появятся на другом (или же не произойдет ни того, ни другого). Вы не столкнетесь ни с ситуацией, когда деньги придут на текущий счет, но не будут сняты со сберегательного (это было бы неплохо для вас, но плохо для банка), ни с противоположной ситуацией (неприятной для вас, но хорошей для банка). Ничто не помешает вашей супруге потратить эти деньги на ужин в модном ресторане.
При оптимистическом блокировании предполагается, что ничего подобного не случится, но вам следует быть готовым к такой ситуации, если она все-таки возникнет. Использование пессимистического блокирования требует координации действий всех пользователей таблицы базы данных таким образом, чтобы предотвратить подобную ситуацию. Разумеется, чем меньше блокировок накладывается на столбец базы данных, тем шире возможности использования вашего приложения.
Следует понимать, что такая ситуация влияет и на считывание данных, и на их обновление. Скажем, если ваша супруга видит, что на счету есть деньги, и строит свои планы относительно этих денег, это может привести к не меньшим проблемам, чем просто потеря денег с общего текущего счета.
Хотя обсуждение способов решения таких проблем выходит далеко за пределы рассматриваемого в этой главе материала, важно помнить, что они возникают, если записи, считанные в объект DataSet (Набор данных), не блокированы. Использование SqlDa-taAdapter при работе с DataSet (Набор данных) предполагает применение оптимистической стратегии блокировки.
Почему это так важно? Прежде всего потому, что от этого зависит производительность и масштабируемость вашего приложения. А почему это так сложно? Потому что нельзя дать совета, подходящего для всех приложений в любой ситуации. Когда пользователи не обращаются одновременно к одним и тем же данным, использование оптимистической стратегии блокировок является наилучшим вариантом. Если необходимо заблокировать доступ к записи на долгий период времени, время ожидания получения доступа к этой записи значительно увеличится, понижая тем самым производительность и масштабируемость приложения.
Вы должны понимать, что такое уровни локализации транзакций, администратор блокировок базы данных, и что существует возможность конфликта при доступе к данным, а такой конфликт может привести к зависанию приложения. Вы должны понимать, сколько времени и ресурсов может потратить ваше приложение на разрешение конфликтов, как оно должно поступать с несогласованными или некорректными данными. Все это необходимо для принятия решения, в каких ситуациях допускается попытка избежать зависания любой ценой, и как следует поступать при возникновении последовательности конфликтующих операций.
Иногда может понадобиться использовать объект DataSet (Набор данных) с дополнительно реализованными возможностями для проверки того, были ли изменены записи, содержащиеся в нем, со времени их последней выборки или модификации. А можно просто использовать SqlDataReader и заново произвести выборку. Все это зависит от ситуации.
Так, при бронировании комнат в нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя делать оптимистических предположений о наличии свободных мест. Это равносильно предположению о бесконечном количестве комнат в отеле и приведет к ситуации, когда администратор должен будет распределить ограниченное количество комнат на гораздо большее количество желающих. В нашем примере для проверки того, зарезервирована ли комната, используется хранимая процедура MakeReservation.
Иногда, даже при отсутствии одновременных запросов, объект DataSet (Набор данных) нельзя использовать для добавления новой строки без установления связи с базой данных. В нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя использовать произвольный первичный ключ. Бронирование могут производить одновременно несколько пользователей. Поэтому идентификаторы бронирования не могут быть локальными. Их определение должно производиться самой базой данных. В нашем примере это делает хранимая процедура MakeReservation.
Способ использования отсоединенных операций в вашем приложении следует определить еще прежде, чем вы решите, каким образом будут использоваться объекты SqlDataReader и DataSet (Набор данных).
Зачем вообще в приложении HotelBroker (Посредник, бронирующий места в гостинице) используется DataSet (Набор данных)? Фактически, в реализации объекта Customer (Клиент) DataSet (Набор данных) никак не используется. Но его использует объект HotelBroker (Посредник, бронирующий места в гостинице), и делается это по двум причинам. Первая — педагогическая. Мы хотели показать, как объект DataSet (Набор данных) может быть использован в полноценном приложении, а не только в простой программе. Во-вторых, в Web-ориентированной версии приложения, реализованной в последующих главах книги, удобно производить кэширование некоторых данных. Например, вполне разумно сделать так, чтобы пользователь мог работать с локальной копией системы бронирования. С другой стороны, такую информацию, как электронный адрес пользователя, достаточно запросить один раз — при его регистрации в системе. Поэтому в нашем случае нет необходимости в сложном механизме кэширования информации о пользователе, так что реализованный объект Customer (Клиент) использует методы объекта SqlCommand.