Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Comment sérialiser un grand graphique d'objet .NET dans un BLOB SQL Server sans créer de grand tampon ?

Il n'y a pas de fonctionnalité ADO.Net intégrée pour gérer cela avec élégance pour les données volumineuses. Le problème est double :

  • il n'y a pas d'API pour "écrire" dans une ou des commandes SQL ou des paramètres comme dans un flux. Les types de paramètres qui acceptent un flux (comme FileStream ) accepter le flux pour LIRE de celui-ci, ce qui n'est pas d'accord avec la sémantique de sérialisation de write dans un ruisseau. Quelle que soit la manière dont vous tournez cela, vous vous retrouvez avec une copie en mémoire de l'intégralité de l'objet sérialisé, mauvais.
  • même si le point ci-dessus était résolu (et il ne peut pas l'être), le protocole TDS et la façon dont SQL Server accepte les paramètres ne fonctionnent pas bien avec des paramètres volumineux car la requête entière doit d'abord être reçue avant d'être lancée en exécution et cela créerait des copies supplémentaires de l'objet dans SQL Server.

Il faut donc vraiment aborder cela sous un angle différent. Heureusement, il existe une solution assez simple. L'astuce consiste à utiliser le très efficace UPDATE .WRITE syntaxe et transmettre les blocs de données un par un, dans une série d'instructions T-SQL. Il s'agit de la méthode recommandée par MSDN, voir Modification des données de grande valeur (max) dans ADO.NET. Cela semble compliqué, mais est en fait trivial à faire et à brancher sur une classe Stream.

La classe BlobStream

C'est le pain et le beurre de la solution. Une classe dérivée de Stream qui implémente la méthode Write en tant qu'appel à la syntaxe T-SQL BLOB WRITE. Simplement, la seule chose intéressante à ce sujet est qu'il doit garder une trace de la première mise à jour car le UPDATE ... SET blob.WRITE(...) la syntaxe échouerait sur un champ 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
 }

Utiliser le BlobStream

Pour utiliser cette classe de flux de blob nouvellement créée, vous vous connectez à un BufferedStream . La classe a une conception triviale qui gère uniquement l'écriture du flux dans une colonne d'une table. Je vais réutiliser une table d'un autre exemple :

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

Je vais ajouter un objet factice à sérialiser :

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

Enfin, la sérialisation proprement dite. Nous allons d'abord insérer un nouvel enregistrement dans les Uploads table, puis créez un BlobStream sur l'identifiant nouvellement inséré et appelez la sérialisation directement dans ce flux :

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 vous surveillez l'exécution de cet exemple simple, vous verrez qu'aucun flux de sérialisation volumineux n'est créé nulle part. L'exemple allouera le tableau de [1024*1024] mais c'est à des fins de démonstration pour avoir quelque chose à sérialiser. Ce code sérialise de manière mise en mémoire tampon, morceau par morceau, en utilisant la taille de mise à jour recommandée par SQL Server BLOB de 8040 octets à la fois.