Große Dateien hochladen und in eine Datenbank schreiben
Auch wenn es meistens nicht Ideal ist Dateien in der Datenbank abzulegen gibt es dennoch hin und wieder Fälle in denen es dennoch sinnvoll ist.
Als häufigster Grund um Dateien in der Datenbank abzulegen wird immer wieder einfacheres Backup der Daten genannt. En weiterer Grund ist, dass man über sog. IFilter auch die Volltextsuche des SQL Server über die Dokumente laufen lassen kann.
Die Nachteile liegen auf der Hand: schlechtere Performance, Fehleranfälligkeit und Mehraufwand.
Microsoft beschreibt in dem Artikel "Sparsames Verwenden von Ressourcen beim Schreiben von BLOB-Werten in SQL Server" wie man Dateien in einer Datenbank ablegt ohne die Ressourcen zu stark zu beanspruchen.
Der Trick an der Sache ist, die Daten immer in kleinen Stückchen aus dem Stream auszulesen und in die Datenbank zu schreiben.
Unten habe ich ein Beispielprojekt verlinkt in dem gezeigt wird, wie die vom Microsoft beschriebene Methode in einer ASP.NET Anwendung mit einem FileUplad angewendet werden kann.
In dem Beispielprojekt habe ich eine Klasse mit dem Namen "BinaryManager" angelegt, die statische Methoden für die Datenbankoperationen enthält.
Das stückchenweise Schreiben in die Datenbank löst Microsoft mit einem Pointer auf die Binärspalte des aktuellen Datensatzes. Mehr zu den Pointern: TEXTPTR (Hinweis: Microsoft schreibt hier das diese Funktion in Zukunft im SQL Server nicht mehr unterstützt wird. So wie es aussieht, scheint es aber im 2008er noch enthalten zu sein. Getestet habe ich es mit dem 2005er und mit dem 2000er schon des Öfteren angewendet.)
INSERT INTO allfiles (filename, contenttype, filesize, filecontent) VALUES(@filename, @contenttype, @filesize, 0x0);
SELECT @Identity = SCOPE_IDENTITY();
SELECT @Pointer = TEXTPTR(filecontent) FROM allfiles WHERE id = @Identity
Hier wird also erst mal der Datensatz mit den nötigen Infos und ohne die Datei angelegt. In die Binärspalte wird als Standardwert NULL geschrieben. Anschließend wird die neue ID ermittelt und anhand dieser der Pointer erstellt. Sowohl die neue ID als auch der Pointer werden per SqlParameter ausgelesen:
SqlParameter idParm = comd.Parameters.Add("@Identity", SqlDbType.Int);
idParm.Direction = ParameterDirection.Output;
SqlParameter ptrParm = comd.Parameters.Add("@Pointer", SqlDbType.Binary, 16);
ptrParm.Direction = ParameterDirection.Output;
Der Pointer wird dann zusammen mit dem Stream an eine weitere Methode übergeben in der die eigentliche Arbeit stattfindet.
Mit Hilfe der T-SQL Funktion UPDATETEXT werden jetzt immer nur 1024 Bytes auf einmal in die Datenbank geschrieben, so lange bis der BinaryReader am Ende des Streams angekommen ist.
int bufferLen = 1024;
[...]
using (BinaryReader br = new BinaryReader(InputStream))
{
byte[] buffer = br.ReadBytes(bufferLen);
int offset = 0;
while (buffer.Length > 0)
{
binParm.Value = buffer;
command.ExecuteNonQuery();
offset += bufferLen;
offsetParm.Value = offset;
buffer = br.ReadBytes(bufferLen);
}
br.Close();
InputStream.Close();
}
Die Puffergröße (bufferLen) bestimmt nicht nur Größe der Datenpakete und somit auch die Anzahl der Schleifendurchläufe. Ein niedriger Wert erhöht die Schleifendurchläufe reduziert den Speicherbedarf, erhöht aber die Prozessorlast. Umgekehrt verringert ein höherer Wert die Prozessorlast, erhöht aber den Speicherbedarf. Man sollte also mit der Größe spielen um den perfekten Mittelwert zu finden.
Der eigentliche Upload ist dann nur noch ein Kinderspiel:
protected void Button1_Click(object sender, EventArgs e)
{
int newid = BinaryManager.SaveToDB(
this.FileUpload1.PostedFile.InputStream,
this.FileUpload1.PostedFile.FileName,
this.FileUpload1.PostedFile.ContentType,
this.FileUpload1.PostedFile.ContentLength);
}
Beispielprojekt: download (228KB)