Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Wie kann ich ein großes Diagramm eines .NET-Objekts in ein SQL Server-BLOB serialisieren, ohne einen großen Puffer zu erstellen?

Es gibt keine integrierte ADO.Net-Funktionalität, um dies bei großen Datenmengen wirklich elegant zu handhaben. Das Problem ist zweifach:

  • es gibt keine API, um in einen oder mehrere SQL-Befehle oder Parameter wie in einen Stream zu „schreiben“. Die Parametertypen, die einen Stream akzeptieren (wie FileStream ) akzeptieren Sie den Stream zu READ davon, was nicht mit der Serialisierungssemantik von write übereinstimmt in einen Bach. Egal in welche Richtung Sie dies drehen, Sie erhalten am Ende eine Kopie des gesamten serialisierten Objekts im Speicher, schlecht.
  • selbst wenn der obige Punkt gelöst wäre (und es nicht sein kann), funktionieren das TDS-Protokoll und die Art und Weise, wie SQL Server Parameter akzeptiert, nicht gut mit großen Parametern, da die gesamte Anfrage zuerst empfangen werden muss, bevor sie zur Ausführung gestartet wird und dies würde zusätzliche Kopien des Objekts innerhalb von SQL Server erstellen.

Also muss man das wirklich aus einem anderen Blickwinkel angehen. Glücklicherweise gibt es eine ziemlich einfache Lösung. Der Trick besteht darin, das hocheffiziente UPDATE .WRITE zu verwenden Syntax und übergeben Sie die Datenblöcke einzeln in einer Reihe von T-SQL-Anweisungen. Dies ist die von MSDN empfohlene Methode, siehe Modifying Large-Value (max) Data in ADO.NET. Das sieht kompliziert aus, ist aber eigentlich trivial und in eine Stream-Klasse einzufügen.

Die BlobStream-Klasse

Das ist das Brot und die Butter der Lösung. Eine von Stream abgeleitete Klasse, die die Write-Methode als Aufruf der T-SQL BLOB WRITE-Syntax implementiert. Geradeaus, das einzig Interessante daran ist, dass es das erste Update verfolgen muss, weil UPDATE ... SET blob.WRITE(...) Syntax würde bei einem NULL-Feld fehlschlagen:

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 verwenden

Um diese neu erstellte Blob-Stream-Klasse zu verwenden, stecken Sie sie in einen BufferedStream . Die Klasse hat ein triviales Design, das nur das Schreiben des Streams in eine Spalte einer Tabelle handhabt. Ich werde eine Tabelle aus einem anderen Beispiel wiederverwenden:

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

Ich füge ein zu serialisierendes Dummy-Objekt hinzu:

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

Zum Schluss die eigentliche Serialisierung. Wir werden zuerst einen neuen Datensatz in die Uploads einfügen Tabelle, erstellen Sie dann einen BlobStream auf die neu eingefügte ID und rufen Sie die Serialisierung direkt in diesen Stream auf:

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

Wenn Sie die Ausführung dieses einfachen Beispiels überwachen, werden Sie feststellen, dass nirgendwo ein großer Serialisierungsstrom erstellt wird. Das Beispiel weist das Array von [1024*1024] zu, aber das dient zu Demozwecken, um etwas zum Serialisieren zu haben. Dieser Code wird gepuffert, Stück für Stück, unter Verwendung der empfohlenen SQL Server-BLOB-Aktualisierungsgröße von jeweils 8040 Byte serialisiert.