Няма вградена ADO.Net функционалност, която да се справя наистина грациозно с големи данни. Проблемът е два пъти:
- няма API за „записване“ в SQL команда(и) или параметри като в поток. Типовете параметри, които приемат поток (като
FileStream
) приемете потока за ЧЕТЕНЕ от него, което не е в съгласие със семантиката на сериализация на write в поток. Независимо по какъв начин го включите, в крайна сметка ще получите копие в паметта на целия сериализиран обект, лошо. - дори и точката по-горе да бъде решена (а не може да бъде), протоколът TDS и начинът, по който SQL Server приема параметри, не работят добре с големи параметри, тъй като цялата заявка трябва първо да бъде получена, преди да бъде стартирана в изпълнение и това ще създаде допълнителни копия на обекта в SQL Server.
Така че наистина трябва да подходите към това от различен ъгъл. За щастие има доста лесно решение. Номерът е да използвате високоефективния UPDATE .WRITE
синтаксис и предават парчетата данни една по една в серия от T-SQL оператори. Това е препоръчания от MSDN начин, вижте Промяна на данни с големи стойности (макс.) в ADO.NET. Това изглежда сложно, но всъщност е тривиално да се направи и да се включи в клас Stream.
Класът BlobStream
Това е хлябът и маслото на разтвора. Клас, извлечен от поток, който реализира метода Write като извикване на синтаксиса на T-SQL BLOB WRITE. Направо, единственото интересно нещо в него е, че трябва да следи първата актуализация, защото UPDATE ... SET blob.WRITE(...)
синтаксисът ще се провали в поле NULL:
class BlobStream: Stream
{
private SqlCommand cmdAppendChunk;
private SqlCommand cmdFirstChunk;
private SqlConnection connection;
private SqlTransaction transaction;
private SqlParameter paramChunk;
private SqlParameter paramLength;
private long offset;
public BlobStream(
SqlConnection connection,
SqlTransaction transaction,
string schemaName,
string tableName,
string blobColumn,
string keyColumn,
object keyValue)
{
this.transaction = transaction;
this.connection = connection;
cmdFirstChunk = new SqlCommand(String.Format(@"
UPDATE [{0}].[{1}]
SET [{2}] = @firstChunk
WHERE [{3}] = @key"
,schemaName, tableName, blobColumn, keyColumn)
, connection, transaction);
cmdFirstChunk.Parameters.AddWithValue("@key", keyValue);
cmdAppendChunk = new SqlCommand(String.Format(@"
UPDATE [{0}].[{1}]
SET [{2}].WRITE(@chunk, NULL, NULL)
WHERE [{3}] = @key"
, schemaName, tableName, blobColumn, keyColumn)
, connection, transaction);
cmdAppendChunk.Parameters.AddWithValue("@key", keyValue);
paramChunk = new SqlParameter("@chunk", SqlDbType.VarBinary, -1);
cmdAppendChunk.Parameters.Add(paramChunk);
}
public override void Write(byte[] buffer, int index, int count)
{
byte[] bytesToWrite = buffer;
if (index != 0 || count != buffer.Length)
{
bytesToWrite = new MemoryStream(buffer, index, count).ToArray();
}
if (offset == 0)
{
cmdFirstChunk.Parameters.AddWithValue("@firstChunk", bytesToWrite);
cmdFirstChunk.ExecuteNonQuery();
offset = count;
}
else
{
paramChunk.Value = bytesToWrite;
cmdAppendChunk.ExecuteNonQuery();
offset += count;
}
}
// Rest of the abstract Stream implementation
}
Използване на BlobStream
За да използвате този новосъздадени blob stream клас, вие включвате в BufferedStream
. Класът има тривиален дизайн, който обработва само записването на потока в колона на таблица. Ще използвам повторно таблица от друг пример:
CREATE TABLE [dbo].[Uploads](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FileName] [varchar](256) NULL,
[ContentType] [varchar](256) NULL,
[FileData] [varbinary](max) NULL)
Ще добавя фиктивен обект, който да бъде сериализиран:
[Serializable]
class HugeSerialized
{
public byte[] theBigArray { get; set; }
}
И накрая, действителната сериализация. Първо ще вмъкнем нов запис в Uploads
таблица, след което създайте BlobStream
върху нововмъкнатия идентификатор и извикайте сериализацията направо в този поток:
using (SqlConnection conn = new SqlConnection(Settings.Default.connString))
{
conn.Open();
using (SqlTransaction trn = conn.BeginTransaction())
{
SqlCommand cmdInsert = new SqlCommand(
@"INSERT INTO dbo.Uploads (FileName, ContentType)
VALUES (@fileName, @contentType);
SET @id = SCOPE_IDENTITY();", conn, trn);
cmdInsert.Parameters.AddWithValue("@fileName", "Demo");
cmdInsert.Parameters.AddWithValue("@contentType", "application/octet-stream");
SqlParameter paramId = new SqlParameter("@id", SqlDbType.Int);
paramId.Direction = ParameterDirection.Output;
cmdInsert.Parameters.Add(paramId);
cmdInsert.ExecuteNonQuery();
BlobStream blob = new BlobStream(
conn, trn, "dbo", "Uploads", "FileData", "Id", paramId.Value);
BufferedStream bufferedBlob = new BufferedStream(blob, 8040);
HugeSerialized big = new HugeSerialized { theBigArray = new byte[1024 * 1024] };
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(bufferedBlob, big);
trn.Commit();
}
}
Ако наблюдавате изпълнението на тази проста извадка, ще видите, че никъде не е създаден голям поток за сериализация. Пробата ще разпредели масива от [1024*1024], но това е за демонстрационни цели, за да има нещо за сериализиране. Този код се сериализира по буфериран начин, парче по парче, използвайки препоръчания размер за актуализация на SQL Server BLOB от 8040 байта наведнъж.