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 Oracle OleDb.NET Oracle OleDb.NET Ms Oracle CODBC Ms SQLServer OleDb.NET Ms SQLServer
  Sätze Beq. Opt. Trans. Beq. Opt. Trans. Beq. Opt. Trans. Beq. Opt. Trans. Beq. Opt. Trans.
Insert  500 6.394 6.350 793 8.032 - 2.049 8.488 - 2.620 4.075 3.911 444 4.062 - 894
  1.000 13.825 12.373 2.609 18.529 - 5.083 17.341 - 6.122 8.428 8.289 781 8.368 - 1.725
  2.000 29.004 25.475 5.139 32.770 - 10.174 34.539 - 12.611 16.818 15.730 1.514 16.290 - 3.401
Update  500 7.435 5.724 700 8.021 - 3.151 9.119 - 3.284 4.296 3.864 421 4.319 - 1.238
  1.000 16.593 11.420 3.023 15.198 - 4.713 18.229 - 6.402 8.587 8.371 876 9.109 - 1.845
  2.000 32.244 22.927 5.016 33.544 - 9.757 32.853 - 12.518 17.732 16.564 1.669 18.189 - 3.565
Delete  500 6.937 675 276 7.714 - 1.588 8.168 - 2.289 3.908 657 324 4.683 - 667
  1.000 12.746 1.211 548 20.923 - 3.822 16.286 - 5.220 7.857 1.381 562 8.809 - 1.225
  2.000 24.980 2.320 1.113 32.429 - 6.933 32.813 - 9.794 14.640 1.589 1.125 16.577 - 3.051
Select  1.000 83 63 63 280 223 240 156 103 113 54 33 33 106 86 96
  5.000 372 279 274 1.385 1.078 1.108 747 453 474 266 158 159 510 407 420
  10.000 738 537 538 2.757 2.169 2.173 1.475 911 931 531 316 322 1.018 811 824
Connect 904 - - 0 - - 0 - - 28 - - 0 - -
Memory Usage 9.652kB 20.480kB 19.668kB 4.552kB 17.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: