【问题标题】:Is it possible to using Data Module without .DFM?是否可以在没有 .DFM 的情况下使用数据模块?
【发布时间】:2015-06-24 21:58:58
【问题描述】:

我在一个单独的数据模块中卸载了所有 ADO 引擎,因此一个模块可以被多个应用程序引用。我所有的应用程序基本上只需要两个 worker 方法来访问数据:

AdoQueryTADODataSet 的形式提供结果集。
AdoExecute 执行简单的更新/删除查询,但不获取任何结果。

这是类结构:

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;

然后我向类方法添加了两个公开的包装器。我用它来避免调用中的长类引用:

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

implementation

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

上面是我在所有表单中调用的 worker 函数。

AdoConnectDataModuleCreate 事件中只运行一次。 TDatModule 派生自 TPersistent,它允许在整个运行时保持单个连接实例。

到目前为止,唯一让我烦恼的是一个无用的 .DFM,我根本不需要它。
有什么办法可以摆脱它吗?

【问题讨论】:

  • 停止使用数据模块,只使用一个普通的类,在运行时创建所有组件(或者更好的是,一个实现接口的类,该接口将被注入到您的应用程序类中)。数据模块的主要(也是唯一?)目的是在设计时分离(非可视)组件。如果你连这个都不用,就没有理由拥有数据模块。
  • 没有设计时间元素,数据模块的意义何在?
  • 关于祖先,可以只是class。不要被TPersistent 类的名称所迷惑。这不是它听起来的样子。它的祖先,class(TObject)(或简称为class)实例保持持久(存在直到你销毁它们)。
  • @Sertac,您将丢失一个数据模块。这就是我觉得OP即将做的事情。我将很快从我的最后几个应用程序中停止使用它们(我可以没有实时绑定,而不是谈论丑陋的数据感知控件)。
  • @InterfaceUnknown 您应该使用单例模式。您可以按需创建类,也可以在使用初始化部分创建单元时创建类,然后使用终结部分将其销毁。可能最简单的方法是查看Vcl.Printers。有一个 TPrinter 类和一个方法 function Printer: TPrinter; 返回单个 TPrinter 实例。该对象在完成部分的单元末尾被释放。任何想要获得打印机的人只需调用Printer 函数。如果你在线程中使用这个类,那么你需要做更多的工作。

标签: delphi datamodule dfm


【解决方案1】:

我会通过接口或继承两种方式之一来处理这种类型的事情。在这些情况下,我宁愿不向外界公开课程。第二个几乎可以称为没有接口的接口:)

接口

此版本返回一个包含所需方法的接口。外界只需要使用接口即可。我们将实施细节保密。我们的 TMyDBClass 实现了我们已经暴露给外界的接口,我们的全局函数GetDBInterface 返回单个实例。

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.

继承

此版本使用具有所需方法的基类。这对人们来说更容易处理,因为它排除了对刚开始的人来说可能很复杂的界面。我们再次向用户隐藏实现细节,只公开一个类的外壳,其中包括我们希望人们访问的两种方法。这些方法的实现由继承自公开类的实现中的类执行。我们还有一个返回此类实例的全局函数。与此方法相比,接口方法的最大优势是该对象的用户不会意外释放该对象。

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.

用法

使用这些真的很容易。

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;

【讨论】:

  • 使用接口确实是一种很好的做事方式,但是在这种情况下我会认为它是一种奢侈。我不会将 OP 与此混淆 - 我们不知道他们是否准备好或愿意接受理解接口。只需要基础知识。而您的第二个选项可以解决问题(尽管仍然比 OP 真正想要的要多一点)。
  • @JerryDodge 我对暴露类的最大抱怨正是 OP 在问题中所做的。通过使私有变量对整个单元可见的 Delphi“错误”访问私有变量。与旧的受保护黑客相同。如果不需要公开一个实现,那么我会尽量避免公开它。这种类型的东西允许功能随着时间的推移而改变,而不会破坏基于应该是私有的功能构建的代码。
  • 确实如此,但这也取决于如何使用该代码。如果是一个开发人员构建一个程序,那么谁在乎呢?另一方面,如果您正在构建一个希望被成千上万的开发人员使用的库,那么您当然需要某种保护。
  • 我很喜欢使用接口跨 DLL 边界传递“对象”。
  • @JerryDodge 通常由单个开发人员构建的代码会增长:)
【解决方案2】:

如果您没有将任何设计时非可视组件拖放到数据模块上,并且不打算这样做,那么您根本不需要数据模块。整个目的是用于设计时组件和其他实现,例如 Web 模块甚至 Windows 服务应用程序。但不适用于包装没有设计时组件的纯代码。

另外,正如 cmets 中提到的,不要混淆 TPersistent 的含义。此类的使用方式完全不同,可以集成到 IDE 对象检查器中(作为组件中的子属性)。

因此,理想的做法是将所有内容封装在一个类中。为了您的目的,数据库连接...

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;

至于“持久”的解释,您可以通过多种方式创建/销毁它的实例。例如,您可以使用单元的 initializationfinalization 部分(需要 CoInitialize),或者您可以让主窗体在创建时初始化一个全局实例。

这样做的一种常见方法是添加...

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.

第一次从任何地方调用MyData 时,将实例化一个新的全局实例。然后,每次它重新使用相同的实例。这也解决了ActiveXCoInitialize 等的需要,因为此时预计COM 已经被实例化(这是ADO 所必需的)。

这个单元的使用将非常简单——在任何地方使用将它包含在uses 中,并通过MyData 函数访问它的实例。

备注

你应该改掉全局变量的习惯。在尝试做以后的工作时,这是在自找麻烦。上面的示例显示了如何适应全局对象实例。所有其他变量都应该在该对象中自包含,或者通常是范围/相关性之一。您的TADOConnection 的全部控制权应在此处,包括连接/断开连接、异常处理、分配连接字符串。

【讨论】:

  • 我最关注你答案的最后两句话 :) 你能详细说明一下类的实例化吗?我的主窗体积极使用数据库调用。几乎所有其他子表单也是如此......到目前为止,我无法找到将初始化代码放在哪里,所以我的自定义类的实例可以从子表单中访问。子表单不得进行任何设置(即建立新连接)。他们必须触发 AdoQuery,因为引擎盖已经设置好了。
  • 当您的应用启动时,它不会立即创建它。它不应该,因为 ADO 需要 COM,VCL 会自动为您初始化它,但仅在单元初始化之后。因此,与其再次初始化 COM,不如在您第一次需要使用它之前,甚至还没有创建您的类。对于您的应用程序的整个其余部分,您将重复使用同一个实例。您将以函数的形式访问MyData,就像它是一个全局变量一样。唯一的区别是它是一个函数,因此它可以防止开发人员意外分配它。
  • 您所要做的就是从任何其他单元中使用此单元,并通过MyData 访问您的实例,而不必担心它是否已创建。甚至不要尝试从其他任何地方创建或释放它 - 让单元/功能完成所有工作。
  • 感谢您的输入,这实际上使我走上了正轨。我决定实现一个 InterfacedObject,但我从你的帖子中学到了很多。
【解决方案3】:

如果您可能对完全没有 DataModules 的替代方案感兴趣,请查看以下内容: https://github.com/stijnsanders/xxm/blob/master/Delphi/demo2/03%20Data%20ADO/xxmData.pas

查询存储在单个.sql 文件中,便于在特定的 SQL 编辑器或工作台中对其进行编辑。查询用--"QueryName" 分隔,并在启动时加载到查询存储中。假设您大部分时间都在查询较小的记录集,最好的锁定和打开方式是只读和静态的,这样可以提供最好的性能和最少的数据库服务器负载。获取字段值使用 Collect 调用,这也提供了一点性能提升。

【讨论】:

    猜你喜欢
    • 2012-10-14
    • 2018-09-21
    • 2021-11-30
    • 2013-08-21
    • 2012-01-25
    • 1970-01-01
    • 1970-01-01
    • 2014-06-16
    • 2020-01-22
    相关资源
    最近更新 更多