Объект Stream (поток) предназначен специально для работы с нереляционными и двоичными данными. Его возможности очень велики.
Он поддерживает интерфейс IStream, а значит, его можно спокойно пересылать по сети, сохранять в составной файл и загружать из него изображение.
Его можно сохранять/загружать в/из файла.
Его можно сохранять/загружать в/из текстовой строки.
Он может загружать данные из объекта Record или какого-либо ресурса по URL.
Его можно клонировать.
Для иллюстрации работы с объектом Stream приведу-таки пример на VB6, который сохраняет изображение в файл без использования инструкции Put:
Dim stream As New ADODB.stream 'Тип потока - бинарный stream.Type = adTypeBinary 'Открываем пустой stream.Open 'Записываем значение поля img stream.Write rs.Fields("img") 'Созраняем в файл stream.SaveToFile "c:\temp_img.bmp" |
Но хватит БЕЙСИКа (по крайней мере, VB6 в этой статье больше не встретится), перейдем к реализации описанного выше алгоритма.
void ShowError() { CComPtr<IErrorInfo> ef; GetErrorInfo(NULL,&ef); CComBSTR desc; ef->GetDescription(&desc); USES_CONVERSION; MessageBox(NULL,OLE2T(desc),_T("Error"),MB_ICONERROR); } void LoadPicture(IPicture** pic) { //Создаем соединение HRESULT hr; hr = conn.CoCreateInstance(L"ADODB.Connection"); if (FAILED(hr)){ ShowError(); return; } //Устанавливаем провайдера hr = conn->put_Provider(CComBSTR(L"sqloledb")); if (FAILED(hr)){ ShowError(); return; } //Открываем соединение hr = conn->Open(CComBSTR(L"Data source=localhost"),CComBSTR(L"user"),CComBSTR(L"psw")); if (FAILED(hr)){ ShowError(); return; } //Создаем Recordset CComPtr<ADORecordset> rs; hr = rs.CoCreateInstance(L"ADODB.Recordset"); if (FAILED(hr)){ ShowError(); return; } //Открываем Recordset hr = rs->Open(CComVariant(L"blob_test"),CComVariant(conn)); if (FAILED(hr)){ ShowError(); return; } //Получаем Fields CComPtr<ADOFields> flds; hr = rs->get_Fields(&flds); if (FAILED(hr)){ ShowError(); return; } //Получаем бинарное поле CComPtr<ADOField> fld; hr = flds->get_Item(CComVariant(L"img"),&fld); if (FAILED(hr)){ ShowError(); return; } //Считываем значение CComVariant v; hr = fld->get_Value(&v); if (FAILED(hr)){ ShowError(); return; } CComVariant vtMissing(DISP_E_PARAMNOTFOUND,VT_ERROR); //Создаем Stream CComPtr<ADOStream> stream; hr = stream.CoCreateInstance(L"ADODB.Stream"); ATLASSERT(SUCCEEDED(hr)); //Задаем тип содержимого hr = stream->put_Type(adTypeBinary); if (FAILED(hr)){ ShowError(); return; } //Открываем Stream hr = stream->Open(vtMissing); if (FAILED(hr)){ ShowError(); return; } //Записываем данные в Stream hr = stream->Write(v); if (FAILED(hr)){ ShowError(); return; } //Устанавливаем внутренний курсор на начало hr = stream->put_Position(0); if (FAILED(hr)){ ShowError(); return; } //Загружаем картинку из Stream-а CComQIPtr<IStream> strm(stream); if (strm){ hr = OleLoadPicture(strm,0,TRUE,IID_IPicture,(void**)pic); if (FAILED(hr)){ ShowError(); return; } } } |
Для того чтобы подобный код работал, необходимо подключить заголовочный файл adoint.h. Можно было воспользоваться директивой import для генерирования удобных оберток над соответствующими объектами и методами ADO. Пример получился бы проще, но тогда вы могли упустить кое-какие детали.
В примере производится загрузка изображения из базы данных в объект, поддерживающий интерфейс IPicture. Этот интерфейс позволит вам в дальнейшем выводит (рендерить) изображение или сохранять его на диск в различных форматах. Вывод изображения на экран делается примерно так (обработчик WM_PAINT):
LRESULT CMainDlg::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { CPaintDC dc(m_hWnd); if (pic){ OLE_XSIZE_HIMETRIC SizeX; OLE_YSIZE_HIMETRIC SizeY; pic->get_Height(&SizeY); pic->get_Width(&SizeX); RECT rc; GetClientRect(&rc); //Собственно вывод pic->Render(dc,0,0,rc.right,rc.bottom,0,SizeY,SizeX,-SizeY,NULL); } return 0; } |
Загружать файл в базу на С++ примерно так же просто, как и получать:
#define TESTHR(hr) do{HRESULT _hr = hr; if (FAILED(_hr)){ \ IErrorInfo* ef = 0; GetErrorInfo(0,&ef); \ _com_raise_error(_hr,ef);}}while(false) void UploadPicture2DB(PCWSTR szPath) { try{ CComVariant vtMissing(DISP_E_PARAMNOTFOUND,VT_ERROR); //Загружаем картинку из файла TESTHR(OleLoadPicturePath(szPath,NULL,NULL,0,IID_IPicture,(void**)&pic)); CComPtr<ADOStream> stream; TESTHR(stream.CoCreateInstance(L"ADODB.Stream")); TESTHR(stream->put_Type(adTypeBinary)); TESTHR(stream->Open(vtMissing)); CComQIPtr<IStream> strm(stream); CComQIPtr<IPersistStream> ps(pic); //Сохраняем картинку в объект ADODB.Stream TESTHR(ps->Save(strm,TRUE)); CComPtr<ADORecordset> rs; TESTHR(rs.CoCreateInstance(L"ADODB.Recordset")); TESTHR(rs->Open(CComVariant(L"blob_test"),CComVariant(conn),adOpenStatic, adLockOptimistic,adCmdTable)); //Добавляем новую пустую запись TESTHR(rs->AddNew(vtMissing,vtMissing)); CComPtr<ADOFields> flds; TESTHR(rs->get_Fields(&flds)); CComPtr<ADOField> fld; TESTHR(flds->get_Item(CComVariant(L"img"),&fld)); TESTHR(stream->put_Position(0)); //Заливаем содержимое Stream-а в поле img CComVariant v; TESTHR(stream->Read(adReadAll,&v)); TESTHR(fld->put_Value(v)); //Сохраняем изменения в БД TESTHR(rs->Update(vtMissing,vtMissing)); } catch(_com_error& e){ MessageBox(e.Description(),_T("Error"),MB_ICONERROR); } return; } |
Чтобы максимально упростить пример, я использовал исключения вместо анализа кодов ошибок и стандартный класс _com_error, который определен в файле comutil.h.
Кроме этого, в примере нет кода открытия соединения, так как предполагается, что в момент вызова этой функции соединение с БД уже открыто (глобальная переменная conn).
При чтении данных типа text или ntext, тип варианта будет VT_BSTR. Я не думаю, что с ним могут возникнуть какие-либо проблемы.
ПРИМЕЧАНИЕ В случае бинарных данных тип варианта VT_ARRAY|VT_UI1. |
Теперь давайте рассмотрим метод добавления бинарных данных в базу с помощью хранимых процедур.
Наша процедура будет выглядеть следующим образом:
create proc AddBlob(@img image) as insert into blob_test(img) values(@img) |
К счастью, в ADO.NET работа с потоками не слишком изменилась (их по-прежнему нужно откручивать назад).
Алгоритм загрузки графического файла с диска в базу может быть таким:
Получаем имя файла.
Открываем соединение.
Создаем объект FileStream.
Читаем данные из файла.
Создаем объект SqlCommand для вызова хранимой процедуры.
Одному из параметров передаем считанные данные.
Вызываем метода ExecuteNonQuery объекта SqlCommand.
Параметры соединения с базой данных можно настроить в окне дизайнера, поэтому в коде этого делать не придется, нужно лишь вызвать функцию Open без параметров – и соединение будет установлено.
ofd1.Filter = "(*.bmp)|*.bmp" If ofd1.ShowDialog() = DialogResult.OK Then sb.Text = "connecting to database..." sb.Refresh() 'Если соединение не открыто, открываем If conn.State <> ConnectionState.Open Then conn.Open() End If sb.Text = "loading image..." sb.Refresh() Dim stream As New FileStream(ofd1.FileName, FileMode.Open) Dim b() As Byte ReDim b(CInt(stream.Length)) 'Чтение данных из файла stream.Read(b, 0, CInt(stream.Length)) 'Создание и подготовка к вызову хранимой процедуры Dim cmd As New SqlClient.SqlCommand("AddBlob", conn) With cmd .CommandType = CommandType.StoredProcedure .Parameters.Add("@img", SqlDbType.Image) With .Parameters("@img") .Direction = ParameterDirection.Input .Value = b End With 'Вызов хранимой процедуры .ExecuteNonQuery() End With sb.Text = "Ready" End If |
Все выглядит достаточно просто, и если учитывать, что весь мусор уберет GC, жизнь становится совсем легкой.
А что, если у вас уже есть изображение (скажем, в объекте PictureBox) и вам нужно сохранить его в базу? В этом случае нужно использовать другой поток – MemoryStream. Вот как это может быть сделано:
sb.Text = "connecting to database..." sb.Refresh() 'Если соединение не открыто, открываем If conn.State <> ConnectionState.Open Then conn.Open() End If sb.Text = "loading image..." sb.Refresh() 'Создание и подготовка к вызову хранимой процедуры Dim cmd As New SqlClient.SqlCommand("AddBlob", conn) cmd.CommandType = CommandType.StoredProcedure 'Сохранение изображения в поток в памяти в формате BMP Dim stream As New MemoryStream() img1.Image.Save(stream, Imaging.ImageFormat.Bmp) stream.Seek(0, SeekOrigin.Begin) 'Подготовка параметров cmd.Parameters.Add("@img", SqlDbType.Image) With cmd.Parameters("@img") .Direction = ParameterDirection.Input 'Воспользуемся удобным методом ToArray. Жалко что его нет у FileStream-a .Value = stream.ToArray() End With 'Вызов хранимой процедуры cmd.ExecuteNonQuery() sb.Text = "Ready" |
Теперь рассмотрим случай, когда нужно извлекать изображение из базы с помощью ADO.NET. Последовательность действий может быть такой:
Открыть соединение.
Подготовить команду (SqlCommand).
Заполнить SqlDataReader – аналог ADO Recordset в очень урезанном варианте.
Считать данные в MemoryStream.
Загрузить Image из потока.
Перед чтением данных из DataReader-а необходимо вызвать метод Read. Вот реализация:
sb.Text = "connecting to database..." sb.Refresh() 'Установка соединения If conn.State <> ConnectionState.Open Then conn.Open() End If sb.Text = "Loading image..." sb.Refresh() 'Подготовка запроса на выборку данных Dim cmd As New SqlClient.SqlCommand("select img from blob_test where id = 3", conn) 'Создание и заполнение объекта DataReader Dim reader As SqlDataReader = cmd.ExecuteReader() Dim ms As New MemoryStream() 'Переход на первую строку reader.Read() Dim bb() As Byte bb = reader.Item("img") 'Запись данных в поток в памяти ms.Write(bb, 0, CInt(bb.Length)) ms.Seek(0, SeekOrigin.Begin) 'Загружаем графическое изображение в Image pb.Image = Image.FromStream(ms) sb.Text = "Ready" |
Если нужно читать данные порциями, а не целиком, как это сделано в примере, воспользуйтесь методом GetBytes, который аналогичен ADO-методу GetChunk. Однако если объем данных очень велик, это не сильно поможет, так как они передаются клиенту все сразу в момент вызова метода Read. Для того чтобы данные передавались на клиента только по запросу, необходимо передать в качестве параметра метода ExecuteReader флаг CommandBehavior.SequentialAccess. В этом случае данные будут считываться непосредственно в момент вызова GetBytes.
ПРИМЕЧАНИЕ Для этого флага есть одно замечание. Если вы выбираете несколько полей, считывать их значения вы должны в той последовательности, в которой они возвращаются из базы. Видимо поэтому флаг называется «Последовательный доступ». |
Я знаю, что многие не любят хранимые процедуры. Действительно, для использования хранимых процедур вы должны быть более-менее знакомы с T-SQL и знать общие принципы работы с реляционными БД. Кроме этого, считается, что архитектура, в которой слишком большой упор сделан на вынесение логики на уровень SQL Server-а, имеет ряд недостатков: плохое масштабирование, сложное сопровождение и отладка. Отчасти все это правда, однако лично я все же стараюсь использовать хранимые процедуры везде, где возможно, и лишь в тех случаях, когда бизнес-логика действительно сложна и требует взаимодействия нескольких бизнес-компонентов, отказываюсь от их использования. Тем, кто не хочет использовать хранимые процедуры для обновления больших объектов, я советую прочитать статью
http://support.microsoft.com/default.aspx?scid=kb;en-us;Q309158&ID=kb;en-us;Q309158, в которой излагается метод чтения и записи больших объектов с помощью DataSet.