SUMMARY
This step-by-step article describes how to use the
Microsoft SQL Server READTEXT and UPDATETEXT statements to read and write data
from BLOB (LongVarBinary) columns in a database table.
Because of
network constraints, sometimes you may need to retrieve a large BLOB file in
smaller chunks and then piece the chunks together rather than retrieve the
whole BLOB all at once. However, ADO.NET data providers do not have
GetChunk and
AppendChunk methods available to the Data Access Object (DAO) and ActiveX
Data Objects (ADO)
Recordset objects. This article describes alternate ways to retrieve data
in smaller chunks.
NOTES:
back to the topRequirements
The following items describe the recommended hardware, software,
network infrastructure, skills and knowledge, and service packs you will need:
- Microsoft Windows 2000 Professional, Microsoft Windows 2000
Server, Microsoft Windows 2000 Advanced Server, or Microsoft Windows NT 4.0
Server
- Microsoft Visual Studio .NET
- Microsoft SQL Server 7.0 or later
This article assumes that you are familiar with the following
topics:
- Visual Studio .NET
- ADO.NET fundamentals and syntax
back to the topRead Chunks from a BLOB Column
The following functions use the SQL Server READTEXT statement and
DataReader to retrieve a portion of the BLOB value in a single-row,
single-column rowset. Two commands perform this task: the first command
retrieves the size of the BLOB field and a pointer to its location; the second
command executes the READTEXT command, which retrieves the chunk of data into a
Byte array, and then increments an
Offset. The
System.IO.Filesream object writes the
Byte array to disk.
void SqlChunkBlob2File(String *DestFilePath)
{
try
{
int PictureCol = 0; // position of Picture column in DataReader
int BUFFER_LENGTH = 32768; // chunk size
SqlConnection *cn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;");
// make sure Photo is non-NULL and return TEXTPTR to it
SqlCommand *cmdGetPointer = new SqlCommand("SELECT @Pointer=TEXTPTR(Picture), @Length=DataLength(Picture) FROM Categories WHERE CategoryName='Test'", cn);
SqlParameter *PointerOutParam = cmdGetPointer->Parameters->Add("@Pointer", SqlDbType::VarBinary, 100);
PointerOutParam->Direction = ParameterDirection::Output;
SqlParameter *LengthOutParam = cmdGetPointer->Parameters->Add("@Length", SqlDbType::Int);
LengthOutParam->Direction = ParameterDirection::Output;
cn->Open();
cmdGetPointer->ExecuteNonQuery();
if(PointerOutParam->Value == 0)
{
cn->Close();
// add code to deal with NULL BLOB
return;
}
// Set up READTEXT command, parameters, and open BinaryReader
SqlCommand *cmdReadBinary = new SqlCommand("READTEXT Categories.Picture @Pointer @Offset @Size HOLDLOCK", cn);
SqlParameter *PointerParam = cmdReadBinary->Parameters->Add("@Pointer", SqlDbType::Binary, 16);
SqlParameter *OffsetParam = cmdReadBinary->Parameters->Add("@Offset", SqlDbType::Int);
SqlParameter *SizeParam = cmdReadBinary->Parameters->Add("@Size", SqlDbType::Int);
SqlDataReader *dr;
FileStream *fs = new FileStream(DestFilePath, FileMode::Create, FileAccess::Write);
int Offset= 0;
OffsetParam->Value = __box(Offset);
Byte Buffer[] = __gc new Byte[BUFFER_LENGTH ];
// Read buffer full of data and write to the file stream
do
{
PointerParam->Value = PointerOutParam->Value;
// Calculate buffer size - may be less than BUFFER_LENGTH for last block
if( (Offset + BUFFER_LENGTH) >= Convert::ToInt32(LengthOutParam->Value))
SizeParam->Value = __box(Convert::ToInt32(LengthOutParam->Value) - Offset);
else SizeParam->Value = __box(BUFFER_LENGTH);
dr = cmdReadBinary->ExecuteReader(CommandBehavior::SingleResult);
dr->Read();
dr->GetBytes(PictureCol, 0, Buffer, 0,Convert::ToInt32(SizeParam->Value));
dr->Close();
fs->Write(Buffer, 0, Convert::ToInt32(SizeParam->Value));
Offset += Convert::ToInt32(SizeParam->Value);
OffsetParam->Value = __box(Offset);
}while(Offset <Convert::ToInt32(LengthOutParam->Value));
fs->Close();
cn->Close();
Console::WriteLine("SqlChunkBlob2File executed successfully.\nPress return to continue.");
Console::ReadLine();
}catch(SqlException *ex)
{
Console::WriteLine(ex->Message);
}
}
void OleDbChunkBlob2File(String *DestFilePath)
{
try
{
int PictureCol = 0; // position of Picture column in DataReader
int BUFFER_LENGTH = 32768; // chunk size
OleDbConnection *cn = new OleDbConnection("Provider=SQLOLEDB;Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;");
// make sure Photo is non-NULL and return TEXTPTR to it
OleDbCommand *cmdGetPointer = new OleDbCommand("SELECT ?=TEXTPTR(Picture), ?=DataLength(Picture) FROM Categories WHERE CategoryName='Test'", cn);
OleDbParameter *PointerOutParam = cmdGetPointer->Parameters->Add("@Pointer", OleDbType::VarBinary, 100);
PointerOutParam->Direction = ParameterDirection::Output;
OleDbParameter *LengthOutParam = cmdGetPointer->Parameters->Add("@Length", OleDbType::Integer);
LengthOutParam->Direction = ParameterDirection::Output;
cn->Open();
cmdGetPointer->ExecuteNonQuery();
if(PointerOutParam->Value == 0)
{
cn->Close();
// add code to deal with NULL BLOB
return;
}
// Set up READTEXT command, parameters, and open BinaryReader
OleDbCommand *cmdReadBinary = new OleDbCommand("READTEXT Categories.Picture ? ? ? HOLDLOCK", cn);
OleDbParameter *PointerParam = cmdReadBinary->Parameters->Add("@Pointer", OleDbType::Binary, 16);
OleDbParameter *OffsetParam = cmdReadBinary->Parameters->Add("@Offset", OleDbType::Integer);
OleDbParameter *SizeParam = cmdReadBinary->Parameters->Add("@Size", OleDbType::Integer);
OleDbDataReader *dr;
FileStream *fs = new FileStream(DestFilePath, FileMode::Create, FileAccess::Write);
int Offset= 0;
OffsetParam->Value = __box(Offset);
Byte Buffer[] = __gc new Byte[BUFFER_LENGTH ];
// Read buffer full of data and write to the file stream
do
{
PointerParam->Value = PointerOutParam->Value;
// Calculate buffer size - may be less than BUFFER_LENGTH for last block
if( (Offset + BUFFER_LENGTH) >= Convert::ToInt32(LengthOutParam->Value))
SizeParam->Value = __box(Convert::ToInt32(LengthOutParam->Value) - Offset);
else SizeParam->Value = __box(BUFFER_LENGTH);
dr = cmdReadBinary->ExecuteReader(CommandBehavior::SingleResult);
dr->Read();
dr->GetBytes(PictureCol, 0, Buffer, 0,Convert::ToInt32(SizeParam->Value));
dr->Close();
fs->Write(Buffer, 0, Convert::ToInt32(SizeParam->Value));
Offset += Convert::ToInt32(SizeParam->Value);
OffsetParam->Value = __box(Offset);
}while(Offset <Convert::ToInt32(LengthOutParam->Value));
fs->Close();
cn->Close();
Console::WriteLine("OleDbChunkBlob2File executed successfully.\nPress return to continue.");
Console::ReadLine();
}catch(OleDbException *ex)
{
Console::WriteLine(ex->Message);
}
}
back to the topWrite Chunks to a BLOB Column
The following functions use the
Command object, the
Parameter object, and the SQL Server UPDATETEXT statement to write chunks
of data from a
Byte array to a BLOB column. The BLOB column cannot be
null with this method, therefore a single byte is assigned to the
column before the function retrieves the TEXTPTR.
On the first
execution of the UPDATETEXT statement, the
DeleteParam.Value is set to
1 to delete the existing byte from the column before the function
inserts the chunk. This assignment prevents extraneous data from being appended
to the BLOB. The UPDATETEXT statement is executed multiple times, and the
offset increments with the size of the buffer after each call.
void ChunkFile2SqlBlob(String *SourceFilePath)
{
try
{
int BUFFER_LENGTH = 32768; // chunk size
SqlConnection *cn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;");
// make sure Photo is non-NULL and return TEXTPTR to it
String *sqlstr = "SET NOCOUNT ON;UPDATE Categories SET Picture = 0x0 WHERE CategoryName='Test';SELECT @Pointer=TEXTPTR(Picture) FROM Categories WHERE CategoryName='Test'";
SqlCommand *cmdGetPointer = new SqlCommand(sqlstr, cn);
SqlParameter *PointerOutParam = cmdGetPointer->Parameters->Add("@Pointer", SqlDbType::VarBinary, 100);
PointerOutParam->Direction = ParameterDirection::Output;
cn->Open();
cmdGetPointer->ExecuteNonQuery();
// Set up UPDATETEXT command, parameters, and open BinaryReader
SqlCommand *cmdUploadBinary = new SqlCommand("UPDATETEXT Categories.Picture @Pointer @Offset @Delete WITH LOG @Bytes", cn);
SqlParameter *PointerParam = cmdUploadBinary->Parameters->Add("@Pointer", SqlDbType::Binary, 16);
SqlParameter *OffsetParam= cmdUploadBinary->Parameters->Add("@Offset", SqlDbType::Int);
SqlParameter *DeleteParam = cmdUploadBinary->Parameters->Add("@Delete", SqlDbType::Int);
DeleteParam->Value = __box(1); // delete 0x0 character
SqlParameter *BytesParam = cmdUploadBinary->Parameters->Add("@Bytes", SqlDbType::Binary, BUFFER_LENGTH);
FileStream *fs = new FileStream(SourceFilePath, FileMode::Open, FileAccess::Read);
BinaryReader *br = new BinaryReader(fs);
int Offset = 0;
OffsetParam->Value = __box(Offset);
// Read buffer full of data and execute UPDATETEXT statement
Byte Buffer[] = br->ReadBytes(BUFFER_LENGTH);
while(Buffer->Length > 0)
{
PointerParam->Value = PointerOutParam->Value;
BytesParam->Value = Buffer;
cmdUploadBinary->ExecuteNonQuery();
DeleteParam->Value = 0; //don't delete any other data
Offset += Buffer->Length;
OffsetParam->Value = __box(Offset);
Buffer = br->ReadBytes(BUFFER_LENGTH);
}
br->Close();
fs->Close();
cn->Close();
Console::WriteLine("ChunkFile2SqlBlob completed successfully.\nPress return to continue.");
Console::ReadLine();
}catch(SqlException *ex)
{
Console::WriteLine(ex->Message);
}
}
void ChunkFile2OleDbBlob(String *SourceFilePath)
{
try
{
int BUFFER_LENGTH = 32768; // chunk size
OleDbConnection *cn = new OleDbConnection("Provider=SQLOLEDB;Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;");
// make sure Photo is non-NULL and return TEXTPTR to it
String *sqlstr = "SET NOCOUNT ON;UPDATE Categories SET Picture = 0x0 WHERE CategoryName='Test';SELECT ?=TEXTPTR(Picture) FROM Categories WHERE CategoryName='Test'";
OleDbCommand *cmdGetPointer = new OleDbCommand(sqlstr, cn);
OleDbParameter *PointerOutParam = cmdGetPointer->Parameters->Add("@Pointer", OleDbType::VarBinary, 100);
PointerOutParam->Direction = ParameterDirection::Output;
cn->Open();
cmdGetPointer->ExecuteNonQuery();
// Set up UPDATETEXT command, parameters, and open BinaryReader
OleDbCommand *cmdUploadBinary = new OleDbCommand("UPDATETEXT Categories.Picture ? ? ? WITH LOG ?", cn);
OleDbParameter *PointerParam = cmdUploadBinary->Parameters->Add("@Pointer", OleDbType::Binary, 16);
OleDbParameter *OffsetParam= cmdUploadBinary->Parameters->Add("@Offset", OleDbType::Integer);
OleDbParameter *DeleteParam = cmdUploadBinary->Parameters->Add("@Delete", OleDbType::Integer);
DeleteParam->Value = __box(1); // delete 0x0 character
OleDbParameter *BytesParam = cmdUploadBinary->Parameters->Add("@Bytes", OleDbType::Binary, BUFFER_LENGTH);
FileStream *fs = new FileStream(SourceFilePath, FileMode::Open, FileAccess::Read);
BinaryReader *br = new BinaryReader(fs);
int Offset = 0;
OffsetParam->Value = __box(Offset);
// Read buffer full of data and execute UPDATETEXT statement
Byte Buffer[] = br->ReadBytes(BUFFER_LENGTH);
while(Buffer->Length > 0)
{
PointerParam->Value = PointerOutParam->Value;
BytesParam->Value = Buffer;
cmdUploadBinary->ExecuteNonQuery();
DeleteParam->Value = 0; //don't delete any other data
Offset += Buffer->Length;
OffsetParam->Value = __box(Offset);
Buffer = br->ReadBytes(BUFFER_LENGTH);
}
br->Close();
fs->Close();
cn->Close();
Console::WriteLine("ChunkFile2OleDbBlob completed successfully.\nPress return to continue.");
Console::ReadLine();
}catch(OleDbException *ex)
{
Console::WriteLine(ex->Message);
}
}
back to the topCreate a Project and Add Code
- Open SQL Query Analyzer.
- Type the following command, and then press F5 to change the
default database to Northwind.
use Northwind
- Type the following command, and then press F5 to insert a
new record into the Categories table of the Northwind database.
Insert into categories(categoryname) values ('Test')
NOTE: You add this test record to the Categories table only for the purpose of this example. With the test record
in place, you do not need to modify any of the existing data in this table.
- Start Visual Studio
.NET.
- In Visual .NET 2002, create a new Managed C++ Application.
In Visual .NET 2003, create a new Console Application (.NET).
Name the project "BLOBChunkReadWrite." - You add this test record to the Paste the following sample
code in the BLOBChunkReadWrite.cpp file. Overwrite the existing code generated
by Visual Studio .NET:
#include "stdafx.h"
#using <mscorlib.dll>
#using <System.dll>
#using <System.Data.dll>
using namespace System;
using namespace System::Data;
using namespace System::Data::SqlClient;
using namespace System::Data::OleDb;
using namespace System::IO;
//Prototypes for functions that will do the real work.
void ChunkFile2SqlBlob(String *);
void ChunkFile2OleDbBlob(String *);
void SqlChunkBlob2File(String *);
void OleDbChunkBlob2File(String *);
// This is the entry point for this application.
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{
String *DestFilePath = "c:\\mytest.bmp"; //File we will create from database
String *SourceFilePath = "c:\\windows\\coffee bean.bmp"; //File we will insert into database
//Use the SQL Provider to insert BLOB into the database from the file.
ChunkFile2SqlBlob(SourceFilePath);
//Use the OLE DB Provider to insert BLOB into database from the file.
ChunkFile2OleDbBlob(SourceFilePath);
//Use the SQL Provider to read BLOB from the database into the file.
SqlChunkBlob2File(DestFilePath);
//Use the OLE DB Provider to read BLOB from the database into the file.
OleDbChunkBlob2File(DestFilePath);
return 0;
}
- You add this test record to the Paste the following four
functions at the bottom of the BLOBReadWright.cpp file.NOTE: You may need to change the connection strings in the code as
necessary for your environment. There are four connection strings.
- You add this test record to the Press CTRL+F5 to execute
the code. Notice that messages are written to the Console window that confirm
the success of each function.
back to the topTroubleshooting
- The sample code in this article may not be suitable for use
against the LongVarChar or LongVarWChar columns without modification.
- Remember to modify the connection string and the SQL
statements to suit your own server.
- Add error checking in case your query returns no
records.
- READTEXT and UPDATETEXT are specific to Microsoft SQL
Server. If you use a different database system, you may have similar commands
available to you. If you do not have similar commands available, refer to the
following Microsoft Knowledge Base articles, which describe reading and writing
BLOB data without chunking:
316887 HOW TO: Read and Write a File to and from a BLOB Column by Using ADO.NET and Visual Basic .NET
317017 HOW TO: Read and Write a File to and from a BLOB Column by Using ADO.NET and Visual C++ .NET
back to the top