Metadata.NET - by Noel Rice

By: Lino Tadros

Abstract: This article will show the capability of requesting information from .NET providers at runtime and deal with Databases, Table, Fields, constraints, Index and data Types.
MetaData - Noel

It is with great pleasure to have Noel Rice, Senior Architect and a Delphi veteran, be the first guest author on my "Caught in the .NET" column.  This article is extremely important to understand the advanced capabilities of the .NET providers.  Enjoy!
Lino Tadros

To create flexible open-ended database applications you need to know whats available at runtime. The examples below written in Delphi for .NET demonstrate retrieving information about a database server and navigating a result set of that information. . NET provides a consistent way to get all significant information about database servers that have ADO providers. A sampling of whats available:

  • Databases (Catalogs)
  • Tables
  • Columns
  • Keys & Indexes
  • Permissions
  • Constraints
  • Data types

You get all this for free with the OleDBConnection component from System.Data.OleDb.

Note: For typical access to an MSSQL server you would use SQLConnection for best performance, but here we want to access any database that has an ADO provider. The methods we use to retrieve metadata (data describing the databases were interested in, such as table and field structure) require OleDBConnection.

Say you want all tables in the Northwind database. The results are returned as a DataTable and can be hooked up directly to a DataGrid component:

The code to get it done is deceptively brief:


procedure TfrmMain.btnLoadGrid_Click(Sender: TObject; E: EventArgs);
var
  tblDatabases: System.Data.DataTable;
begin
  Connection.ChangeDatabase('Northwind');
  tblDatabases := Connection.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, nil);
  DBGrid.DataSource := tblDatabases;
end;

The call to ChangeDatabase makes Northwind the current database. GetOleDbSchemaTable next retrieves all tables for the current database. The first parameter, OleDbSchemaGuid, determines what kind of metadata youre looking for. There are over 30 possibilities listed in the .NET Framework SDK help. In our examples here we use Catalog (Micro-Speak for Database), Table, and Column. The second parameter lists restrictions to filter that list. Well talk more about restricting the list in a moment but for now pass nil and return all table entries.

Finally the DataTable returned is hooked up to the DataGrid DataSource allowing the schema to be shown.

For an example we can create a simple tree view UI that displays databases, tables and fields for a given connection.

The code that loads the top-level list of databases demonstrates how to navigate the returned DataTable in code:


procedure TfrmMain.btnLoadTree_Click(Sender: TObject; E: EventArgs);
var
  I: integer;
  NodeDatabases: TreeNode;
  tblDatabases: System.Data.DataTable;
  Row: System.Data.DataRow;
  Col: System.Data.DataColumn;
  SDBName: string;
begin
  TV.Nodes.Clear;
  NodeRoot := TV.Nodes.Add('Databases');
  tblDatabases := Connection.GetOleDbSchemaTable(OleDbSchemaGuid.Catalogs, nil);
  for I := 0 to tblDatabases.Rows.Count - 1 do
  begin
    Row := tblDatabases.Rows[I];
    Col := tblDatabases.Columns['CATALOG_NAME'];
    SDBName := Row[Col, DataRowVersion.Current].ToString();
    NodeDatabases := NodeRoot.Nodes.Add(SDBName);
...

DataTable has DataRow and DataColumn properties used to iterate the list. For every row get a DataRow object by ordinal value, and DataColumn using the column name. To extract the actual data use the DataColumn property as an index into the row, along with the DataRowVersion member Current, and finally use ToString() to get the text from the row item object.

Note: DataRowVersion is an enumeration specifying the state of the data as it changes before and after editing. For example, you could look at DataRowVersion.Proposed rows of data before accepting changes made by user editing. DataRowVersion Members are: Current, Default, Original and Proposed.

Getting a list of tables from a given database is slightly more involved because the list needs to be restricted. GetOleDbSchemaTable brings back all tables (including system tables) and we want user tables only. A parallel example in C# looks like this:


DataTable schemaTable = conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
new object[] {null, null, null, "TABLE"});

The second parameter for GetOleDbSchemaTable uses an array of objects that map to columns in the returned DataTable. If null was passed instead of new object[], all tables, including system tables would be returned. Take a look back at the earlier screen print showing tables listed in a DataGrid where the fourth column is TABLE_TYPE and the values are TABLE and SYSTEM_TABLE. By passing the array of restrictions criteria we allow the first three columns to have any values and but the last column must match the value TABLE. The .NET Framework SDK help lists for each OleDbSchemaGuid, column names that can be used to restrict the results. To get the same effect in Delphi for .NET code, use an array of System.Object:


procedure TfrmMain.btnLoadTree_Click(Sender: TObject; E: EventArgs);
var
  T: integer;
  NodeTables: TreeNode;
  tblTables: System.Data.DataTable;
  Row: System.Data.DataRow;
  Col: System.Data.DataColumn;
  STableName: string;
  ArrFilter: array[0..3] of System.Object;
begin
  .
  .
  .
  Connection.ChangeDatabase(SDBName);
  ArrFilter[0] := nil;
  ArrFilter[1] := nil;
  ArrFilter[2] := nil;
  ArrFilter[3] := 'TABLE'; // only user tables
  tblTables := Connection.GetOleDBSchemaTable(OleDbSchemaGuid.Tables, ArrFilter);
  .
  .
  .

Heres the code for the Load Tree button altogether including column level metadata:


procedure TfrmMain.btnLoadTree_Click(Sender: TObject; E: EventArgs);
var
  I, T, F: integer;
  NodeDatabases, NodeTables, NodeRoot: TreeNode;
  tblDatabases, tblTables, tblFields: System.Data.DataTable;
  Row: System.Data.DataRow;
  Col: System.Data.DataColumn;
  SDBName, STableName, SFieldName: string;
  ArrFilter: array[0..3] of System.Object;
begin
  TV.Nodes.Clear;
  NodeRoot := TV.Nodes.Add('Databases');
  tblDatabases := Connection.GetOleDbSchemaTable(OleDbSchemaGuid.Catalogs, nil);
  for I := 0 to tblDatabases.Rows.Count - 1 do
  begin
    Row := tblDatabases.Rows[I];
    Col := tblDatabases.Columns['CATALOG_NAME'];
    SDBName := Row[Col, DataRowVersion.Current].ToString();
    NodeDatabases := NodeRoot.Nodes.Add(SDBName);
     
    Connection.ChangeDatabase(SDBName);
    ArrFilter[0] := nil;
    ArrFilter[1] := nil;
    ArrFilter[2] := nil;
    ArrFilter[3] := 'TABLE'; // only user tables
    tblTables := Connection.GetOleDBSchemaTable(OleDbSchemaGuid.Tables, ArrFilter);
     
    for T := 0 to tblTables.Rows.Count - 1 do
    begin
      Row := tblTables.Rows[T];
      Col := tblTables.Columns['TABLE_NAME'];
      STableName := Row[Col, DataRowVersion.Current].ToString();
      NodeTables := NodeDatabases.Nodes.Add(STableName);
       
      ArrFilter[0] := SDBName;
      ArrFilter[1] := nil;
      ArrFilter[2] := STableName;
      ArrFilter[3] := nil;
      tblFields := Connection.GetOleDBSchemaTable(OleDbSchemaGuid.Columns, ArrFilter);
       
      for F := 0 to tblFields.Rows.Count - 1 do
      begin
        Row := tblFields.Rows[F];
        Col := tblFields.Columns['COLUMN_NAME'];
        SFieldName := Row[Col, DataRowVersion.Current].ToString();
        NodeTables.Nodes.Add(SFieldName);
      end;

    end;
  end;
end;

Now that you can get databases, tables and columns, what if you want to get more information on the column itself such as data type, size or precision? Column data happens to include a DATA_TYPE identifier, but doesnt actually tell you what the data type is. Where is this information hiding? Call GetOleDBSchemaTable again and pass PROVIDER_TYPES for the OleDBSchemaGuid and you get back a nicely normalized list of all supported types for the provider. (In a future article we can talk about using a DataView object to lookup into the provider types table). The returned DataTable looks like this:

One last piece of housekeeping  connecting to the server. Pass an ADO style connection string to the OleDbConnection component constructor (You will need to change the connection string SConnect constant in the example below to fit your environment). This form of the constructor will automatically open the connection. When youre done using the connection, close it (closing doesnt occur automatically by going out of scope).


constructor TfrmMain.Create;
const
  SConnect = 'Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security ' +
    'Info=False;Use Procedure for Prepare=1;Auto Translate=True;Packet ' +
    'Size=4096;Workstation ID=NR1;Use Encryption for Data=False;Tag with ' +
    'column collation when possible=False';
begin
  inherited Create;
  Initialize();
  Connection := OleDbConnection.Create(SConnect);
  Connection.Open();
end;
 
destructor TfrmMain.Destroy;
begin
  Connection.Close();
end;

I especially like this one size fits all approach to querying server data structure regardless of database type. If an ADO provider exists for the server, you can find out everything you need to know and then some.

Thank you for your time and write to you later.

About Falafel Software Inc:

Falafel Software is all about making the most of software development technology in order to complete the project on time and on budget with best possible user experience. Falafel Software offers a comprehensive suite of software development solutions ranging from strategy to design to implementation that businesses need in order to realize high returns on their investment.

Falafel Logo

Copyright ) 2003 Noel Rice
ALL RIGHTS RESERVED. NO PART OF THIS DOCUMENT CAN BE COPIED IN ANY FORM WITHOUT THE EXPRESS, WRITTEN CONSENT OF THE AUTHOR.


Server Response from: SC3

 
Copyright© 1994 - 2008 Embarcadero Technologies, Inc. All rights reserved. Contact Us   Site Map   Legal Notices   Privacy Policy   Report Software Piracy