connectionString ="..." p>
type = "OleDbConnection" /> p>
Database> p>
Тут декларується, що додаток використовує два
бази даних. Перша з них називається alfa, обслуговується об'єктом типу
SqlConnection (бо нічого іншого не вказано), і є підключенням по
замовчуванням. Друга носить логічне ім'я beta і обслуговується об'єктом типу
OleDbConnection. Безумовно, для обох баз зазначені і коректні рядка
підключень. p>
Маючи простий і зручний спосіб опису підключень
через конфігураційний файл, ми, тим не менш, не повинні забувати, що бувають
ситуації, коли все це повинно бути зроблено програмним шляхом. Наприклад, так: p>
Configuration config = new Configuration (); p>
// Налаштовуємо об'єкт config p>
... p>
// Призначаємо конфігурацію менеджеру p>
DbManager.Configure (config); p>
У даному випадку об'єкт типу Configuration
надає нам ті ж можливості налаштування, що й файл конфігурації. p>
Дуже важко уявити додаток, в якому
існувало б кілька окремих наборів підключень до баз даних. Я
кажу, наприклад, про ситуацію, коли у двох різних місцях програми ми
використовуємо два різних підключення з ім'ям beta. Які висновки з цього випливають?
p>
По-перше, це означає, що всі екземпляри менеджера
підключень, що використовуються в програмі, повинні бути налаштовані однаково.
Відповідно, методи Configure (...) ми сміливо можемо робити статичними. P>
По-друге, напрошується висновок, що ми цілком можемо
обійтися одним екземпляром менеджера на всі додаток. У деяких випадках, про
які ми поговоримо пізніше, нам знадобиться більше, але все ж таки обмежений
кількість екземплярів. З цього випливає, що примірник менеджера ми повинні
отримувати не за допомогою оператора new, а за допомогою якогось статичного методу
класу. Приклад: p>
DbManager dbmgr = DbManager.Get (); p>
Подібний підхід нагадує про паттерні
проектування Singleton, але, на відміну від класичної трактування, у нас може
бути не один екземпляр, а декілька. Втім, про це ми ще поговоримо. P>
Структура класу
h2>
продуманий сценарій використання менеджера, ми можемо
спроектувати структуру класу. Ось вона: p>
public class DbManager: IEnumerable p>
( p>
public static DbManager Get ()
{...} p>
public IDbConnection
this [string name] p>
( p>
get {...} p>
) p>
public IDbConnection Default p>
( p>
get {...} p>
) p>
public static void Configure (
bool forceReload) {...} p>
public static void Configure (
Configuration config) {...} p>
public IEnumerator
GetEnumerator () {...} p>
// Непублічні методи і члени класу p>
... p>
) p>
Короткий опис методів: p>
Get - повертає менеджер підключень. Якщо примірники
менеджера ще немає, створюється новий. p>
this [string] - повертає об'єкт підключення по
даного логічного імені. У тому випадку, якщо ім'я не вказано (так само null),
повертається об'єкт підключення за замовчуванням. p>
Default - повертає об'єкт підключення за замовчуванням. p>
Configure (bool) - читає конфігураційні інформацію з
конфігураційного файлу. Якщо ми намагаємося працювати з ще не сконфігурованих
менеджером, він повинен автоматично викликати цей метод. p>
Configure (Configuration) - налаштовує менеджер в
Відповідно до даного конфігураційних об'єктом. p>
GetEnumerator - дозволяє пробігтися по всіх
підключень менеджера циклом foreach. p>
Варіанти роботи з базою
p>
Ми вже розглядали шматок типового коду, що працює
з базою. Більш повний фрагмент виглядає так: ми спочатку створюємо підключення
(наприклад, SqlConnection), потім створюємо команду (SqlCommand), додаємо до
команді параметри, асоціюємо її з підключенням, відкриваємо підключення,
виконуємо команду, закриваємо підключення: p>
SqlConnection con = new SqlConnection (); p>
con.ConnectionString = "..."; p>
SqlCommand cmd = new SqlCommand (); p>
cmd.CommandText = "..."; p>
cmd.Connection = con; p>
cmd.Parameters.Add (new SqlParameter (...)); p>
using (con) ( p>
con.Open (); p>
cmd.Execute (); p>
... p>
) p>
Ми робимо це при кожному зверненні до бази, так що
виникає питання: а чи не буде швидше заздалегідь створити і зберегти підключення
і команду, а потім тільки їх використати? З точки зору елементарної логіки
здається очевидним, що має бути швидше. З іншого боку, відомо, що
створення об'єктів будівництва. NET Framework відбувається дуже швидко, тому що виграш
навряд чи буде великим. p>
Проведемо тест. В одному прогоні ми будемо кожен раз
створювати підключення і команду, а в іншому - використовувати готові об'єкти. Команді
визначимо три параметри. У двох прогонах по 100 000 ітерацій вдалося з'ясувати
наступне: p>
Перший підхід, при якому все створюється заново,
приблизно на 5 відсотків повільніше друга. p>
В абсолютному вирахуванні це уповільнення складає
всього 0.08 мілісекунди на кожну ітерацію, тобто дуже мало. Якщо врахувати, що
саме звернення до бази виконується на кілька порядків повільніше створення
будь-якого об'єкта, то виграш виходить і зовсім умоглядний. p>
Які висновки? По-перше, логіка перемогла - не
створювати об'єкти виявилося швидше, ніж створювати. По-друге, це зовсім
не важливо. Різниця в швидкості між пересозданіем об'єктів і використанням
готових настільки мала, що розробник може сміливо вибирати той чи інший
підхід, керуючись тільки своїм особистим розумінням прекрасного. p>
Говорячи ж про практичну економії, можна зробити таку
оцінку: якщо у нас є якась динамічна web-сторінка, яка робить одне
звернення до бази, а до неї самої звертаються 10 раз в секунду, то збереження
об'єктів допоможе нам виграти цілу секунду за півгодини. p>
Природно, між цими двома полярними варіантами
є велике число проміжних станів. Наприклад, можна щоразу
створювати об'єкт підключення, але зберігати готові команди. Цей підхід,
ймовірно, дуже органічно поєднується з візуальним дизайнером компонентів з
Visual Studio. Накидавши на компонент команди, ми отримуємо код для їх
ініціалізації, який виконується при створенні екземпляра компонента.
Очевидно, що витягати цей код з методу InitializeComponent нерозумно, краще
просто призначити потрібної команді той об'єкт підключення, що ми збираємося
відкривати в даний момент. p>
Повторне використання підключень
p>
У той час як з повторним використанням командних
об'єктів (SqlCommand, OleDbCommand і т.п.) все, загалом, зрозуміло, питання
повторного використання об'єкта підключення залишається відкритим. Чи потрібно це
кому-небудь, а якщо потрібно, то навіщо? p>
Під «повторним» ми тут розуміємо таке використання,
коли один і той самий об'єкт підключення використовується знову і знову в усіх
частинах програми, де потрібен доступ до відповідної бази даних. При цьому ми
усвідомлюємо, що всі стандартні для ASP.NET об'єкти підключення не є
безпечними для багатопотокового використання (non thread safe), тому для початку
будемо вважати, що наша програма має тільки один потік. p>
Які проблеми можуть виникнути при подібному
використання об'єкта підключення? По-перше, кожного разу, відкриваючи підключення
до бази даних, можна виявити, що воно вже було відкрито раніше. Спроба
відкриття вже відкритого підключення викликає помилку. По-друге, відкритий об'єкт
DataReader блокує своє підключення, так що до його закриття виконати ще
будь-яку команду неможливо. Це може створити проблему в методі, викликаному
під час читання даних з бази. p>
Першу проблему обійти нескладно. Досить перевіряти
стан підключення перед відкриттям, і пропускати цей крок, якщо воно вже
відкрито. Тут важливо помітити, що метод, який відкрив підключення, обов'язково
повинен його закрити, так що, якщо перевірка показала, що підключення потрібно
відкривати, це означає і те, що його потрібно закрити після використання. p>
Обійти другу проблему в тому місці, де вона дала про
себе знати, неможливо. Дійсно, дані вже читаються, підключення вже
заблоковано, і зробити ми з цим нічого не можемо. Єдине рішення тут
- Так проектувати блоки читання, щоб вони навіть потенційно не могли нікого
блокувати. Наприклад, можна спочатку прочитати всі дані в масив, а вже потім
проводити їх подальшу обробку. p>
Основні проблеми ясні і досить серйозні. Які
переваги можуть бути у даного підходу? Переважують вони недоліки? P>
По-перше, ми можемо істотно прискорити роботу в тих
додатках, де свежеоткритое підключення потрібно спеціально готувати. Наприклад,
додаток може використовувати application roles. Щоб увійти в роль MS SQL Server
вимагає виконання збереженої процедури sp_setapprole: p>
EXEC
sp_setapprole 'SalesApprole', 'AsDeFXX' p>
Очевидно, що якщо обробка запиту полягає, до
Наприклад, з п'яти звернень до бази, то набагато швидше буде відкрити підключення
і виконати цю команду один раз, ніж усі п'ять. Сама операція відкриття
підключення вимагає дуже мало часу - на це є connection pooling. Зайве
ж звернення до бази - це серйозний удар по швидкодії. p>
Природно, я говорю не про простому випадку, коли
всі п'ять звернень до бази знаходяться в одному методі. Врешті-решт, ми живемо у
часи переміг об'єктно-орієнтованого підходу, тому що «макаронний» код
майже спочив у бозі. Всі ці звернення здійснюються різними компонентами,
обслуговуючими запит. Як бути в цьому випадку? Пропонується відкрити підключення
і виконати цю команду на початку обробки запиту, а потім передати об'єкт
підключення у подальше використання. p>
Видається, що ця перевага виглядає
досить серйозним (звісно, для певного класу додатків). До речі,
тут варто звернути увагу ще на одну особливість: увійшовши в роль і
розраховуючи на автоматичний вихід з неї із закриття підключення, можна отримати
неприємний сюрприз у тому випадку, якщо підключення на самому справі закрито не
буде. Подальші звернення до БД, можливо, будуть виконуватися з
невідповідними правами. З іншого боку, це може статися тільки в
додатку з вельми специфічною архітектурою. p>
По-друге, можна уявити собі додаток,
що відкриває надто багато підключень. Величезна вкладеність дзвінків. Може
Можливо, навіть рекурсія. Всі методи відкривають підключення і, не закриваючи, викликають
інші методи. У такому (абсолютно гіпотетичний) додатку можна зіткнутися
з тим, що вільні підключення закінчаться, і в якийсь момент часу ми не
зможемо відкрити підключення до бази. Використання одного об'єкта підключення
могло б нас тут врятувати. p>
Втім, подібна проблема виглядає абсолютно
надуманою. Якщо вона і має де-те місце, то це, швидше, помилка в
проектуванні програми, і вирішувати її потрібно іншими способами. p>
Режими функціонування менеджера
p>
Повернемося до менеджера підключень. Очевидно, що він міг
б функціонувати в двох режимах: або кожного разу створювати новий об'єкт
підключення, або повертати вже готовий екземпляр, що відповідає заданому
логічного імені. p>
На практиці менеджер, який здійснює кешування
об'єктів підключення, повинен успішно проходити ось такий тест: p>
DbManager.Mode
= DbManagerMode.CacheConnections; p>
DbManager
dbmgr = DbManager.Get (); p>
IDbConnection
c1 = dbmgr [ "beta "]; p>
IDbConnection
c2 = dbmgr ["beta "]; p>
Assert.IsTrue (c1 == c2);// Менеджер
повертає один і той же екземпляр p>
А «простий» менеджер, тобто не здійснюють
кешування, такий: p>
DbManager.Mode
= DbManagerMode.DoNotCacheConnections; p>
DbManager
dbmgr = DbManager.Get (); p>
IDbConnection
c1 = dbmgr [ "beta "]; p>
IDbConnection
c2 = dbmgr [ "beta "]; p>
Assert.IsTrue (c1! = c2);// Менеджер
повертає різні екземпляри p>
Подібна функціональність дозволила б розробнику
легко вибирати між двома описаними вище варіантами роботи з з'єднаннями, і
навіть без великих складнощів перейти з одного підходу на інший у вже частково
написаному додатку. p>
Нить
p>
Раніше ми розглядали додаток, в якому є
тільки один потік (thread). Які складнощі можуть зустрітися, якщо потоків
буде декілька? p>
По суті, складність тут тільки один - один об'єкт
підключення можна одночасно використовувати тільки в одному потоці. Якщо для
кожного звернення створюється новий об'єкт підключення, то нас ця проблема
абсолютно не стосується. Відповідно, менеджер підключень, що працює в цьому
режимі, дуже простий у реалізації. Метод, який повертає об'єкт підключення по
імені, має виглядати так: p>
public override IDbConnection this [String name] p>
( p>
get p>
( p>
ConnectionInfo info =
(ConnectionInfo) _config.Connections [name]; p>
if (info! = null) p>
( p>
return CreateConnection (info
); p>
) p>
return null; p>
) p>
) p>
Кілька складніше виглядають дії при
кешуванні об'єктів підключення. З одного боку, потрібно організувати якийсь
словник готових об'єктів, з якого видаються об'єкти за запитом. З іншого
боку, ми повинні зробити так, щоб кожен потік працював зі своїм екземпляром
даного підключення. Намагатися реалізувати таку функціональність в одному
об'єкті, що обслуговує відразу всі потоки, може бути дуже складно. Тому
пропонується зробити так, щоб кожен екземпляр менеджера обслуговував тільки
один потік. Очевидно, що в цьому випадку створюється якийсь невизначений
кількість примірників, не більше, ніж загальна кількість потоків у додатку.
Невизначеність обумовлена тим, що для потоків, в яких менеджер не
потрібно, створювати екземпляр не потрібно. p>
Таким чином, потрібно, по-перше, написати такий метод
Get, який би повертав екземпляр, приписаний до викликає потоку, і,
по-друге, зробити словник готових об'єктів. Приблизно так може виглядати
цей фрагмент коду: p>
[ThreadStatic] private static DbManager _instance; p>
private ListDictionary _connections = new ListDictionary (); p>
internal static new DbManager Get () p>
( p>
// Якщо екземпляр вже є, повернути його p>
if (_instance! = null) return _instance; p>
// Створити новий екземпляр p>
_instance = new CachingDbManager (); p>
_instance.Init (); p>
return _instance; p>
) p>
public override IDbConnection this [String name] p>
( p>
get p>
( p>
// Намагаємося взяти готовий об'єкт зі словника p>
IDbConnection result =
(IDbConnection) _connections [name]; p>
if (result == null) p>
( p>
// Шукаємо опис підключення в
конфігурації p>
ConnectionInfo info =
(ConnectionInfo) _config.Connections [name]; p>
if (info! = null) p>
result = CreateConnection (
info); p>
) p>
return result; p>
) p>
) p>
Менеджер і ASP.NET
p>
Як відомо, додатки ASP.NET досить активно
використовують багатопоточність. У той же час роблять вони це настільки неявно, що
цей факт легко залишити без уваги і отримати несподівані помилки. p>
Згадаймо, у загальних рисах, структуру звичайного
додатки ASP.NET. У домені додатку (AppDomain) є кілька примірників
класу HttpApplication. Кожен з цих примірників має набір
супутніх йому модулів (HttpModule). Набір модулів у кожного програми
однаковий, та й самі програми, по ідеї, не повинні нічим відрізнятися. Далі,
домен програми має набір робочих потоків (working threads), готових
обслуговувати запити користувачів. З усією очевидністю, потоків існує
принаймні стільки ж, скільки об'єктів Http-додатки. p>
При обслуговуванні запиту ASP.NET якимсь псевдовипадкових
чином вибирає робочий потік і об'єкт додатка, з яким цей потік буде
працювати. У зв'язку з цим, певний об'єкт програми в різних запитах
буде, швидше за все, працювати з різними потоками. p>
Припустимо тепер, що в нашому додатку ми створили
командний об'єкт (SqlCommand) і зберегли його для подальшого використання.
Команда пов'язана з певним об'єктом підключення, а саме, з тим об'єктом,
який був повернутий менеджером підключень у момент створення і перший виконання
команди. Не будемо, однак, забувати, що даний об'єкт HttpApplication при
обслуговуванні наступного (у його хронології) запиту, швидше за все, буде
працювати вже з іншим робочим потоком, а тому менеджер підключень поверне не
то підключення, з яким пов'язана наша команда. Гірше того, з повернутих
підключенням, ймовірно, буде пов'язана аналогічна команда в іншому об'єкті
додатки. p>
Вихід з описаної ситуації досить простий.
Необхідно зробити такий модуль (HttpModule), який на початку обробки запиту
буде пов'язувати менеджер підключень, приписаний до даного об'єкта додатки,
з потоком, який зараз працює з цієї програми та з усіма підлеглими
йому об'єктами. Це усуне всі проблеми такого роду і дозволить знову забути
про реальний стан справ з потоками в ASP.NET. p>
Код модуля гранично простий: p>
public class AspAdapter: IHttpModule p>
( p>
private HttpApplication
application; p>
private DbManager manager; p>
public void
Init (System.Web.HttpApplication context) p>
( p>
application = context; p>
manager = DbManager.Get (); p>
application.BeginRequest + =
new EventHandler (OnBeginRequest); p>
) p>
protected void OnBeginRequest (
object sender, EventArgs e) p>
( p>
manager.Init (); p>
) p>
public void Dispose () p>
( p>
application.BeginRequest -=
new EventHandler (OnBeginRequest); p>
) p>
) p>
В останніх прикладах можна помітити раніше не
згадуваний метод Init. Він служить для прив'язки даного екземпляра менеджера до
викликає потоку. p>
Будь ласка, закривайте двері!
p>
Загальновідомо, що при використанні пулу підключень
(connection pool) основний принцип роботи з з'єднаннями говорить:
відкривай пізно, закривай рано. Іншими словами, потрібно відкривати перед самим
використанням, а закривати відразу після оного. При цьому потрібно пам'ятати, що в
блоці використання підключення до бази може статися якась помилка
(exception), яка завадить закрити підключення. p>
Ми вже розглядали звичайний для ADO.NET спосіб роботи
з об'єктом підключення, приблизно такий: p>
using (connection) ( p>
connection.Open (); p>
// Активніше використовуємо підключення! Інакше,
навіщо відкривали?! p>
... p>
) p>
Він простий і вдалий, якщо не шкода знищити об'єкт
підключення в кінці блоку, тобто якщо кожного разу створюється новий об'єкт
підключення. Якщо ж хочеться використовувати один об'єкт, код стає набагато
менш красивим: p>
bool wasOpened = false; p>
if (connection.State == ConnectionState.Closed) p>
( p>
connection.Open (); p>
wasOpened = true; p>
) p>
try ( p>
// Використовуємо підключення p>
... p>
) p>
finally p>
( p>
if (wasOpened)
connection.Close (); p>
) p>
Крім цілому, він ще й ускладнює
можливість перемикання між двома режимами роботи менеджера підключень. Ми
ж не хочемо, перекинувши прапор, ще й ред всі блоки роботи з базою. p>
Використовуємо для вирішення той же механізм
детермінованою деструкції - інтерфейс IDisposable, який настільки спрощує
використання підключення за стандартною схемою. Нам достатньо створити клас,
який при конструюванні відкривав би підключення, якщо це необхідно, а при
знищення - закривав би, коли відкривав його сам. При цьому ми можемо зробити клас
досить розумним, щоб він розумів, в якому режимі працює менеджер
підключень. Якщо менеджер не кешує об'єкти, наш сервісний клас буде
видаляти їх в кінці блоку using. p>
У використанні це буде виглядати приблизно так: p>
using (new DbOpen (connection
)) ( P>
//
Використовуємо підключення, раз відкривали! P>
... p>
) p>
Відзначимо також, що при конструюванні примірника
класу підключення відкривається автоматично. Таким чином, ще один рядок у
стандартному сценарії стає непотрібною. p>
Реалізація
p>
Даний підхід уже реалізований і неодноразово пройшов
польові випробування. Більш докладно про готовому модулі можна дізнатися на http://www.byte-force.com/russian/products/tech/lsddatabase.html.
p>
Посилання
p>
E. Gamma, et al.,
Design Pattern: Elements of Reusable Object-Oriented Software, Addison-Wesley,
1995 p>
ПРИМІТКА p>
Від редакції p>
Багато учасників редакційної колегії,
мають досвід роботи з базами даних, неоднозначно розцінює цю статтю.
З одного боку, ідея інкапсуляції роботи з підключенням, що дозволяє
отримувати підключення по логічним іменах, хороша. Вона спрощує код, тим
самим знижуючи ймовірність появи помилок. З іншого боку, створення
саморобного пулу, а також реалізація закриття з'єднання і многопоточной
роботи, є успішним рішенням власноруч створеної проблеми. Більше
того, так як кеш може повертати один і той же екземпляр підключення при
різних викликах, що в програмі може виникнути помилка з-за випадкового
(неявного) використання одного підключення в різних алгоритмах. Тобто
велика ймовірність того, що програміст у двох алгоритмах спробує створити
два незалежних транзакції, але оскільки підключення фізично одне, це йому
не вдасться. Як, власне, зауважив сам автор, виграшу в швидкості таке
рішення не дає, і сенс кешування просто незрозумілий. p>
Таким чином, ми рекомендуємо
використовувати на практиці ідею інкапсуляції роботи з з'єднаннями, але не
кешування підключень. Однак вивчення цієї частини статті цікаво, тому що
в ній використовуються методи оптимізації, цілком застосовні в інших випадках. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>