Робота з об'єктами великого обсягу в MS SQL і ADO h2>
Олексій Ширшов p>
Вступ h2>
Ця стаття з'явилася на світ лише завдяки вашим НЕ
перестав з'являтися питань типу: «Хто-небудь може привести приклад коду
для роботи з полями бази, що містять зображення ... використовуючи ADO і Visual C + + ... », і
тому, що мені лінь на них відповідати. p>
Робота в MS SQL h2>
Давайте спочатку розберемося, як працювати з великими
об'єктами (LOB - large objects) на рівні бази даних. MS SQL Server
підтримує такі типи великих об'єктів: p>
image - містить бінарні дані змінної довжини.
Довжина не може перевищувати 2 гігабайт. p>
text - містить текстові дані змінної довжини в
кодуванні сервера (in code page of the server). Довжина не може перевищувати 2
гігабайт. p>
ntext - містить текстові дані в Unicode-форматі.
Довжина не може перевищувати 2 гігабайт. p>
Для зберігання даних всіх цих типів і низькорівневою
роботи з ними SQL Server використовує один і той же механізм. p>
Фізичне розміщення великих об'єктів
p>
MS SQL Server 2000 підтримує два методи зберігання
великих об'єктів: перший метод на заради сумісності з старими версіями
і не забезпечує належної продуктивності в певних випадках, за
порівняно з новим методом. За замовчуванням сервер працює у старому режимі. P>
При використанні старого методу сервер завжди
розміщує дані в окремих сторінках, а покажчик на перший з них зберігає
безпосередньо в рядку даних. p>
ПРИМІТКА p>
Точніше, в рядку даних зберігається покажчик
на корінь B-tree, а не на будь-які таблиці даних. Докладніше про це, см.
наступний розділ. p>
Використовуючи нову стратегію, сервер може зберігати частину
даних безпосередньо в рядку таблиці. Це призводить до економії пам'яті та
збільшення продуктивності для LOB-ів невеликого розміру. p>
Стратегія розміщення за замовчуванням
h2>
Як структури зберігання даних використовується
B-tree. У рядку даних зберігається 16-байтним покажчик на корінь дерева --
структуру розміром 84 байта. Якщо розмір даних не перевищує 32 Кб, в кореневий
структурі зберігаються посилання на блоки даних, розташованих на цій же або інших
сторінках. Великі об'єкти зберігаються на спеціальних сторінках, на яких не можна
розміщувати ніякі інші дані, крім image, text і ntext. Проте дані цих
типів з різних таблиць можуть бути розміщені на одній сторінці. Якщо загальний
розмір даних не більше 64 байт, всі дані зберігаються в кореневій структурі. p>
p>
Малюнок 1. p>
Якщо розмір даних більше 32 Кб, корінь дерева
посилається на проміжні вузли. Проміжні вузли розташовуються на окремих
сторінках, які не можуть містити будь-які інші дані, або
проміжні вузли інших таблиць або навіть інших колонок даної таблиці. p>
Покращена стратегія
p>
У SQL Server 2000 з'явилася можливість використовувати
новий метод зберігання великих об'єктів. У ньому відсутня 16-байтним покажчик.
У рядку даних (data row) можуть знаходитися як самі дані (у випадку, якщо вони
менше заданої величини), так і корінь B-tree. Для кожної таблиці розмір
збережених великих об'єктів можна задавати індивідуально за допомогою процедури
sp_tableoption. Перевірити режим розміщення можна за допомогою інструкції
objectproperty з параметром TableTextInRowLimit. У наступному скрипті створюється
таблиця (яку ми будемо використовувати протягом всієї статті) blob_test,
потім перевіряється режим розміщення даних у цій таблиці, і, нарешті,
встановлюється розмір даних в рядку (350 байт), що автоматично ставить
поліпшену стратегію розміщення великих об'єктів у таблиці. p>
create table blob_test (id int identity, img image, txt text, ntxt ntext) p>
select case p>
when
OBJECTPROPERTY (object_id ( 'blob_test'), 'TableTextInRowLimit') = 0 p>
then 'data outside the table' p>
else 'data in row' p>
end p>
sp_tableoption blob_test, 'text in row', 350 p>
Замість розміру великих об'єктів у процедуру
sp_tableoption можна було передати значення On. У цьому випадку розмір
установлюється рівним 250 байтах. Вимкнути розміщення даних у рядку можна,
задавши в якості параметра значення 0 або Off. Максимальний розмір даних в
рядку дорівнює 7000 байт. Наступний малюнок ілюструє схему розподілу
даних при розмірі, що перевищує 350 байт (для нашої таблиці). p>
p>
Малюнок 2 p>
Якщо в рядку даних присутня розширюване поле
типу varchar або varbinary, то при його розширенні, якщо загальний розмір рядка
перевищить 8060 байт, частина даних з рядка може бути вивантажено на
додаткові сторінки. Іншими словами, інші поля мають пріоритет перед
LOB при нестачі простору в рядку даних. Повернемо нашу таблицю в початкове
стан, тому що наступні приклади розраховані на режим за замовчуванням: p>
sp_tableoption
blob_test, 'text in row', 'off' p>
Після перекладу таблиці в режим «дані в рядку» самі
дані в рядок не переносяться, проте зворотна дія викликає негайну
операцію з перенесення даних на окремі сторінки. При цьому вся таблиця
повністю блокується, а при великій кількості їх переносите даних операція
може зайняти тривалий час. p>
Робота з великими даними
p>
У роботі з двійковими даними на рівні сервера
великого сенсу немає. Тому більшість прикладів використовує текстові дані,
хоча що описуються процедури цілком згодяться і для бінарних даних. p>
При роботі з LOB можна використовувати звичайні оператори
SQL (select, insert). Але іноді може знадобитися працювати не з LOB цілком, а
з його частинами. Оператори роботи з такими невеликими порціями досить незвичайні
для SQL тим, що в них використовуються покажчики, зміщення та інші низькорівневі
поняття. p>
Покажчик являє собою 16-байтове змінну
типу binary або varbinary. Це абстракція, що вказує на дані в конкретній
колонці конкретної рядка. Покажчик виходить шляхом виклику функції textptr,
куди передається ім'я колонки. Він може бути дорівнює NULL в тому випадку, якщо даних
не існує. Якщо покажчик дорівнює NULL, ви не можете використовувати функції
READTEXT, WRITETEXT і UPDATETEXT. Покажчик повинен містити будь-яке
значення, тому для правильної роботи цих функцій у колонці спочатку
повинні міститися дані. Для простоти ми запишемо туди наступні значення: p>
insert
into blob_test values (0x0, 'My wife is Rosa', 'My son is Dima') p>
Значення для колонки типу image повинні вказуватися в
шістнадцятковому форматі, а для типів text і ntext це повинні бути рядки. p>
Для всіх операторів DML, які змінюють дані, попереднє
значення всієї рядки скидається в лог транзакцій, проте для операторів
WRITETEXT і UPDATETEXT це залежить від моделі відновлення бази даних. Для
моделі Bulk logged дані не записуються в лог транзакцій, натомість
змінені сторінки позначаються особливим чином і записуються в архів логу
транзакцій при виклику відповідної операції архівування. p>
READTEXT p>
Цей оператор призначений для блочного читання великих
текстових і бінарних даних: p>
READTEXT
(Table.column text_ptr offset size) [HOLDLOCK] p>
Параметри: p>
table.column - таблиця і колонка; p>
text_ptr - дороговказ, отриманий за допомогою функції
textptr; p>
offset - зсув, з якого починається читання
даних; p>
size - розмір прочитуються даних. p>
Приклад: p>
declare @ p binary (16) p>
select @ p = textptr (txt) p>
from blob_test p>
where id = 1 p>
select case p>
when @ p is not null then '@ p is
valid ' p>
else '@ p is invalid' p>
end p>
if @ p is not null p>
READTEXT blob_test.txt @ p 0 4 p>
Для пошуку певного текстового фрагмента потрібно
скористатися функцією PATINDEX. Вона не така зручна, як хотілося б (наприклад,
відсутня можливість шукати, починаючи з певної позиції), але цілком
підходить для простих операцій. У наступному прикладі виводиться весь текст після
слова is. p>
declare @ p binary (16) p>
declare @ idx int, @ l int p>
select @ p = textptr (ntxt), p>
@ idx = patindex ( '% is%', ntxt) -1, p>
@ l =
datalength (ntxt)/2 - (patindex ( '% is%', ntxt) -1) p>
from blob_test p>
where id = 1 p>
if @ p is not null p>
readtext blob_test.ntxt @ p
@ idx @ l p>
Тут хочеться відзначити дві особливості: patindex
повертає зсув щодо початку рядка в символах, рахуючи від одиниці,
тоді як readtext сприймає зміщення від нуля, а datalength повертає довжину
даних в байтах, так що для типу ntext ми повинні поділити її на два. p>
Давайте замислимося, що станеться, якщо хто-небудь
спробує змінити дані між операціями одержання покажчика та його
використання. Нічого особливого, просто SQL Server видасть помилку 7123,
яка говорить, що була спроба використовувати недійсний вказівник. Однією
перевірки на NULL виявляється недостатньо. Для перевірки покажчика на дійсність
потрібно скористатися функцією textvalid. Однак ця перевірка не позбавляє нас
від проблеми, а лише допомагає виявити її. Нам потрібно, щоб для даного покажчика
дотримувалася умова повторюваного читання. Цього найпростіше добитися, використавши
у запиті хинт REPEATABLEREAD. Перепишемо приклад наступним чином: p>
declare @ p binary (16) p>
declare @ idx int, @ l int p>
begin tran p>
select @ p = textptr (ntxt), p>
@ idx =
patindex ( '% is%', ntxt) -1, p>
@ l =
datalength (ntxt)/2 - (patindex ( '% is%', ntxt) -1) p>
from blob_test (REPEATABLEREAD) p>
where id = 1 p>
if textvalid (@ p) = 1 and @ idx> = 0 and @ l> 0 p>
readtext blob_test.ntxt @ p
@ idx @ l p>
commit p>
Тепер код написаний «за всіма правилами»: p>
на рядок з ідентифікатором 1 накладається
колективна блокування, що запобігає її зміни з інших транзакцій; p>
зсув перевіряється на негативні значення, так
як функція patindex може повернути 0, якщо не знайде шаблон; p>
довжина зчитує тексту також перевіряється на
невід'ємні значення. p>
Функція READTEXT не поверне вам усього обсягу даних.
Розмір максимально доступних даних, які можна отримати за допомогою цієї
функції, дорівнює @ @ textsize. За замовчуванням це значення дорівнює 4 Кб. Збільшити його
можна за допомогою функції set textsize. Для скидання змінної в значення за замовчуванням
встановіть розмір, що дорівнює нулю. p>
WRITETEXT p>
Ця функція залишена тільки для сумісності. Її
замінила більш потужна UPDATETEXT, яку я розгляну пізніше. p>
Ось синтаксис функції WRITETEXT: p>
WRITETEXT
(Table.column text_ptr) p>
[WITH
LOG] (data) p>
table.column - таблиця і колонка; p>
text_ptr - покажчик; p>
with log - ігнорується для SQL Server 2000; p>
Data - дані. Їх розмір не може перевищувати 120 Кб. p>
WRITETEXT повністю перезаписує вміст стовпчика.
Для операції оновлення також актуальна проблема дійсності покажчика. Але
тут вже недостатньо просто встановити колективну блокування на оновлювану
рядок, тому що це може призвести до взаімоблокіровке, наприклад, якщо дві
транзакції одночасно отримують колективну блокування та намагаються
конвертувати її в монопольну. Для запобігання подібній ситуації
необхідно накласти блокування оновлення. У наступному прикладі проводиться
оновлення бінарних даних в першому рядку: p>
declare
@ p binary (16) p>
begin
tran p>
select
@ p = textptr (img) p>
from
blob_test (updlock) p>
where
id = 1 p>
if
textvalid ( 'blob_test.img', @ p) = 1 p>
writetext blob_test.img @ p 0x4034 p>
commit p>
Більш детально механізм блокувань в MS SQL Server та
поняття рівнів ізоляції транзакцій розглянуті в попередньому номері журналу. p>
UPDATETEXT p>
Ця більш потужна функція оновлення даних, ніж
WRITETEXT. Вона також дозволяє копіювати дані з іншої колонки (правда,
не можна вказати розмір копіюються в цьому випадку даних). Ось її синтаксис: p>
UPDATETEXT (table_name.dest_column_name dest_text_ptr) p>
(NULL | insert_offset) p>
(NULL | delete_length) p>
[WITH LOG] p>
[inserted_data p>
| (table_name.src_column_name
src_text_ptr)] p>
table_name.dest_column_name
- Таблиця і колонка. p>
dest_text_ptr - покажчик на оновлювану область. p>
insert_offset - зсув у байтах, за яким будуть
змінюватися дані. Якщо вказується NULL, дані будуть додані до поточних
даними. p>
delete_length - кількість видаляються байт. Якщо
вказується NULL, дані будуть віддалені від зсуву до кінця. Для вставки даних
необхідно вказати значення 0. p>
with log - не має значення на SQL Server 2000. p>
inserted_data - вставляються дані. p>
table_name.src_column_name - таблиця і колонка, звідки
дані вставляються. p>
src_text_ptr - покажчик на вихідні дані. p>
Наступні два виклики аналогічні: p>
WRITETEXT
table.column text_ptr inserted_data p>
UPDATETEXT
table.column text_ptr 0 NULL inserted_data p>
Давайте розглянемо приклад. Припустимо, я помилився,
набираючи ім'я своєї дружини, і мені його зараз необхідно замінити: p>
declare @ p binary (16) p>
declare @ l int, @ idx int p>
begin tran p>
select @ p = textptr (txt), p>
@ idx =
patindex ( '% Rosa%', txt) -1, p>
@ l =
datalength (txt) - (patindex ( '% Rosa%', txt) -1) p>
from blob_test (updlock) p>
where id = 1 p>
if textvalid ( 'blob_test.txt', @ p) = 1 p>
updatetext blob_test.txt @ p @ idx
4 '[Correct name]' p>
commit p>
Мабуть, це все. Залишилося ще одна тонкість. P>
Дані в рядку p>
Читаючи Books Online, я натрапив на таку пропозицію: p>
After you have
turned on the text in row option, you cannot use the READTEXT, UPDATETEXT or
WRITETEXT statements, to read or modify parts of any text, ntext, or image
value stored in the table. p>
Ось це так! Тобто я не можу користуватися функціями,
наведеними вище, якщо таблиця знаходиться в режимі «дані в рядку»? Це
неправда. Хоча ось такий приклад може переконати кого завгодно: p>
declare @ p binary (16) p>
declare @ idx int, @ l int p>
select @ p = textptr (ntxt), p>
@ idx =
patindex ( '% is%', ntxt) -1, p>
@ l = datalength (ntxt)/2 - (patindex ( '% is%', ntxt) -1) p>
from blob_test (repeatableread) p>
where id = 1 p>
if textvalid ( 'blob_test.ntxt', @ p) = 1 p>
readtext blob_test.ntxt @ p 0
14 p>
Він видає помилку: p>
You cannot use a
text pointer for a table with option 'text in row' set to ON. p>
Справа в тому, що в режимі «дані в рядку» покажчик
становить невірним відразу ж після закінчення транзакції. Так як SQL Server по
умовчанням знаходиться в режимі автоматичного підтвердження транзакції (auto
commit), покажчик перестає бути дійсним відразу після виконання
запиту. Щоб наш приклад запрацював, необхідно включити обидві операції
(одержання покажчика та його використання) в одну транзакцію. Крім цього, SQL
Server автоматично встановлює колективну блокування в момент одержання
покажчика для даних в рядку, так що не потрібно вдаватися до будь-яких Хінта.
Це блокування зніметься після того, як покажчик стане недійсним. Як
я сказав, це відбувається в кінці транзакції або при використанні наступних
команд: p>
create clustered
index p>
drop clustered
index p>
alter table p>
drop table p>
truncate table p>
sp_tableoption ( 'text
in row ') p>
sp_indexoption p>
Можна і вручну зробити покажчик недійсним з
допомогою виклику функції sp_invalidate_textptr. p>
Якщо транзакція виконується на рівні ізоляції READ
UNCOMMITTED, отриманий покажчик можна використовувати лише в операціях читання.
Операція оновлення закінчиться помилкою 7106: You cannot update a blob with a read-only text
pointer. p>
Робота з ADO
h2>
У цьому розділі я приведу приклади роботи з великими
об'єктами, використовуючи ADO.NET і ADO. Почнемо з простого. P>
Читання зображення і виведення на екран за допомогою VB6
p>
Хоча VB6 вже не так популярний, як кілька років
тому, на ньому все-таки дуже зручно писати певного виду програми. Я іноді
просто дивуюся, як багато VS.NET перейняла з-посеред VB6, аж до іконок.
Програмуючи на C #, ви, як і раніше, в меню Project можете знайти пункт
References, а в списку подій форми - події OnLoad. Дуже зручна технологій
DataBinding, яку я і буду використовувати в прикладах, також благополучно
перекочувала в дотнет. Насправді, дуже багато знань з «колишнього життя» ви
можете використовувати в новому середовищі. Ось тільки я не розумію, чому
сумісності в ADO.NET приділено найменше уваги. Наприклад, той же самий
DataBinding не працює зі старим ADO. Замість цього потрібно «заливати» ADO-шний
Recordset в DataSet, і використовувати вже його. Ну, вистачить лірики, давайте
перейдемо до предмету розмови. p>
Алгоритм виведення зображення на екран з БД може бути
таким: p>
Підготовка з'єднання; p>
Відкриття з'єднання; p>
Вибірка даних в Recordset; p>
Зв'язування зображення за допомогою вбудованої технології
DataBinding. p>
Ось фрагменти коду з демонстраційного додатки,
реалізують цей алгоритм. p>
'Задаємо провайдера p>
conn.Provider =
"sqloledb" p>
sb.SimpleText = "Connecting to DB ..." p>
sb.Refresh p>
'Відкриваємо з'єднання з БД p>
conn.Open "localhost", "user", "psw" p>
sb.SimpleText = "Ready" p>
sb.SimpleText = "Loading image ..." p>
sb.Refresh p>
Dim ra As Long p>
'Створюємо Recordset p>
Set rs = conn.Execute ( "select * from blob_test", ra) p>
'Виробляємо зв'язування
даних p>
Set imgImg.DataSource = rs p>
'Тут виконується
фактична пересилання даних p>
'і виведення зображення на
екран p>
imgImg.DataField = "img" p>
sb.SimpleText = "Ready" p>
... p>
Якщо потрібно просто зберегти графічний об'єкт в файл
на диску, алгоритм дещо змінюється. Замість зв'язування даних потрібно відкрити
файл на запис та записати в нього дані. Проте все не так просто: p>
'Відкриваємо фото як
бінарний для запису p>
Open "c: temp_img.bmp" For Binary Access Write As # 1 p>
Dim b () As Byte p>
'Виділяємо пам'ять під масив p>
ReDim b (Len (rs.Fields ( "img"). Value)) p>
'Копіювання даних в
масив p>
b =rs.Fields ( "img"). Value p>
'запис у файл p>
Put # 1,, b p>
Close # 1 p>
У цьому прикладі мені довелося скопіювати дані під
тимчасовий буфер, так як інструкція Put додає до деяких типів, екземпляри
яких ви хочете зберегти, різні заголовки. Навіщо це зроблено, мені не зовсім
зрозуміло; мабуть розробники хотіли спростити реалізацію
збереження/відновлення стану змінних програми, однак це у них не
дуже добре вийшло - для об'єктів ця інструкція не підтримується. У
випадку збереження таким чином: p>
Put
# 1,, rs.Fields ( "img"). Value p>
в файл запишеться одному лише богу відомий заголовок,
який буде заважати сприймати це фото як стандартний bmp. Тому я
змушений копіювати дані в додатковий масив байтів та зберігати вже його. p>
Для читання графічної інформації з файлу можна
скористатися інструкцією Get. p>
Все йде добре до тих пір, поки не знадобиться читати/писати
бінарні дані невеликими блоками. Тут на допомогу приходять такі методи: p>
AppendChunk - застосуємо до полів з атрибутом adFldLong.
Якщо метод викликаний перший раз з тих пір, як ви редагуєте поточне поле,
дані перезаписуються. Інакше - метод додає дані до існуючого
значенням. Іншими словами, якщо ви тільки почали редагувати поле, викликавши
метод AppendChunk, що містяться в ньому до цього значення будуть втрачені. Однак
наступні виклики методу будуть додавати дані до існуючого значенням. Як
тільки ви почнете редагувати інше поле, можливість додавати дані
зникне. Цей метод також можна викликати для параметрів з встановленим
атрибутом adParamLong. Для параметрів дані завжди додаються до існуючих.
p>
GetChunk - застосуємо до полів з атрибутом adFldLong.
Повертає вказану кількість байтів з позиції, на якій закінчилося
попереднє зчитування даних. До тих пір, поки ви не перейдете до роботи з
іншим полем, дані зчитуються послідовно. Якщо ви почали працювати
з іншим полем, а потім повернулися до цього, дані знову будуть читатися з
нульового зсуву. p>
Ці два методи дозволяють працювати з порціями (chunks)
даних. Наприклад, ось такий код дозволяє вважати лише перші 100 байт
даних: p>
Dim
b () As Byte p>
b =
rs.Fields ( "img"). GetChunk (100) p>
Це все чудово, але як же рекомендації MSDN
використовувати більш гнучкий об'єкт Stream? Зараз ми і до нього доберемося. P>
Робота з зображенням за допомогою Stream на С + +
p>
Я вибрав С + +, так як з VB6 ми вже попрацювали
(гарного помаленьку), і тому, що більшість питань стосується саме С + +.
(На RSDN ходять справжні індіанці.) P>
Алгоритм дій приблизно той же, що і в попередньому
прикладі: p>
Підготовка з'єднання. p>
Відкриття з'єднання. p>
Вибірка даних в Recordset. p>
Створення та відкриття об'єкта Stream. p>
Читання даних в Stream. p>
Робота зі Stream. p>
Код реалізації алгоритму наведено далі, а зараз я б
хотів зупинитися на шостому пункті, так як він виглядає занадто розпливчасте, а
ми, програмісти, і чужі невизначеності. p>
Об'єкт Stream (потік) призначений спеціально для
роботи з нереляціоннимі і двійковими даними. Його можливості дуже великі. P>
Він підтримує інтерфейс IStream, а отже, його можна
спокійно пересилати по мережі, зберігати в складовою файл і завантажувати з нього зображення.
p>
Його можна зберігати/завантажувати в/з файлу. p>
Його можна зберігати/завантажувати в/з текстового рядка. p>
Він може завантажувати дані з об'єкта Record або
будь-якого ресурсу по URL. p>
Його можна клонувати. p>
Для ілюстрації роботи з об'єктом Stream наведу-таки
приклад на VB6, який зберігає зображення в файл без використання
інструкції Put: p>
Dim stream As New ADODB.stream p>
'Тип потоку - бінарний p>
stream.Type = adTypeBinary p>
'Відкриваємо порожній p>
stream.Open p>
'Записуємо значення поля img p>
stream.Write rs.Fields ( "img") p>
'Созраняем в файл p>
stream.SaveToFile
"c: temp_img.bmp" p>
Але чи вистачить БЕЙСІК (принаймні, VB6 в цій статті
більше не зустрінеться), перейдемо до реалізації описаного вище алгоритму. p>
void ShowError () p>
( p>
CComPtr ef; p>
GetErrorInfo (NULL, & ef); p>
CComBSTR desc; p>
ef-> GetDescription (& desc); p>
USES_CONVERSION; p>
MessageBox (NULL, OLE2T (desc), _T ( "Error"), MB_ICONERROR); p>
) p>
void LoadPicture (IPicture ** pic) p>
( p>
// Створюємо з'єднання p>
HRESULT hr; p>
hr =
conn.CoCreateInstance (L "ADODB.Connection "); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Встановлюємо провайдера p>
hr =
conn-> put_Provider (CComBSTR (L "sqloledb ")); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Відкриваємо з'єднання p>
hr =
conn-> Open (CComBSTR (L "Data
source = localhost "), CComBSTR (L" user "), CComBSTR (L" psw ")); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
p>
// Створюємо Recordset p>
CComPtr rs; p>
hr =
rs.CoCreateInstance (L "ADODB.Recordset "); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
p>
// Відкриваємо Recordset p>
hr =
rs-> Open (CComVariant (L "blob_test"), CComVariant (conn )); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Отримуємо Fields p>
CComPtr flds; p>
hr =
rs-> get_Fields (& flds); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Отримуємо бінарне поле p>
CComPtr fld; p>
hr =
flds-> get_Item (CComVariant (L "img"), & fld); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Прочитуємо значення p>
CComVariant v; p>
hr = fld-> get_Value (& v); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
CComVariant
vtMissing (DISP_E_PARAMNOTFOUND, VT_ERROR); p>
// Створюємо Stream p>
CComPtr
stream; p>
hr =
stream.CoCreateInstance (L "ADODB.Stream "); p>
ATLASSERT (SUCCEEDED (hr )); p>
// Задаємо тип вмісту p>
hr = stream-> put_Type (adTypeBinary); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Відкриваємо Stream p>
hr =
stream-> Open (vtMissing); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Записуємо дані в Stream p>
hr = stream-> Write (v); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Встановлюємо внутрішній курсор на початок p>
hr = stream-> put_Position (0); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
// Завантажуємо картинку з Stream-а p>
CComQIPtr
strm (stream); p>
if (strm) ( p>
hr =
OleLoadPicture (strm, 0, TRUE, IID_IPicture, (void **) pic); p>
if (FAILED (hr )){ p>
ShowError (); p>
return; p>
) p>
) p>
) p>
Для того щоб подібний код працював, необхідно
підключити заголовки adoint.h. Можна було скористатися директивою
import для створення зручних обгорток над відповідними об'єктами і
методами ADO. Приклад вийшов би простіше, але тоді ви могли упустити деякі
деталі. p>
У прикладі проводиться завантаження зображення з бази
даних в об'єкт, що підтримує інтерфейс IPicture. Цей інтерфейс дозволить вам
надалі виводить (рендери) зображення або зберігати його на диск в
різних форматах. Виведення зображення на екран робиться приблизно так
(обробник WM_PAINT): p>
LRESULT CMainDlg:: OnPaint (UINT/* uMsg * /, WPARAM/* wParam * /, LPARAM/* lParam * /,
BOOL &/* bHandled */) p>
( p>
CPaintDC dc (m_hWnd); p>
if (pic) ( p>
OLE_XSIZE_HIMETRIC SizeX; p>
OLE_YSIZE_HIMETRIC SizeY; p>
pic-> get_Height (& SizeY); p>
pic-> get_Width (& SizeX); p>
RECT rc; p>
GetClientRect (& rc); p>
// Власне висновок p>
pic-> Render (dc, 0,0, rc.right, rc.bottom, 0, SizeY, SizeX,-SizeY, NULL); p>
) p>
return 0; p>
) p>
Завантажувати файл в базу на С + + приблизно так само просто,
як і отримувати: p>
# define TESTHR (hr) do (HRESULT _hr = hr; if (FAILED (_hr)) ( p>
IErrorInfo * ef = 0;
GetErrorInfo (0, & ef); p>
_com_raise_error (_hr, ef);)) while (false) p>
void UploadPicture2DB (PCWSTR szPath) p>
( p>
try ( p>
CComVariant
vtMissing (DISP_E_PARAMNOTFOUND, VT_ERROR); p>
// Завантажуємо картинку з файлу p>
TESTHR (OleLoadPicturePath (szPath, NULL, NULL, 0, IID_IPicture, (void **) & pic )); p>
CComPtr
stream; p>
TESTHR (stream.CoCreateInstance (L "ADODB.Stream ")); p>
p>
TESTHR (stream-> put_Type (adTypeBinary )); p>
TESTHR (stream-> Open (vtMissing )); p>
CComQIPtr
strm (stream); p>
CComQIPtr ps (pic); p>
// Зберігаємо картинку в об'єкт ADODB.Stream p>
TESTHR (ps-> Save (strm, TRUE )); p>
CComPtr
rs; p>
TESTHR (rs.CoCreateInstance (L "ADODB.Recordset ")); p>
p>
TESTHR (rs-> Open (CComVariant (L "blob_test"), CComVariant (conn), adOpenStatic, p>
adLockOptimistic, adCmdTable )); p>
// Додаємо нову порожню запис p>
TESTHR (rs-> AddNew (vtMissing, vtMissing )); p>
CComPtr flds; p>
TESTHR (rs-> get_Fields (& flds )); p>
CComPtr fld; p>
TESTHR (flds-> get_Item (CComVariant (L "img"), & fld )); p>
TESTHR (stream-> put_Position (0 )); p>
// Заливаємо вміст Stream-а в полі img p>
CComVariant v; p>
TESTHR (stream-> Read (adReadAll, & v )); p>
TESTHR (fld-> put_Value (v )); p>
// Зберігаємо зміни до БД p>
TESTHR (rs-> Update (vtMissing, vtMissing )); p>
) p>
catch (_com_error & e) ( p>
MessageBox (e.Description (), _T ( "Error"), MB_ICONERROR); p>
) p>
return; p>
) p>
Щоб максимально спростити приклад, я використав
винятку замість аналізу кодів помилок і стандартний клас _com_error, який
визначений у файлі comutil.h. p>
Крім цього, у прикладі немає коду відкриття з'єднання,
оскільки передбачається, що в момент виклику цієї функції з'єднання з БД вже
відкрито (глобальна змінна conn). p>
При читанні даних типу text або ntext, тип варіанту
буде VT_BSTR. Я не думаю, що з ним можуть виникнути якісь проблеми. P>
ПРИМІТКА p>
У випадку бінарних даних тип варіанту
VT_ARRAY | VT_UI1. P>
Тепер давайте розглянемо метод додавання бінарних
даних у базу за допомогою збережених процедур. p>
Збережені процедури і ADO.NET
p>
Наша процедура буде виглядати наступним чином: p>
create proc AddBlob (@ img image) p>
as p>
insert into blob_test (img) values (@ img) p>
На щастя, в ADO.NET робота з потоками не дуже
змінилася (їх як і раніше потрібно відкручувати тому). p>
Алгоритм завантаження графічного файлу з диска в базу
може бути таким: p>
Отримуємо ім'я файлу. p>
Відкриваємо з'єднання. p>
Створюємо об'єкт FileStream. p>
Читаємо дані з файлу. p>
Створюємо об'єкт SqlCommand для виклику збереженої
процедури. p>
Одному з параметрів передаємо лічені дані. p>
Викликаємо методу ExecuteNonQuery об'єкта SqlCommand. p>
Параметри з'єднання з базою даних можна налаштувати в
вікні дизайнера, тому в коді цього робити не доведеться, треба лише викликати
функцію Open без параметрів - і з'єднання буде встановлено. p>
ofd1.Filter =
"(*. bmp) | *. bmp" p>
If ofd1.ShowDialog () =
DialogResult.OK Then p>
sb.Text = "connecting to
database ..." p>
sb.Refresh () p>
'Якщо з'єднання не відкрито, відкриваємо p>
If conn.State <> ConnectionState.Open
Then p>
conn.Open () p>
End If p>
sb.Text = "loading
image ..." p>
sb.Refresh () p>
Dim stream As New
FileStream (ofd1.FileName, FileMode.Open) p>
Dim b () As Byte p>
ReDim b (CInt (stream.Length)) p>
'Читання даних з файлу p>
stream.Read (b, 0,
CInt (stream.Length)) p>
'Створення та підготовка до виклику збереженої процедури p>
Dim cmd As New
SqlClient.SqlCommand ( "AddBlob", conn) p>
With cmd p>
. CommandType = CommandType.StoredProcedure p>
. Parameters.Add ( "@ img", SqlDbType.Image) p>
With
. Parameters ( "@ img") p>
. Direction =
ParameterDirection.Input p>
. Value = b p>
End With p>
'Виклик збереженої процедури p>
. ExecuteNonQuery () p>
End With p>
sb.Text = "Ready" p>
End If p>
Все виглядає досить просто, і якщо враховувати, що
все сміття прибере GC, життя стає зовсім легкою. p>
А що, якщо у вас вже є зображення (скажімо, у
об'єкті PictureBox) і вам потрібно зберегти його в базу? У цьому випадку потрібно
використовувати інший потік - MemoryStream. Ось як це може бути зроблено: p>
sb.Text = "connecting to database ..." p>
sb.Refresh () p>
'Якщо з'єднання не
відкрито, відкриваємо p>
If conn.State <> ConnectionState.Open Then p>
conn.Open () p>
End If p>
sb.Text = "loading image ..." p>
sb.Refresh () p>
'Створення та підготовка до
викликом збереженої процедури p>
Dim cmd As New SqlClient.SqlCommand ( "AddBlob", conn) p>
cmd.CommandType =
CommandType.StoredProcedure p>
'Збереження зображення в
потік в пам'яті у форматі BMP p>
Dim stream As New MemoryStream () p>
img1.Image.Save (stream, Imaging.ImageFormat.Bmp) p>
stream.Seek (0, SeekOrigin.Begin) p>
'Підготовка параметрів p>
cmd.Parameters.Add ( "@ img", SqlDbType.Image) p>
With cmd.Parameters ( "@ img") p>
. Direction =
ParameterDirection.Input p>
'Скористаємося зручним методом ToArray. Жалко що його немає в
FileStream-a p>
. Value = stream.ToArray () p>
End With p>
'Виклик збереженої процедури p>
cmd.ExecuteNonQuery () p>
sb.Text = "Ready" p>
Тепер розглянемо випадок, коли треба витягувати
зображення з бази за допомогою ADO.NET. Послідовність дій може бути
такий: p>
Відкрити з'єднання. p>
Підготувати команду (SqlCommand). p>
Заповнити SqlDataReader - аналог ADO Recordset в дуже
урізаному варіанті. p>
Вважати дані в MemoryStream. p>
Завантажити Image з потоку. p>
Перед читанням даних з DataReader-а необхідно викликати
метод Read. Ось реалізація: p>
sb.Text = "connecting to database ..." p>
sb.Refresh () p>
'Установка з'єднання p>
If conn.State <> ConnectionState.Open Then p>
conn.Open () p>
End If p>
sb.Text = "Loading image ..." p>
sb.Refresh () p>
'Підготовка запиту на
вибірку даних p>
Dim cmd As New p>
SqlClient.SqlCommand ( "select img from
blob_test where id = 3 ", conn) p>
'Створення і заповнення об'єкта DataReader p>
Dim reader As SqlDataReader = cmd.ExecuteReader () p>
Dim ms As New MemoryStream () p>
'Перехід на перший рядок p>
reader.Read () p>
Dim bb () As Byte p>
bb = reader.Item ( "img") p>
'Запис даних у потік в
пам'яті p>
ms.Write (bb, 0, CInt (bb.Length)) p>
ms.Seek (0,
SeekOrigin.Begin) p>
'Завантажуємо графічне
зображення в Image p>
pb.Image = Image.FromStream (ms) p>
sb.Text = "Ready" p>
Якщо потрібно читати дані порціями, а не цілком, як
це зроблено в прикладі, скористайтеся методом GetBytes, який аналогічний
ADO-методом GetChunk. Однак якщо обсяг даних дуже великий, це не сильно
допоможе, так як вони передаються клієнту все відразу в момент виклику методу Read.
Для того щоб дані передавалися на клієнта тільки на запит, необхідно
передати як параметр методу ExecuteReader прапор
CommandBehavior.SequentialAccess. У цьому випадку дані зчитуються
безпосередньо в момент виклику GetBytes. p>
ПРИМІТКА p>
Для цього прапора є одне зауваження.
Якщо ви обираєте кілька полів, зчитувати їх значення ви повинні в тій
послідовності, в якій вони повертаються з бази. Мабуть тому прапор
називається «Послідовний доступ». p>
Я знаю, що багато хто не любить процедури, що зберігаються.
Дійсно, для використання збережених процедур ви повинні бути більш-менш
знайомі з T-SQL і знати загальні принципи роботи з реляційними БД. Крім цього,
вважається, що архітектура, в якій занадто великий акцент зроблено на винесення
логіки на рівень SQL Server-а, має ряд недоліків: погане масштабування,
складне супровід і налагодження. Почасти все це правда, проте особисто я все-таки
намагаюся використовувати процедури, що зберігаються скрізь, де можливо, і лише в тих
випадках, коли бізнес-логіка дійсно складна і вимагає взаємодії
декількох бізнес-компонентів, відмовляюся від їх використання. Тим, хто не
хоче використати збережені процедури для оновлення великих об'єктів, я
раджу прочитати статтю http://support.microsoft. com/default.aspx? scid = kb; en-us; Q309158 & ID = kb; en-us; Q309158,
в якій викладається метод читання і запису великих об'єктів за допомогою DataSet. p>
Останнє, на чому я б хотів зупинитися, і що
викликає найбільшу кількість проблем у початківців програмістів це робота з
великими об'єктами в Oracle. Традиційно це питання досить складний: в
про-дотнетовскую еру доступ до СУБД Oracle за допомогою OLEDB-провайдерів в
основному забезпечували дві бібліотеки: MSDAORA і OraOLEDB. p>
Провайдер MSDAORA - надавав слабкий сервіс (не можна
було, наприклад, дізнатися кількість рядків у вибірці) і не дозволяв працювати з
великими об'єктами в принципі. Єдиною перевагою даного провайдера
є те, що він входить в стандартний пакет MDAC - Microsoft Data Access
Components. p>
Провайдер OraOLEDB - рідний провайдер, який
надавав більшу функціональність, ніж MSDAORA, і дозволяв працювати з
великими об'єктами. p>
ПРИМІТКА p>
В Oracle, як і в MS SQL, великі
текстові дані відрізняються від великих бінарних даних. Перші називаються CLOB і NCLOB --
Character Large OBjects, другий - BLOB - Binary
Large OBjects. Аналогію з типами MS SQL
провести нескладно. p>
Отримувати і змінювати дані за допомогою об'єкта Recordset
можна точно так само, як і у випадку MS SQL Server-а. Приклад я приводив вище. Єдина
проблема пов'язана з передачею великих об'єктів в якості параметрів до збережених
процедур. Для того щоб це працювало, необхідно в об'єкті Command встановити
динамічний параметр SPPrmsLOB в TRUE. p>
Я не буду детально і з прикладами зупинятися на
роботі з цими провайдерами, натомість зосередившись на керованому
ADO.NET провайдера. Якщо у вас будуть питання, зверніться до документації або,
якщо зовсім погано буде, поставте питання в нашому форумі з баз даних. p>
Провайдер для ADO.NET не поставляється з. NET Framework
1.0, його потрібно встановлювати вручну. Завантажити останню версію можна за адресою http://www .microsoft.com/downloads/details.aspx? familyid = 4f55d429-17dc-45ea-bfb3-076d1c052524 & languageid = f49e8428-7071-4979-8a67-3cffcb0c2524 & displaylang = en. p>
вступну інформацію про використання цього провайдера в
ASP.NET-програм можна отримати зі статті http:// msdn.microsoft.com/vstudio/using/building/default.aspx? pull =/library/en-us/dndotnet/html/manprooracperf.asp. p>
Наступний приклад демонструє методи роботи з
великими об'єктами за допомогою керованого провайдера. Для його правильної роботи
вам необхідно підключити збірку System.Data.OracleClient.dll. p>
Sub FillDBFromStream (stream as Stream, UserName as String) p>
'Відкриваємо
з'єднання p>
Dim con As New OracleClient.OracleConnection ( "server = srv; Uid = u; pwd = p") p>
'Створюємо запит з одним параметром username p>
Dim da As New
OracleClient.OracleDataAdapter ( "select * p>
from USERS_STATE where
USERNAME =: username ", con) p>
'Додаємо параметр і встановлюємо його значення p>
da.SelectCommand.Parameters.Add ( "username",
Data.OracleClient.OracleType.VarChar). Value = UserName p>
Dim ds As New DataSet () p>
'Об'єкт CommandBuilder необхідний для оновлення даних p>
Dim cb As New
OracleClient.OracleCommandBuilder (da) p>
'Виробляємо вибірку p>
da.Fill (ds, "UState") p>
'Прочитуємо дані з потоку у внутрішній
масив p>
Dim b () as Byte p>
ReDim b (CInt (ms.Length)) p>
stream.Read (b, 0,
CInt (stream.Length)) p>
'Якщо вибірка порожня, створюємо нову запис p>
If ds.Tables ( "UState"). Rows.Count =
0 Then p>
'Створення запису p>
Dim dr As DataRow =
ds.Tables ( "UState"). NewRow () p>
dr ( "username") =
UserName p>
dr ( "b_state") = b p>
'Додавання запису p>
ds.Tables ( "UState"). Rows.Add (dr) p>
Else p>
'Оновлення запису p>
Dim dr As DataRow =
ds.Tables ( "UState"). Rows (0) p>
dr ( "b_state") = b p>
End If p>
'Зміни записуємо в базу p>
da.Update (ds, "UState") p>
End Sub p>
Треба сказати, що цей спосіб буде працювати, тільки
якщо в таблиці є первинний ключ. У моєму випадку, логічніше за все він
виглядає на полі username. При відсутності ключа в момент виконання команди
Update ви отримаєте наступну помилку: p>
Dynamic
SQL generation for the UpdateCommand is not supported against a SelectCommand
that does not return any key column information. p>
У документації з Oracle стверджується, що оновлення
даних краще проводити в явних транзакції. p>
ПРИМІТКА p>
За замовчуванням підключення відкривається в
режимі неявної транзакції, в якому транзакція починається в момент
виконання першої команди після закінчення попередньої транзакції. Цей режим аналогічний установці SET IMPLICIT_TRANSACTIONS ON в MS SQL Server. P>
Це пов'язано з тим, що транзакція, в контексті якої
проводиться оновлення, автоматично підтверджується в момент виконання
команди Update. Так що якщо ви потім бажаєте скасувати зміни - це вам так
просто не вдасться зробити. Проблема вирішується за допомогою об'єкта Transaction,
приклад використання якого, як і приклад збереження великих даних за допомогою
збережених процедур, можна знайти за адресою http:// support.microsoft.com/default.aspx? scid = kb; EN-US; Q322796. p>
Ну ось, мабуть, і все. Буду радий, якщо ця стаття
допомогла вам у роботі з великими об'єктами. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>