4

A friend of mine asked me how he can at runtime to create a class to 'map' a database table. He is using ADO to connect to the database.

My answer was that he can fill an ADOQuery with a 'select first row from table_name', set the connection to database, open the query, and after that by using a cycle on the ADOQuery.Fields he can get FieldName and FieldType of all the fields from the table. In this way he can have all the fields from the table and their type as members of the class.

There are other solutions to his problem?

RBA
  • 12,337
  • 16
  • 79
  • 126

5 Answers5

6

@RBA, one way is to define the properties of the class you want to map as "published", then use RTTI to cycle through properties and assign the dataset rows to each property.

Example:

TMyClass = class
private
  FName: string;
  FAge: Integer;
published
  property Name: string read FName write FName;
  property Age: Integer read FAge write FAge;
end;

Now, do a query:

myQuery.Sql.Text := 'select * from customers';
myQuery.Open;
while not myQuery.Eof do
begin
  myInstance := TMyClass.create;
  for I := 0 to myQuery.Fields.Count - 1 do
    SetPropValue(myInstance, myQuery.Fields[I].FieldName, myQuery.Fields[I].Value);
  // now add myInstance to a TObjectList, for example
  myObjectList.Add(myInstance);  
  Next;
end;

This simple example only works if all fields returned by the query have an exact match in the class.

A more polished example (up to you) should first get a list of properties in the class, then check if the returned field exists in the class.

Hope this helps, Leonardo.

RBA
  • 12,337
  • 16
  • 79
  • 126
Leonardo M. Ramé
  • 178
  • 1
  • 1
  • 5
  • +1 for the RTTI. Anyway, I'm not interested how he wants to create the list of the classes. I'm interested if it's possible in other manner than FieldName / FieldProperty. – RBA Sep 21 '11 at 15:53
  • Well, apart from RTTI, the only other way I know is to create a list of property<->field relations, but this involves more work from the programmer part. – Leonardo M. Ramé Sep 21 '11 at 17:02
3

Not a real class, but something quite similar. Sometime ago I blogged about a solution that might fit into your needs here. It uses an invokeable custom variant for the field mapping that lets you access the fields like properties of a class.

The Delphi Help can be found here and the two part blog post is here and here. The source code can be found in CodeCentral 25386

Uwe Raabe
  • 45,288
  • 3
  • 82
  • 130
  • Custom variant types are very handy. I implemented the same for our BigTable DB unit and our high-performance DB access (using OleDB, SQLite3 or Oracle), just one month before your blog post (I still have the premice). I found out that late-binding of variant properties was very time-consuming, so I made some hack in order to speed it a lot for Delphi 6 up to XE (sadly, it's not working with XE2 yet because they changed the internal implementation and I don't have the RTL source code because it costs a lot of money). See http://blog.synopse.info/post/2011/07/01/Faster-variant-late-binding – Arnaud Bouchez Sep 23 '11 at 14:10
2

This is what is called ORM. That is, Object-relational mapping. You have several ORM frameworks available for Delphi. See for instance this SO question.

Of course, don't forget to look at our little mORMot for Delphi 6 up to XE2 - it is able to connect to any database using directly OleDB (without the ADO layer) or other providers. There is a lot of documentation available (more than 600 pages), including general design and architecture aspects.

For example, with mORMot, a database Baby Table is defined in Delphi code as:

/// some enumeration
// - will be written as 'Female' or 'Male' in our UI Grid
// - will be stored as its ordinal value, i.e. 0 for sFemale, 1 for sMale
TSex = (sFemale, sMale);

/// table used for the Babies queries TSQLBaby = class(TSQLRecord)   private     fName: RawUTF8;     fAddress: RawUTF8;     fBirthDate: TDateTime;     fSex: TSex;   published     property Name: RawUTF8 read fName write fName;     property Address: RawUTF8 read fAddress write fAddress;     property BirthDate: TDateTime read fBirthDate write fBirthDate;     property Sex: TSex read fSex write fSex; end;

By adding this TSQLBaby class to a TSQLModel instance, common for both Client and Server, the corresponding Baby table is created by the Framework in the database engine. Then the objects are available on both client and server side, via a RESTful link (over HTTP, using JSON for transmission). All SQL work ('CREATE TABLE ...') is done by the framework. Just code in Pascal, and all is done for you. Even the needed indexes will be created by the ORM. And you won't miss any ' or ; in your SQL query any more.

My advice is not to start writing your own ORM from scratch.

If you just want to map some DB tables with objects, you can do it easily. But the more time you'll spend on it, the more complex your solution will become, and you'll definitively reinvent the wheel! So for a small application, this is a good idea. For an application which may grow in the future, consider using an existing (and still maintained) ORM.

Community
  • 1
  • 1
Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
2

Code generation tools such as those used in O/RM solutions can build the classes for you (these are called many things, but I call them Models).

It's not entirely clear what you need (having read your comments as well), but you can use these tools to build whatever it is, not just models. You can build classes that contain lists of field / property associations, or database schema flags, such as "Field X <--> Primary Key Flag", etc.

There are some out there already, but if you want to build an entire O/RM yourself, you can (I did). But that is a much bigger question :) It generally involves adding the generation of code which knows how to query, insert, delete and update your models in the database (called CRUD methods). It's not hard to do, but then you take away your ability to integrate with Delphi's data controls and you'll have to work out a solution for that. Although you don't have to generate CRUD methods, the CRUD support is needed to fully eliminate the need for manual changes to adapt to database schema changes later on.

One of your comments indicated you want to do some schema querying without using the database connection. Is that right? I do this in my models by decorating them with attributes that I can query at runtime. This requires Delphi 2010 and its new RTTI. For example:

[TPrimaryKey]
[TField('EmployeeID', TFieldType.Integer)]
property EmployeeID: integer read GetEmployeeID write SetEmployeeID;

Using RTTI, I can take an instance of a model and ask which field represents the primary key by looking for the one that has the TPrimaryKeyAttribute attribute. Using the TField attribute above provides a link between the property and a database field where they do not have to have the same name. It could even provide a conversion class as a parameter, so that they need not have the same type. There are many possibilities.

I use MyGeneration and write my own templates for this. It's easy and opens up a whole world of possibilities for you, even outside of O/RM.

MyGeneration (free code generation tool) http://www.mygenerationsoftware.com/ http://sourceforge.net/projects/mygeneration/

MyGeneration tutorial (my blog) http://interactiveasp.net/blogs/spgilmore/archive/2009/12/03/getting-started-with-mygeneration-a-primer-and-tutorial.aspx

I've taken about 15 mins to write a MyGeneration script that does what you want it to. You'll have to define your Delphi types for the database you're using in the XML, but this script will do the rest. I haven't tested it, and it will probably want to expand it, but it will give you an idea of what you're up against.

<%# reference assembly = "System.Text"%><%

public class GeneratedTemplate : DotNetScriptTemplate
{
    public GeneratedTemplate(ZeusContext context) : base(context) {}

    private string Tab()
    {
        return Tab(1);
    }

    private string Tab(int tabCount)
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        for (int j = 0; j < 1; j++)
            sb.Append("  ");    // Two spaces
        return sb.ToString();
    }

    //---------------------------------------------------
    // Render() is where you want to write your logic    
    //---------------------------------------------------
    public override void Render()
    {
        IDatabase db = MyMeta.Databases[0];

%>unit ModelsUnit;

interface

uses
  SysUtils;

type
<%
        foreach (ITable table in db.Tables)
        {
%>
<%=Tab()%>T<%=table.Name%>Model = class(TObject)
<%=Tab()%>protected
<%          foreach (IColumn col in table.Columns)
            {
%><%=Tab()%><%=Tab()%>f<%=col.Name%>: <%=col.LanguageType%>;
<%          }%>
<%=Tab()%>public
<%          foreach (IColumn col in table.Columns)
            {
%><%=Tab()%><%=Tab()%>property <%=col.Name%>: <%=col.LanguageType%> read f<%=col.Name%> write f<%=col.Name%>;
<%          }%>
<%=Tab()%><%=Tab()%>
<%=Tab()%>end;<%
        }
%>

implementation

end.

<%
    }

}
%>

Here is one of the table classes that was generated by the script above:

TLOCATIONModel = class(TObject)
  protected
    fLOCATIONID: integer;
    fCITY: string;
    fPROVINCE: string;

  public
    property LOCATIONID: integer read fLOCATIONID write fLOCATIONID;
    property CITY: string read fCITY write fCITY;
    property PROVINCE: string read fPROVINCE write fPROVINCE;


  end;
Phil Gilmore
  • 1,286
  • 8
  • 15
1

Depending on the database, you could query the INFORMATION_SCHEMA tables/views for what you need. I've done this in an architecture I created and still use in DB applications. When first connecting to a database it queries "data dictionary" type information and stores it for use by the application.

Jerry Gagnon
  • 1,131
  • 8
  • 16