sql >> Base de Datos >  >> RDS >> Sqlserver

¿Cómo serializo un gráfico grande de un objeto .NET en un BLOB de SQL Server sin crear un búfer grande?

No hay una funcionalidad integrada de ADO.Net para manejar esto con mucha gracia para grandes datos. El problema es doble:

  • no hay una API para 'escribir' en un comando o parámetros SQL como en una secuencia. Los tipos de parámetros que aceptan una secuencia (como FileStream ) acepta la transmisión para LEER de él, que no concuerda con la semántica de serialización de write en un arroyo. No importa de qué manera lo gire, terminará con una copia en memoria de todo el objeto serializado, mal.
  • incluso si el punto anterior se resolviera (y no se puede), el protocolo TDS y la forma en que SQL Server acepta los parámetros no funcionan bien con parámetros grandes, ya que primero se debe recibir la solicitud completa antes de iniciar la ejecución. y esto crearía copias adicionales del objeto dentro de SQL Server.

Así que realmente tienes que abordar esto desde un ángulo diferente. Afortunadamente, hay una solución bastante fácil. El truco es usar el altamente eficiente UPDATE .WRITE sintaxis y pasar los fragmentos de datos uno por uno, en una serie de instrucciones T-SQL. Esta es la forma recomendada por MSDN, consulte Modificación de datos de gran valor (máximo) en ADO.NET. Esto parece complicado, pero en realidad es trivial de hacer y conectarlo a una clase Stream.

La clase BlobStream

Este es el pan y la mantequilla de la solución. Una clase derivada de Stream que implementa el método Write como una llamada a la sintaxis T-SQL BLOB WRITE. Directamente, lo único interesante de esto es que tiene que realizar un seguimiento de la primera actualización porque UPDATE ... SET blob.WRITE(...) la sintaxis fallaría en un 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
 }

Uso de BlobStream

Para usar esta clase de flujo de blob recién creada, se conecta a un BufferedStream . La clase tiene un diseño trivial que maneja solo escribir la secuencia en una columna de una tabla. Reutilizaré una tabla de otro ejemplo:

CREATE TABLE [dbo].[Uploads](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [FileName] [varchar](256) NULL,
    [ContentType] [varchar](256) NULL,
    [FileData] [varbinary](max) NULL)

Agregaré un objeto ficticio para serializar:

[Serializable]
class HugeSerialized
{
    public byte[] theBigArray { get; set; }
}

Finalmente, la serialización real. Primero insertaremos un nuevo registro en Uploads tabla, luego crea un BlobStream en el ID recién insertado y llame a la serialización directamente a esta secuencia:

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();
    }
}

Si supervisa la ejecución de este ejemplo simple, verá que en ninguna parte se crea un gran flujo de serialización. La muestra asignará la matriz de [1024 * 1024] pero eso es para fines de demostración para tener algo para serializar. Este código se serializa en forma de búfer, fragmento por fragmento, utilizando el tamaño de actualización recomendado de SQL Server BLOB de 8040 bytes a la vez.