Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Como serializar um gráfico grande de objeto .NET em um SQL Server BLOB sem criar um buffer grande?


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.