Non esiste alcuna funzionalità ADO.Net incorporata per gestirlo in modo davvero elegante per dati di grandi dimensioni. Il problema è duplice:
- non esiste alcuna API da "scrivere" in uno o più comandi SQL o parametri come in un flusso. I tipi di parametro che accettano uno stream (come
FileStream
) accetta lo stream per LEGGERE da esso, che non è d'accordo con la semantica di serializzazione di write in un ruscello. Non importa in che modo lo giri, finisci con una copia in memoria dell'intero oggetto serializzato, male. - anche se il punto sopra sarebbe risolto (e non può essere), il protocollo TDS e il modo in cui SQL Server accetta i parametri non funzionano bene con parametri di grandi dimensioni poiché l'intera richiesta deve essere ricevuta prima di essere avviata in esecuzione e questo creerebbe copie aggiuntive dell'oggetto all'interno di SQL Server.
Quindi devi davvero avvicinarti a questo da un'angolazione diversa. Fortunatamente, esiste una soluzione abbastanza semplice. Il trucco è usare l'efficientissimo UPDATE .WRITE
sintassi e passare i blocchi di dati uno per uno, in una serie di istruzioni T-SQL. Questo è il metodo consigliato da MSDN, vedere Modifica di dati di valore elevato (max) in ADO.NET. Sembra complicato, ma in realtà è banale da fare e collegarlo a una classe Stream.
La classe BlobStream
Questo è il pane e burro della soluzione. Una classe derivata da Stream che implementa il metodo Write come chiamata alla sintassi T-SQL BLOB WRITE. Semplicemente, l'unica cosa interessante è che deve tenere traccia del primo aggiornamento perché UPDATE ... SET blob.WRITE(...)
la sintassi fallirebbe su 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
}
Utilizzo di BlobStream
Per utilizzare questa classe di stream BLOB appena creata, ti colleghi a un BufferedStream
. La classe ha un design banale che gestisce solo la scrittura del flusso in una colonna di una tabella. Riutilizzerò una tabella da un altro esempio:
CREATE TABLE [dbo].[Uploads](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FileName] [varchar](256) NULL,
[ContentType] [varchar](256) NULL,
[FileData] [varbinary](max) NULL)
Aggiungerò un oggetto fittizio da serializzare:
[Serializable]
class HugeSerialized
{
public byte[] theBigArray { get; set; }
}
Infine, la serializzazione vera e propria. Per prima cosa inseriremo un nuovo record nei Uploads
tabella, quindi crea un BlobStream
sull'ID appena inserito e chiama la serializzazione direttamente in questo flusso:
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 monitori l'esecuzione di questo semplice esempio, vedrai che da nessuna parte viene creato un flusso di serializzazione di grandi dimensioni. L'esempio allocherà l'array di [1024*1024] ma è a scopo dimostrativo per avere qualcosa da serializzare. Questo codice viene serializzato in modo bufferizzato, blocco per blocco, usando la dimensione di aggiornamento consigliata per BLOB di SQL Server di 8040 byte alla volta.