3

I offloaded all ADO hood in a separate Data Module, so a single module can be referred by several applications. All my applications basically need only two worker methonds to access data:

AdoQuery delivers a result set in a form of TADODataSet.
AdoExecute performs simple update/delete queries without fetching any results.

Here is the class structure:

type
  TMyDataModule = class(TDataModule)
    procedure DataModuleCreate(Sender: TObject);
    procedure DataModuleDestroy(Sender: TObject);
  private
    procedure pvtAdoConnect;
    procedure pvtAdoExecute(const sql: string);
    function pvtAdoQuery(const sql: string): TADODataSet;
  public
    AdoConnection: TADOConnection;
  end;

Then I added two publicly exposed wrappers to class methods. I used that to avoid long class references in the calls:

function AdoQuery(const sql: string): TADODataSet;
procedure AdoExecute(const sql: string);

implementation

function AdoQuery(const sql: string): TADODataSet;
begin
  Result := MyDataModule.pvtAdoQuery(sql);
end;

Above are the worker function which I call from within all my forms.

AdoConnect runs only once on DataModuleCreate event. TDatModule derived from TPersistent, which allows to persist the single instance of connection throughout a runtime.

The only thing that annoys me so far - is a useless .DFM which I don't need at all.
Is there any option to get rid of it?

Interface Unknown
  • 713
  • 10
  • 26
  • 6
    Simply stop using data modules and use just a plain class where you create all the components at runtime (or better yet, a class implementing an interface which will be injected into your application classes). The main (and only ?) purpose of data modules was to separate (non-visual) components for design time. If you don't use even this, there's no reason to have a data module. – TLama Jun 24 '15 at 22:07
  • What would be an ancestor of this class? *Where* do I create an instance of that class to make sure the connection is persistent and shared between all forms with multiple active datasets? – Interface Unknown Jun 24 '15 at 22:11
  • It's up to you, but most commonly create it in the `initialization` and free it in `finalization`. – Jerry Dodge Jun 24 '15 at 22:13
  • 1
    Without the design time element, what would be the point of the data module? – David Heffernan Jun 24 '15 at 22:16
  • 1
    About the ancestor, it can be just `class`. Don't be confused by the name of the `TPersistent` class. It's not what it sounds to be. Its ancestor, the `class(TObject)` (or just `class` in short) instances remain *persistent* (exist until you destroy them). – TLama Jun 24 '15 at 22:27
  • Don't use the dfm you don't need, but devise yourself another class which you'll need. I'm a bit confused here. @TLama - What's the difference between using a data module and a class of yours for similar purpose? – Sertac Akyuz Jun 24 '15 at 22:33
  • @Interface - you can use a plain unit that would expose your functions. – Sertac Akyuz Jun 24 '15 at 22:37
  • 1
    @Sertac, you will lose a data module. That's what I feel the OP is about to do. I'm going to drop their usage soon from the last few apps of mine (I can live without live bindings, not speaking about the ugly data aware controls). – TLama Jun 24 '15 at 22:55
  • @TLama, I've just crafted a simple class as per your suggestion. It exposes three described methods. At what point (how/where/when?) to create an instance of this class? This is something I still cannot get. Shall I define a variable of class type in my Main form? How all other forms will reuse the same active connection then? – Interface Unknown Jun 24 '15 at 23:08
  • @TLama - What's wrong with a data module? It's just a component with some design area. – Sertac Akyuz Jun 24 '15 at 23:11
  • Data Modules can still be useful (apart from using live bindings or data) - in Firemonkey, I have 4 data modules, in units `uWindowsDM`, `uOSXDM`, `uiOSDM` and `uAndroidDM` each containing non-visual components only intended for that particular platform. I don't even include the other 3 other platform data modules in the app - just the one for the purpose. However, the only reason to ever need a data module is if you need to have design-time non-visual components dropped - which is actually also useful for Action Managers being used in multiple units. If no design components, no DM is needed. – Jerry Dodge Jun 24 '15 at 23:11
  • 2
    @InterfaceUnknown You should use a singleton pattern. You can either create the class on demand or when the unit is created using the initialization section and destroy it using the finalization section. Probably the easiest is to look at `Vcl.Printers`. There is a `TPrinter` class and a method `function Printer: TPrinter;` that returns the single TPrinter instance. The object is freed right at the end of the unit in the finalization section. Anyone wanting to get the printer just calls the `Printer` function. If you use this class in threading then you will need to do a bit more work. – Graymatter Jun 24 '15 at 23:20
  • @JerryDodge, no. I do not use any design-time components. I declare all ADO vars and then create instances at run-time. Therefore I don't need a DFM. However I just don't get how to correctly instantiate a custom class which resides in a plain unit and incapsulates all the above functionality. I.e. when/where to create it. – Interface Unknown Jun 24 '15 at 23:21
  • You should get out of the habit of declaring global variables and using them from all over. Instead, refer to my answer below to see how to declare just one hidden variable and control it. – Jerry Dodge Jun 24 '15 at 23:32
  • @Sertac, have you experienced a legacy project that had tens of active dataset components on a datamodule with *hardcoded* properties ? Nothing good (could be heard about the original developer at that time :) Yes, a bad design example, but also a lesson to avoid doing the same by myself. If you have your app. framework flexible enough, you'll find that a lot of time spent by design was a wasted time (it's even better to let the users decide what and how they want to see). – TLama Jun 25 '15 at 00:12
  • @TLama I also prefer to completely avoid data modules where possible. The only place where I still see a benefit of using them is for shared image lists. – Graymatter Jun 25 '15 at 00:18
  • @TLama - I wasn't talking about the *intended* usage of a data module, i.e. dropping data access components on it. Everyone is talking about not *needing* a data module, instead, to use a custom class. In that regard I was wondering the disadvantage of having a data module instead of that custom class. You can do anything with the module whatever you can do with that custom class... It's just a component, is it the overhead of being a component instead of a class? – Sertac Akyuz Jun 25 '15 at 22:51

3 Answers3

4

I would handle this type of thing in one of two ways, with interfaces or with inheritance. I prefer not to expose classes to the outside world in these cases. The second one could almost be called an interface without interfaces :)

Interfaces

This version returns an interface that includes the required methods. The outside world only needs to use the interface. We keep the implementation details private. Our TMyDBClass implements the interface that we have exposed to the outside world and our global function GetDBInterface returns the single instance.

interface

uses
  ADODB;

type
  IMyDBInterface = interface
  ['{2D61FC80-B89E-4265-BB3D-93356BD613FA}']
    function AdoQuery(const sql: string): TADODataSet;
    procedure AdoExecute(const sql: string);
  end;

function GetDBInterface: IMyDBInterface;

implementation

type
  TMyDBClass = class(TInterfacedObject, IMyDBInterface)
  strict private
    FConnection: TADOConnection;
  protected
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
  public
    function AdoQuery(const sql: string): TADODataSet;
    procedure AdoExecute(const sql: string);
  end;

var
  FMyDBInterface: IMyDBInterface;

procedure TMyDBClass.AdoExecute(const sql: string);
begin
  // ...
end;

function TMyDBClass.AdoQuery(const sql: string): TADODataSet;
begin
  // ...
end;

procedure TMyDBClass.AfterConstruction;
begin
  inherited;
  FConnection := TADOConnection.Create(nil);
end;

procedure TMyDBClass.BeforeDestruction;
begin
  FConnection.Free;
  inherited;
end;

// Our global function

function GetDBInterface: IMyDBInterface;
begin
  if not Assigned(FMyDBInterface) then
    FMyDBInterface := TMyDBClass.Create;
  Result := FMyDBInterface;
end;

initialization

finalization
  FMyDBInterface := nil;
end.

Inheritance

This version uses a base class that has the required methods. This is a bit easier for people to deal with because it excludes the interface which can be complex to people starting out. Again we hide the implementation details from the user and only expose a shell of a class that includes the two methods we want people to access. The implementation of these methods is performed by a class in the implementation that inherits from the exposed class. We also have a global function that returns the instance of this class. The big advantage that the interface approach has over this approach is that the user of this object can't free the object by accident.

interface

uses
  ADODB;

type
  TMyDBClass = class(TObject)
  public
    function AdoQuery(const sql: string): TADODataSet; virtual; abstract;
    procedure AdoExecute(const sql: string); virtual; abstract;
  end;

function GetDBClass: TMyDBClass;

implementation

type
  TMyDBClassImplementation = class(TMyDBClass)
  strict private
    FConnection: TADOConnection;
  protected
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
  public
    function AdoQuery(const sql: string): TADODataSet; override;
    procedure AdoExecute(const sql: string); override;
  end;

var
  FMyDBClass: TMyDBClassImplementation;

procedure TMyDBClassImplementation.AdoExecute(const sql: string);
begin
  inherited;
  // ...
end;

function TMyDBClassImplementation.AdoQuery(const sql: string): TADODataSet;
begin
  inherited;
  // ...
end;

procedure TMyDBClassImplementation.AfterConstruction;
begin
  inherited;
  FConnection := TADOConnection.Create(nil);
end;

procedure TMyDBClassImplementation.BeforeDestruction;
begin
  FConnection.Free;
  inherited;
end;

// Our global function

function GetDBClass: TMyDBClass;
begin
  if not Assigned(FMyDBClass) then
    FMyDBClass := TMyDBClassImplementation.Create;
  Result := FMyDBClass;
end;

initialization
  FMyDBClass := nil;
finalization
  FMyDBClass.Free;
end.

Usage

Usage of these are really easy.

implementation

uses
  MyDBAccess; // The name of the unit including the code

procedure TMyMainForm.DoSomething;
var
  myDataSet: TADODataSet;
begin
  myDataSet := GetDBInterface.AdoQuery('SELECT * FROM MyTable');
  ...
  // Or, for the class version
  myDataSet := GetDBClass.AdoQuery('SELECT * FROM MyTable');
  ...
end;
Graymatter
  • 6,529
  • 2
  • 30
  • 50
  • Using an interface is indeed a great way of doing things, however I would consider it a sort of luxury in this case. I wouldn't confuse the OP with that - we don't know if they're ready or willing to take on understanding interfaces yet. Just need the basics. And your second option does the trick (although still a bit more than OP really wanted). – Jerry Dodge Jun 24 '15 at 23:56
  • @JerryDodge My biggest gripe with exposing the classes is exactly what the OP did in the question. Accessing the private variables through the Delphi "bug" that makes private variables visible to the whole unit. The same with the old protected hack. If an implementation doesn't need to be exposed then I try and avoid exposing it. This type of thing allows the functionality to change over time without breaking code that has been built on functionality that is supposed to be private. – Graymatter Jun 25 '15 at 00:02
  • Indeed, but it also depends on how that code will be used. If it's one developer building a single program, then who cares? On the other hand, if you're building a library that you expect to be used by thousands of developers, then of course you will want some kind of protection. – Jerry Dodge Jun 25 '15 at 00:04
  • I do enjoy using interfaces to pass "objects" across DLL boundaries. – Jerry Dodge Jun 25 '15 at 00:06
  • @JerryDodge Often code that starts out being built by a single developer grows :) – Graymatter Jun 25 '15 at 00:10
  • Yeah, I built an API wrapper around a card payment device, was originally contained within a DLL (Windows only) and widely using tons of interfaces. Then I needed to make it cross-platform. So the interfaces were completely re-purposed (and `WideString` no longer worked :P ) – Jerry Dodge Jun 25 '15 at 00:23
  • Dear @Graymatter, your post is one of the best and well-structured answers I've ever read. I thank you very much for your efforts. I will definitely mark your post as a answer to my question. This afternoon I dug deep into both inheritance and interfaces and crafted a univestal "data layer" module implementng both approaches. Now I can do conditional compiling like a pro :) – Interface Unknown Jun 25 '15 at 20:59
2

If you don't have any design-time non-visual components dropped onto your data module, and don't plan to ever do so, then you shouldn't need a data module at all. The whole purpose is for design-time components, and other implementations such as a Web Module or even a Windows Service Application. But not for wrapping pure code without design-time components.

Also, as mentioned in the comments, don't be confused about the meaning of TPersistent. This class is used entirely differently, and can be integrated into the IDE Object Inspector (as sub-properties within a component).

So the ideal thing for you to do is encapsulate everything in just a single class. For your purpose, a database connection...

type
  TMyData = class(TObject)
  private
    FConnection: TADOConnection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure pvtAdoConnect;
    procedure pvtAdoExecute(const sql: string);
    function pvtAdoQuery(const sql: string): TADODataSet;
    ...
  end;

implementation

{ TMyData }

constructor TMyData.Create;
begin
  FConnection:= TADOConnection.Create(nil);
end;

destructor TMyData.Destroy;
begin
  FConnection.Connected:= False;
  FConnection.Free;
  inherited;
end;

As for the interpretation of being "persistent", you can create/destroy an instance of it in many ways. For example, you could use a unit's initialization and finalization sections (requiring CoInitialize) or you can have your main form initialize a global instance upon creation.

One common way of doing so is to add...

interface

function MyData: TMyData;

implementation

var
  _MyData: TMyData;

function MyData: TMyData;
begin
  if not Assigned(_MyData) then
    _MyData:= TMyData.Create;
  Result:= _MyData;
end;

initialization
  _MyData:= nil;
finalization
  _MyData.Free;
end.

The first time you call MyData from anywhere, a new global instance will be instantiated. Then, every further time it re-uses the same instance. This also solves the need of ActiveX and CoInitialize etc. because at this point, COM is expected to already be instantiated (which is required for ADO).

Usage of this unit would be extremely simple - use include it in the uses anywhere, and access its instance through the MyData function.

Notes

You should get out of the habit of global variables. This is asking for trouble down the road when trying to do later work. The example above shows how to accommodate for a global object instance. All other variables should be self-contained within that object, or in general one of scope / relevance. The whole control of your TADOConnection should be within here, including connecting/disconnecting, exception handling, assigning the connection string.

Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • I am the most focused on the last two sentences of your answer :) Can you please elaborate a bit more on the instantiation of the class? My main form actively uses database calls. So does almost every other child form... So far I cannot get where to put the initialization code, so the instance of my custom class is accessible from within the child forms. Child forms must not do any setup (i.s. establish a NEW connection). They must just fire AdoQuery cause the hood is already set. – Interface Unknown Jun 24 '15 at 23:38
  • When your app starts, it doesn't create it immediately. It shouldn't, because ADO requires COM, which VCL automatically initializes for you, but only after unit initialization. So rather than initializing COM again, what happens is your class isn't even created yet until the first time you need to use it. For the entire rest of your app, you'll be re-using the same instance. You would access `MyData` in the form of a function the exact same way that you would if it were a global variable. The only difference is that it's a function, so it protects developers from accidentally assigning it. – Jerry Dodge Jun 24 '15 at 23:48
  • All you would have to do is use this unit from any other unit, and access your instance through `MyData` without worrying about whether it's created or not. Don't even try to create or free it from anywhere else - let the unit/function do all the work. – Jerry Dodge Jun 24 '15 at 23:51
  • thank you for you input which actually put me on the right track. I decided to implement an InterfacedObject, but I leaned alot from your post. – Interface Unknown Jun 25 '15 at 21:08
0

In case you might be interested in an alternative without DataModules alltogether, have a look at this: https://github.com/stijnsanders/xxm/blob/master/Delphi/demo2/03%20Data%20ADO/xxmData.pas

Queries are stored in a single .sql file, which is handy to edit it in specific SQL editors or workbenches. Queries are separated with a line with --"QueryName", and loaded in a query-store on start-up. Assuming you query for smaller recordsets most of the time, the best lock and open style is read-only and static, which offers the best possible performance and least load on the database server. Getting field values uses the Collect call which also offers a little performance gain.

Stijn Sanders
  • 35,982
  • 11
  • 45
  • 67