Performance Tipp: Eine DB Connection global für einen Page Request definieren.
Im Moment arbeite ich gerade an einer kleinen Communitiy Anwendung (ja klein, muss ja nicht immer groß sein) die standardmäßig eine Access Datenbank nutzt. Datenbankzugriffe sehen im Moment so aus, dass ich für jede Abfrage eine neue Datenbankverbindung öffne und anschließend wieder schließe. Eigentlich eine normale Prozedur:
using (OleDbConnection conn = new OleDbConnection(this.ConnectionString))
{
string cmdText = "DELETE FROM [emails] WHERE [mailid]=?";
using (OleDbCommand cmd = new OleDbCommand(cmdText, conn))
{
cmd.Parameters.Add("mailid", OleDbType.Guid).Value = ID;
cmd.ExecuteNonQuery();
}
}
Und ausreichend für einen einzelnen Zugriff. Meine Anwendung macht aber pro Page Request mindestens fünf Zugriffe; im Schnitt sind es sogar mehr.
In einem Thread auf MyCSharp, in dem es um zu schließende Connections beim Verlassen einer Website ging, wurde ich von Peter auf eine Diskussion auf Google Groups aufmerksam gemacht, in der es darum ging Datenbankverbindungen global zur Verfügung zu stellen. Stefan stellte dabei einen interessanter Ansatz vor:
Er definierte eine Klasse, die für das Öffnen und Schließen der Datenbankverbindung zuständig ist und die offne Verbindung hält. Diese Klasse wird in der Global.asax bei Application_BeginRequest instanziert, die Connection wird geöffnet und anschließend wird das erzeugte Objekt im aktuellen HttpContext gespeichert. Bei Application_EndRequest wird das Objekt aus dem HttpContext ausgelesen und die Verbindung geschlossen. Im Prinzip eine einfache und geniale Lösung. Die Connection kann jetzt überall in der Anwendung aus dem HttpContext geholt und genutzt werden.
Da ich wie erwähnt nach Optimierungsmöglichkeiten suchte, lag es nahe Stefans Vorschlag auszuprobieren, zumal mir langsam die Ideen für weitere Optimierungen beim Datenbankzugriff ausgegangen sind.
Die Klasse, die die Verbindung zur Datenbank hält, sieht bei mir folgendermaßen aus:
public class DB
{
public string ConnectionString { get; set; }
private IDbConnection dbConnection;
public IDbConnection DbConnection
{
get
{
if (dbConnection == null)
dbConnection = new OleDbConnection(ConnectionString);
if (dbConnection.State == ConnectionState.Closed
|| dbConnection.State == ConnectionState.Broken)
dbConnection.Open();
return dbConnection;
}
}
public DB(string ConnectionString)
{
this.ConnectionString = ConnectionString;
}
public void Open()
{
dbConnection = new OleDbConnection(ConnectionString);
}
public void Close()
{
if (dbConnection != null)
{
dbConnection.Close();
dbConnection.Dispose();
}
}
}
Wichtig ist hier die Eigenschaft DbConnection. Diese instanziert die Datenbankverbindung erst wenn sie das erste mal benötigt wird. Falls keine Connection benötigt wird, enthält diese Klasse nichts anderes als einen Connection String. Das ist auch deswegen von Vorteil, da im IIS7 (IMHO nur im Integrated Pipeline Mode) und im Development Webserver des Visual Studio jeder Request (auch Bilder, CSS und JavaScripts) die Event Handler Application_BeginRequest und Application_EndRequest auslöst. Es wird also somit nicht bei jedem Request eine Datenbankverbindung geöffnet.
So sehen die Event Handler in der Global.asax aus:
void Application_BeginRequest(object sender, EventArgs e)
{
DB db = new DB(ConfigurationManager.ConnectionStrings
["DefaultConnection"].ConnectionString);
HttpContext.Current.Items["DB"] = db;
}
void Application_EndRequest(object sender, EventArgs e)
{
DB db = HttpContext.Current.Items["DB"] as DB;
if (db != null)
db.Close();
}
In meine Basisklassen habe ich nun ebenfalls einen neue Eigenschaft eingefügt, welche mir die Datenbankverbindung aus dem HttpContext holt:
private DB db;
public IDbConnection DbConnection
{
get
{
if (db == null)
db = Context.Items["DB"] as DB;
return db.DbConnection;
}
}
Meine Datenbankabfragen kann ich somit nun um ein paar Zeilen erleichtern:
string cmdText = "DELETE FROM [emails] WHERE [mailid]=?";
using (OleDbCommand cmd =
new OleDbCommand(cmdText, this.DbConnection as OleDbConnection))
{
cmd.Parameters.Add("mailid", OleDbType.Guid).Value = ID;
cmd.ExecuteNonQuery();
}
Nachdem ich das nun komplett in meine Anwendung integriert hatte, waren schon alleine beim Durchklicken eine deutliche Verbesserungen der Performance zu spüren.
Peter hatte in dem Thread zusätzlich den Vorschlag gemacht die Methode so weit zu verfeinern, dass nur Page Handler, bzw. Handler dessen Dateiendung "*.aspx" ist. die Klasse für die DB Verbindung instanzieren dürfen. Er geht sogar soweit, die Instanzierung auch für HttpHandler zu erlauben, die ein spezielles Marker Interface implementiert haben.
Das sieht dann so aus, dass erst ein Marker Interface definiert werden muss:
public interface IRequiresDbConnection{}
Die Implementierung des Interfaces:
public partial class myhandler : IHttpHandler, IRequiresDbConnection
{
[...]
}
Da die Handler bei Application_BeginRequest noch null sind, muss die Instanzierung unserer Klasse und die Abfrage auf den Handler, z. B: im Event Handler Application_PreRequestHandlerExecute erfolgen:
void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
IHttpHandler handler = (sender as HttpApplication).Context.CurrentHandler;
if (handler != null && (handler is Page || handler is IRequiresDbConnection))
{
DB db = new DB(ConfigurationManager.ConnectionStrings
["DefaultConnection"].ConnectionString);
HttpContext.Current.Items["DB"] = db;
}
}
Da ich die eigentliche Verbindung nur öffne, wenn sie das erste mal benötigt wird und im Prinzip jedes Webform und jeder Generic Handle allein wegen der Authentifizierung eine Datenbankverbindung benötigt, halte ich es in - meinem Fall - nicht für erforderlich Peters Vorschlag mit dem Marker Interface zu arbeiten.
Wie bereits erwähnt, ist in einer Anwendung mit vielen Datenbankzugriffen eine deutliche Performancesteigerung zu spüren, wenn pro Request nur eine Verbindung zur Datenbank geöffnet wird.
Danke euch beiden (Stefan und Peter) für den wertvollen Tipp :-)