Evaluierung der .NET Datenbank Mechanismen mit Oracle

oder

Ein Anwendungsvergleich von C++ mit CODBC und C# mit OleDb unter Oracle

Stand: 09.11.2001, Beta 2, Autor: Christian Rodemeyer


Überblick


DB-Paradigmen in der ZW 

Warum ist der DB Zugriff in der ZW so wie er ist? Ein Rückblick:


Forderungen (anno '97)


 Entscheidungen 


CODBC Klassendiagramm 



Wie gehts mit .NET?

Wie können die Forderungen der ZW mit .NET gelöst werden?

Mögliche Lösungen

OleDb als natürlichste Lösung

Der OleDb Data Provider erscheint als der natürlichste Ansatz, da er


System.Data.OleDb Klassendiagramm


Evaluierungsmethodik


CODBC Demo

CODBC Demo starten


OleDb.NET Demo

OleDb.NET Demo starten


Vergleich typischer Anwendungsfälle


Import

#include <CODBC.h>
using System.Data.OleDb;

Verbinden

COConnection* m_pDB; // Deklaration

// Connect
m_pDB = new COConnection(m_dsn, m_usr, m_pwd, m_qal);

// Disconnect
delete m_pDB;  
m_pDB = NULL;  
private OleDbConnection m_db; // Deklaration

[...]
// Connect
m_db = new OleDbConnection();    
m_db.ConnectionString = "Provider="    + m_provider.Text   + ";"
                      + "Data Source=" + m_dataSource.Text + ";"
                      + "User ID="     + m_usr.Text        + ";"
                      + "Password="    + m_pwd.Text        + ";";
m_db.Open();

// Disconnect
m_db.Close();

SQL Befehle ausführen

COStatement stmt(m_pDB);
stmt.Execute("create index #DemoTableIndex on #DemoTable(StringIndexed, IntIndexed)");

// oder on the fly
COStatement(m_pDB).Execute("drop table #DemoTable");
OleDbCommand cmd = new OleDbCommand();
cmd.Connection = m_db;
cmd.CommandText = "create index DemoTableIndex on DemoTable(StringIndexed, IntIndexed)";
cmd.Dispose();

// on the fly ist *nicht* möglich, da Dispose() niemals aufgerufen wird!
// führt zu einem Resourcenleck in der Datenbank
new OleDbCommand("drop table DemoTable", m_db).ExecuteNonQuery(); 

SQL Befehle zusammenbauen

Die wohl häufigste DB-Aufgabe des Programmierer besteht im Zusammenbauen eines SQL-Befehls. CODBC bietet mit der COBuilder Klasse ein Möglichkeit, SQL-Befehle übersichtlich zu formatieren und C++ Datentypen bequem in SQL/ODBC Datentypen umzuwandeln:

CExTime t = CExTime::GetCurrentTime();
CString s = "D'oh";

COBuilder SQL;
SQL << "select Id, StringValue, BoolValue "
       "from   #DemoTable "
       "where  TimeValue < " << t << " and "
              "StringIndexed = " << COEscape(s);
SQL enthält nun:
select Id, StringValue, BoolValue
from   #DemoTable
where  TimeValue < {ts '2001-11-09 14:52:12'} and
StringIndexed = 'D''oh'

.NET bietet keine besondere Möglichkeit zum Zusammenbauen von SQL Befehlen. Man ist auf die Bordmittel des Frameworks angewiesen, was recht mühsam sein kann.

DateTime t = DateTime.Now;
string   s = "D''oh"; // Fürs Escapen ist ein Automatismus dringend erforderlich

string SQL;
SQL = "select Id, StringValue, BoolValue "
    + "from   DemoTable "  
    + "where  TimeValue < {ts '" + t.ToString("s").Replace('T', ' ') + "'} and "                             
    +        "StringIndexed = '" + s + "'";

// Alternative: t.ToString("yyyy'-'MM'-'dd HH':'mm':'ss")
// Achtung: t.ToString("yyyy-MM-dd HH:mm:ss") wäre falsch, weil ':' durch 
// lokalisierten Uhrzeittrenner ersetzt wird. '-' funktioniert nur durch Zufall,
// weil '-' (noch) kein Format Character ist

Selects

LPCSTR SQL = "select Id, TimeValue, StringValue, BoolValue, IntValue from #DemoTable";

// Die bequeme Variante
for (COAdhocQuery q(m_pDB, SQL; q; ++q); // Über alle Zeilen iterieren
{
  // Zugriff auf die Spaltenwerte
  int     gIntValue    = q["Id"         ].AsInt();
  CExTime gTimeValue   = q["TimeValue"  ].AsExTime();
  CString gStringValue = q["StringValue"].AsString();
bool    gBoolValue   = q["BoolValue"  ].AsBool();

if (g["IntValue"].IsNull()) ++nNulls; // NULL Values abprüfen
}
// Die performance orientierte Lösung (fast doppelt so schnell)
struct RDemoTable // eine Struktur muss definiert werden
{
  COInt         id;
  COString<128> stringValue;
  COTimestamp   timeValue;
  COBool        boolValue;
  COInt         intValue;
};

void CDemoDlg::OnSelectQuery()
{
for (COQuery<RDemoTable> q(m_pDB, SQL); q; ++q) // for each row
{
int     gIntValue    = q->id;
    CExTime gTimeValue   = q->timeValue;
    CString gStringValue = q->stringValue;
bool    gBoolValue   = q->boolValue;

if (q->intValue.IsNull()) ++nNulls;
  }
}
string SQL = "select Id, TimeValue, StringValue, BoolValue, IntValue from DemoTable";

m_cmd.CommandText = SQL;
OleDbDataReader reader = m_cmd.ExecuteReader();

while (reader.Read()) // for each row
{
int      iValue  = Convert.ToInt32   (reader["Id"]);    
  DateTime dtValue = Convert.ToDateTime(reader["TimeValue"]);
string   sValue  = Convert.ToString  (reader["StringValue"]);
bool     bValue  = Convert.ToBoolean (reader["BoolValue"]);

if (reader["IntValue"] == DBNull.Value) ++ nNulls;

// typisierter Zugriff ist nur über Spaltennummern möglich!
  // hoffentlich ändert sich das noch in der Release Version
  iValue = reader.GetInt32(4);
  sValue = reader.GetString(2);
}
reader.Close(); // wichtig, da kein out of scope destruktor in C#

Einfache Selects

Häufig möchte man nur kurze SQL Befehle wie ein "select count(*)" absetzen. Dafür gibt es in CODBC und .NET Abkürzungen, GetSimpleQuery() und ExecuteScalar()

int min = COStatement(m_pDB).GetSimpleQueryInt("select min(Id) from #DemoTable");
m_cmd.CommandText = "select min(Id) from DemoTable";
object minId = m_cmd.ExecuteScalar();
return (minId == DBNull.Value) ? 0: Convert.ToInt32(minId);

Insert, Update und Delete

COBuilder SQL;
COStatement stmt(m_pDB);
for (int i = m_nedInsertRows; i--;)
{
  SQL.Reset(); // Befehl zusammenbauen
SQL << "insert into #DemoTable(Id, StringValue, TimeValue, IntValue, BoolValue, StringIndexed, IntIndexed)"
      << "values ("
      << ++id << ", ";
  if (GetRandomBool(1)) SQL << "NULL, ";
  else                  SQL << COEscape(GetRandomString(128)) << ", ";
  if (GetRandomBool(1)) SQL << "NULL, ";
  else                  SQL << GetRandomTime() << ", ";
  if (GetRandomBool(1)) SQL << "NULL";
  else                  SQL << GetRandomInt();
SQL << ", " << (GetRandomBool() ? 1 : 0)
      << ", " << COEscape(GetRandomString(64))
      << ", " << GetRandomInt()
      << ")";
  stmt.Execute(SQL); // Befehl absenden und ausführen
}
for (int i = 0; i < numRows; ++i)
{
string SQL = "insert into DemoTable(Id, StringValue, TimeValue, IntValue, BoolValue, StringIndexed, IntIndexed)"
             + "values (" + (++id).ToString() + ", ";
if (GetRandomBool(1)) SQL += "NULL, ";
else SQL += GetRandomString(128) + ", ";
if (GetRandomBool(1)) SQL += "NULL, ";
else SQL += GetRandomTime() + ", ";
if (GetRandomBool(1)) SQL += "NULL";
else SQL += GetRandomInt();
  SQL += ", " + (GetRandomBool() ? '1' : '0')
      + ", " + GetRandomString(64)
      + ", " + GetRandomInt()
      + ")";
  m_cmd.CommandText = SQL;
  m_cmd.ExecuteNonQuery(); // Befehl absenden und ausführen
}

Parametrierbare SQL Befehle

static LPCSTR SQL_InsertDemoTable =
  "insert into #DemoTable "
  "(Id, StringValue, TimeValue, IntValue, BoolValue, StringIndexed, IntIndexed) "
  "values (?, ?, ?, ?, ?, ?, ?) ";

// struct mit COObject - Attributen definieren, welches reihenenfolgemäßig
// zu den Fragezeichen im SQL Befehl passt.
// Das erste Attribut matcht das erste Fragezeichen, usw.
struct RDemoTable
{
  COInt         id;
  COString<128> stringValue;
  COTimestamp   timeValue;
  COInt         intValue;
  COBool        boolValue;
  COString<64>  stringIndexed;
  COInt         intIndexed;
};

void CDemoDlg::OnInsertAction()
{
  COAction inserter(m_pDB, SQL_InsertDemoTable);

// Alle Attribute/Parameter setzen
inserter->id            = GetNextId();
  inserter->stringValue   . SetNull();       // NULL
inserter->timeValue     = GetRandomTime(); // CExTime oder COleDateTime
inserter->intValue      = GetRandomInt();
  inserter->boolValue     = GetRandomBool();
  inserter->stringIndexed = GetRandomString(64); // kein Escapen notwendig
inserter->intIndexed    = GetRandomInt();

  inserter.Execute(); // Befehl ausführen
}

NULL Werte

CODBC

if (q->intValue.IsNull()) {...}
if (q["IntValue"].IsNull() {...}
updater->intValue.SetNull();

.NET

if (reader["IntValue"] == DBNull.Value) {...}

Fehlerbehandlung

try
{
  COStatement stmt(pDB);
  stmt.Execute("Syntax error ...");
}
catch (COError* pErr) 
{
  pErr->ReportError(); // Standard MFC Fehlerbehandlung
pErr->Delete();
}

try
{
  m_cmd.CommandText = "Syntax error ...";
  m_cmd.ExecuteNonQuery();
}
catch (OleDbException err) 
{
  MessageBox.Show(this, err.ToString(), "OleDb.NET Demo");
}


Transaktionen

m_pDB->SetAutoCommit(false);
try
{
  COStatement stmt(m_pDB);
  stmt.Execute(...);
  stmt.Execute(...);
  m_pDB->Commit(); // <-- Commit
}
catch (COError* pErr)
{
  m_pDB->Rollback(); // <-- Rollback
ReportError(pErr);
}
OleDbTransaction trans = m_db.BeginTransaction(); // OleDbConnection ist Wurzel aller Transaktionen
OleDbCommand cmd = new OleDbCommand(); 
cmd.Connection = m_db;
cmd.Transaction = trans; // Explizite Angabe des Transaktionsobjekts notwendig
try
{
 cmd.ExecuteNonQuery();
 [...]
 cmd.ExecuteNonQuery();
 trans.Commit();
}
catch (OleDbException err)
{
  trans.Rollback();
  [...]
}
cmd.Dispose(); // Nicht vergessen, sonst gibt's ein Resourcenleck

Performance Vergleich

Beide Programme liefen auf dem Visual-Studio-7 Testrechner (400MHz, 256MB Speicher) welcher über einen 100MBit Anschluss mit einem Oracle 8.0.5 auf dem S4 verbunden war. Um zufällige Messstörungen zu verringern, wurde jede Aktion vier mal wiederholt und der Mittelwert der letzten drei Messungen eingetragen. Gemessen wurde jeweils die bequeme Variante (Beq) und wenn verfügbar eine performance optimierte Variante (Opt). Die schnellste Variante wurde dann in einer Transaktion ausgeführt (Trans). Alle Zeitangaben in Millisekunden.

Laufzeit
Messung CODBC Ms OracleOleDb.NET OracleOleDb.NET Ms OracleCODBC Ms SQLServerOleDb.NET Ms SQLServer
 SätzeBeq.Opt.Trans.Beq.Opt.Trans.Beq.Opt.Trans.Beq.Opt.Trans.Beq.Opt.Trans.
Insert 5006.3946.3507938.032-2.0498.488-2.6204.0753.9114444.062-894
 1.00013.82512.3732.60918.529-5.08317.341-6.1228.4288.2897818.368-1.725
 2.00029.00425.4755.13932.770-10.17434.539-12.61116.81815.7301.51416.290-3.401
Update 5007.4355.7247008.021-3.1519.119-3.2844.2963.8644214.319-1.238
 1.00016.59311.4203.02315.198-4.71318.229-6.4028.5878.3718769.109-1.845
 2.00032.24422.9275.01633.544-9.75732.853-12.51817.73216.5641.66918.189-3.565
Delete 5006.9376752767.714-1.5888.168-2.2893.9086573244.683-667
 1.00012.7461.21154820.923-3.82216.286-5.2207.8571.3815628.809-1.225
 2.00024.9802.3201.11332.429-6.93332.813-9.79414.6401.5891.12516.577-3.051
Select 1.0008363632802232401561031135433331068696
 5.0003722792741.3851.0781.108747453474266158159510407420
 10.0007385375382.7572.1692.1731.4759119315313163221.018811824
Connect904--0--0--28--0--
Memory Usage9.652kB20.480kB19.668kB4.552kB17.156kB

OraOleDB.Oracle, msdaora, sqloledb


Fazit

Fazit: Bereits die Beta2 Version des VisualStudio.NET ist für Oracle DB Applikationen voll entwicklungstauglich, falls man Besitzer eines schnellen Rechners (>500MHz) ist. 

Subjektive Anmerkung: Die Entwicklung der C# Demo ging deutlich flotter voran, als die C++ Demo. Vor allem die vielfältige Intellisense Unterstützung in jeder Situation, der geniale ObjectBrowser und die endlich durchgängig funktionierender ClassView sparen viel Zeit (z.B. das Nachschlagen von Funktionsdefinitionen in der Hilfe). Auch die Kompilationszeiten sind bei C# geringer (keine 20MB Precompiled Headers :-). 


Anhang/Quellen

Die Quellen und ausführbare Exe's der beiden Demoprogramme gibt es hier: