Não há nenhuma funcionalidade ADO.Net interna para lidar com isso de maneira realmente graciosa para grandes dados. O problema é duplo:
- não há API para 'gravar' em comandos ou parâmetros SQL como em um fluxo. Os tipos de parâmetros que aceitam um fluxo (como
FileStream
) aceite a transmissão para LER dele, que não concorda com a semântica de serialização de write em um fluxo. Não importa de que maneira você gire isso, você acaba com uma cópia na memória de todo o objeto serializado, ruim. - mesmo que o ponto acima fosse resolvido (e não pode ser), o protocolo TDS e a forma como o SQL Server aceita parâmetros não funcionam bem com parâmetros grandes, pois toda a solicitação deve ser recebida primeiro antes de ser lançada em execução e isso criaria cópias adicionais do objeto dentro do SQL Server.
Então você realmente tem que abordar isso de um ângulo diferente. Felizmente, existe uma solução bastante fácil. O truque é usar o altamente eficiente
UPDATE .WRITE
sintaxe e passar os pedaços de dados um por um, em uma série de instruções T-SQL. Essa é a maneira recomendada do MSDN, consulte Modificando dados de grande valor (máximo) no ADO.NET. Isso parece complicado, mas na verdade é trivial de fazer e conectar a uma classe Stream. A classe BlobStream
Este é o pão com manteiga da solução. Uma classe derivada de Stream que implementa o método Write como uma chamada para a sintaxe T-SQL BLOB WRITE. Direto, a única coisa interessante sobre isso é que ele precisa acompanhar a primeira atualização porque o
UPDATE ... SET blob.WRITE(...)
sintaxe falharia em um campo 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
}
Usando o BlobStream
Para usar essa classe de fluxo de blob recém-criada, você conecta a um
BufferedStream
. A classe tem um design trivial que lida apenas com a gravação do fluxo em uma coluna de uma tabela. Vou reutilizar uma tabela de outro exemplo:CREATE TABLE [dbo].[Uploads](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FileName] [varchar](256) NULL,
[ContentType] [varchar](256) NULL,
[FileData] [varbinary](max) NULL)
Vou adicionar um objeto fictício a ser serializado:
[Serializable]
class HugeSerialized
{
public byte[] theBigArray { get; set; }
}
Finalmente, a serialização real. Primeiro, inseriremos um novo registro em
Uploads
tabela e crie um BlobStream
no Id recém-inserido e chame a serialização diretamente para este stream: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();
}
}
Se você monitorar a execução desse exemplo simples, verá que em nenhum lugar um grande fluxo de serialização é criado. A amostra alocará a matriz de [1024*1024], mas isso é para fins de demonstração, para ter algo para serializar. Esse código é serializado de maneira em buffer, parte por parte, usando o tamanho de atualização recomendado do SQL Server BLOB de 8040 bytes por vez.