Delphi Базовый класс для Model-view-controller

Model-view-controller (MVC, «Модель-представление-поведение», «Модель-представление-контроллер») – один из шаблонов проектирования. При работе с базами данных можно каждый раз писать необходимые запросы или дублировать их, а можно организовать структуры данных (классы). Они будут содержаться данные из базы, но для их получения не будет нужно писать запрос заново, необходимо будет только создать экземпляр соответствующего класса и вызвать метод получения данных, обработать их как требует задача и сохранить, используя все тот же метод экземпляра класса. При таком подходе нет необходимости вникать в сложные схемы данных – класс сам знает, что необходимо делать. Это удобно когда необходимо выполнить простые действия, над данными, без вникания в глубокие глубины.

Сей час есть «мастера», которые генерируют автоматически некоторый класс по схеме данных. Возможно, такой «мастер» существуете для Delphi, но я пока его не встречал. Поэтому для своих собственных нужд разработал базовые классы для единичной записи и списка. В данной статье я приведу исходный код, в котором из комментариев будет понятно, что и как в нем работает, что необходимо будет сделать в базовом классе для того что бы все заработало. Базовые классы полезны тем, что позволят вам сразу заложить в код гибкость в дополнении и изменении. Это избавит от рефакторинга большого количества классов, когда вдруг окажется, что надо поменять общее для всех поведение экземпляров классов. Скачать файл с исходным текстом можно здесь uCommonEngine (после скачивания необходимо будет поменять расширение файла на “.pas”). В примере для работы с базой данных используется технология ADO, а так же особенности СУБД MS SQL Server.

</pre></pre></pre>
unit uCommonEngine;
{*******************************************************************************
*     Базовые классы сожержаште соновные методы для работыы с значениями из БД.
* Позволяет создавать как единичные экземпляры классав, так и списки. Так же
* возможно создниае срутктур вида "Документ" с заголовкам и списком строк. Для
* этого от    TCommonEngine наследуется класс TDocXXX в котором ключевые поля
* заполняются ключевыми полями заголовка документа, и внем создаются два
* экземпляра класса типа заголовок документа и строки документа. Все процедуры
* TDocXXX следует перегрузить с тем расчетом, что бы они обеспечивали
* согласование заголовка и строк документа (получение данны, сохранение,
* удаление и т.д. ).
*******************************************************************************}
interface
uses ADODB, DB, Classes, SysUtils;
type

TFillParamsProc = procedure (AParams: TParameters;
AParamValues: Pointer) of object;

{Класс для единичной записи из БД.}
TCommonEngine = class
private
FSID: Integer;
FID: Integer;
FUpDateString: string;
FInsertString: string;
FTableName: string;
FSelectString: string;
FDeleteString: string;
FisDeleted: Boolean;
procedure SetID(const Value: Integer);
procedure SetSID(const Value: Integer);
procedure SetInsertString(const Value: string);
procedure SetUpDateString(const Value: string);
procedure SetTableName(const Value: string);
procedure SetSelectString(const Value: string);
procedure SetDeleteString(const Value: string);
procedure SetisDeleted(const Value: Boolean);
public
Fconnection: TADOConnection;
{Конструктор, в нем
инициализируются
isDeleted:= False
ID:= -1
Поэтому обязательно писать в наследуемом классе

inherited Create;

заполняются свойства
- TableName
- InsertString
- UpDateString
- SelectString
- DeleteString }
constructor Create;virtual;
{Выполняет выбор значений из БД. Перед візовом не обходимо заполнять ключевіе поля.
Вызывает процедуры
- GetSelectedFields
- FillParametersSelectOrdelete

Имеет смысл
заполнять в наследуемом классе

Result:= inherited GetByID;
}
function GetByID: Boolean; virtual;
{Выполняет заполенение свойств , которые не выбираются из таблицы, которая
записана в свойстве TableName, или же они вычисляются на основе существующих
свойств. вызывается не зависомо от GetByID, но не ранее т.к. все остальные
свойства могут быть не заполнены. Имеет смысл
заполнять в наследуемом классе

Result:= inherited GetAdditionalInfo;
}
function GetAdditionalInfo: Boolean; virtual;
{Выполняет заполеннеи свойств класса значениями из полей, которые
возврщаются из БД при выполнении запроса SelectString. Имеет смысл
заполнять в наследуемом классе

Result:= inherited GetSelectedFields(AFields)
}
function GetSelectedFields(AFields: TFields): Boolean; virtual;
{Выполняет вставку в таблицу записи, вней же происходит дополнение
InsertString параметром id и возврат этого параметра в свойство класса
id идентификтаора вставленной записи. Параметры для вставки берутся из
соотвествующих свойств наследуемого класса и запонюябтсяони в процедуре
FillParamsInsertOrUpdate используя параметр AParameters. Значение id
заполнять не нужно, оно будет заполнено автоматически. Если при вставке
не требутся делать каких-либо дополниеьтльных действий, то в наслеудемо
классе имеет смысл заполнить данную процедуру следующим кодом

Result:= inherited Insert;
}
function Insert: Boolean; virtual;
{Выполняет обновление записи в базе данных. Параметры для вставки берутся
из соотвествующих свойств наследуемого класса и запонюябтсяони в процедуре
FillParamsInsertOrUpdate используя параметр AParameters. Значение id
заполнять не нужно, оно будет заполнено автоматически. Если при вставке
не требутся делать каких-либо дополнительных действий, то в наслеудемо
классе имеет смысл заполнить данную процедуру следующим кодом, если не
требуется выполнять никаких дополнительных действий

Result:= inherited Update;
}
function Update: Boolean; virtual;
{Выполняет удаление записи из базы данных вне зависимости от значения
свойства isDeleted. Параметры для удаления заполняются в процедуре
FillParametersSelectOrdelete
В наслеудемо классе имеет смысл заполнить данную процедуру следующим кодом,
если не требуется выполнять никаких дополнительных действий

Result:= inherrited Delete
}
function Delete: Boolean; virtual;
{Выполняет сохранение класс по его параметрам.
Если установлено true свойство isDeleted, то вызывается процедура Delete,
в противном случае если id > 0, то вызывается процедура Update.
В наслеудемо классе имеет смысл заполнить данную процедуру следующим кодом,
если не требуется выполнять никаких дополнительных действий

Result:= inherrited Save;
}
function Save: Boolean; virtual;
{Заполняем параметры отличне от id значениями свойтсв класса при вставке
или обновлении  записи }
function FillParamsInsertOrUpdate(AParameters: TParameters): Boolean; virtual;
{Заполняем параметры отличне от id значениями свойтсв класса}
function FillParametersSelectOrdelete(AParameters: TParameters): Boolean; virtual;
{строка содержащая SQL - запрос для вставки одной зааписи в БД.
Поле id не надо указывать в запросе, оно будет добавлено при выполнении
запроса и заполнено идентификатором вставленной записи, и возвращено в
свойство класса ID, идентификатор вставленной записи определяется по
свойству  TableName. Данная строка заполняется в конструкторе
наследуемого класса.}
property InsertString: string read FInsertString write SetInsertString;
{строка содержащая SQL - запрос для обновления одной зааписи в БД. Данная
строка заполняется в конструкторе наследуемого класса.}
property UpDateString: string read FUpDateString write SetUpDateString;
{строка содержащая SQL - запрос для обновления одной зааписи в БД. Данная
строка заполняется в конструкторе наследуемого класса.}
property SelectString: string read FSelectString write SetSelectString;
{строка содержащая SQL - запрос для удаления одной зааписи в БД. Данная
строка заполняется в конструкторе наследуемого класса.}
property DeleteString: string read FDeleteString write SetDeleteString;
{Идентификатор записи }
property ID: Integer read FID write SetID;
{Идентификатор станции записи }
property SID: Integer read FSID Write SetSID;
{--------------------------------------------------------------------------}
{Название табилцы БД, в которой хранятся записи для создаваемого класса.
Заполняется в конструкторе класса.}
property TableName: string read FTableName write SetTableName;
{Признак того что экземпляр класса удален и при выполнении процедуры Save
будет вызвана процедура Delete}
property isDeleted: Boolean read FisDeleted write SetisDeleted;
end;

{Для списка значией обного типа из списка базы даннаан. Перед описанием
списка элементов необходимо описать класс единичных записей списка }
TCommonListEngine = class
private
FOnFillParams: TFillParamsProc;
FExternalParams: Pointer;
FSelectAllString: string;
procedure SetExternalParams(const Value: Pointer);
procedure SetOnFillParams(const Value: TFillParamsProc);
procedure SetSelectAllString(const Value: string);
public
FConnection: TADOConnection;
{список записей из таблицы БД.}
FItems: TList;
{Коструктор в нем заполняется
SelectAllString - запрос, который отбирает идентификаторы для списка
объектов из таблицы.

Необходимо заполняется значение процедурного свойства OnFillParams, котоорое
вызывается в момент запленеия параметров запроса к БД. Это позволит при
необходимоссти измениьт параметры отбора путем замены процедуры, которая
их заполняет

обязательно прописываем  inherited Create
}
constructor Create;virtual;
{В деструкторе освобождается память всех элементов списка.
Обязательно прописываем  inherited Create
}
destructor Destroy;virtual;
{Сохраняет все элементы списка, вызывая для каждого из них метод  Save,
обязательно дописываем inherited Save}
function Save: Boolean;virtual;
{Обновляет данные по элементам списка вызывая для каждого из них медоты
GetByID;
GetAdditionalInfo;
}
function RefreshItem: Boolean;virtual;
{Осуществляет поиск лемента списка по ключневым занчениям, если ключеапое
поле только одно - id, то ASID = - 1 }
function FindByID(AID , ASID: Integer): TCommonEngine;
//function AddItem(AItem: TFDBClassesCommon): Boolean;
{Удаляет ве эелемент списка путем выполенняи процедуры DeleteItem дял
каждого элемента
имеет смысл заполнить в наследуемом классе
Result:= inherited Delete;
}
function Delete: Boolean; virtual;
{Удаляет элемент списка по его индесу
имеет смысл заполнить в наследуемом классе
Result:= inherited DeleteItem(AIndex);
}
function DeleteItem(AIndex: Integer): Boolean;
{Выполняет SQL - запрос для выбора индентификаторов при отборе элементов в
список}
function GetAll: Boolean; virtual;
//function FillParams(AParams: TParams): Boolean;virtual;
{Прописывается пребор полученных из БД записей и заполнение
элементтов списка.
При том элмент сиписка явным образом создается, добавляется в список,
заполняются его ключевые поля и после этого для него вызываются методы
GetByID;
GetAdditionalInfo;}
function GetSelectedFields(AQuery: TADOQuery): Boolean;virtual;
{Освобождает память для всех элементов списка}
procedure ClearItems;
{процедурное свойство, которое вызывается при заполнении парамтеров
запроса. При необходимости подменяется другой процедурой, а строка
SelectAllString для того что бы можно было варьировать запрос для отбора
элементов в список, инициализуруется в конструкторе}
property OnFillParams: TFillParamsProc read FOnFillParams
write SetOnFillParams;
{ССылка на знячения параметров используемых при завполнеии параметров
SQL - запроса, если параметр один - то можно заполянть
ExternalParams:= pointer(id)
а извлекать
id:= integre(ExternalParams)
Если параметр не один, то необходимо создавать либо структуру либо класс,
запсывать в указатель ссылку на него , при заплонеии параметров извлекать
из указатля значения , и при заверщении рботы класса освобождатьрессурсы.
}
property ExternalParams: Pointer read FExternalParams
write SetExternalParams;
{строка которая содержит запрос для отбора идентификаторов для списка
элементов, заполняется в кострукторе }
property SelectAllString: string read FSelectAllString write SetSelectAllString;

end;

implementation

{ TCommonEngine }

constructor TCommonEngine.Create;
begin
isDeleted:= False;
ID:= -1;
end;

function TCommonEngine.Delete: Boolean;
begin
with TADOCommand.Create(nil) do
try
Connection:= Fconnection;
CommandText:= DeleteString;
Parameters.ParseSQL(CommandText, True);
FillParametersSelectOrdelete(Parameters);
Execute;
finally
Free;
end;
end;

function TCommonEngine.FillParametersSelectOrdelete(
AParameters: TParameters): Boolean;
begin
with AParameters do
begin
ParamByName('id').DataType:= ftInteger;
ParamByName('id').Value:= ID;
end;
end;

function TCommonEngine.FillParamsInsertOrUpdate(AParameters: TParameters): Boolean;
begin
Result:= True;
with AParameters do
begin
ParamByName('id').DataType:= ftInteger;
ParamByName('id').Value:= ID;
if ID <= 0 then begin
ParamByName('tmpTableName').DataType:= ftString;
ParamByName('tmpTableName').Value:= TableName;
end;
end;
end;

function TCommonEngine.GetAdditionalInfo: Boolean;
begin
Result:= True;
end;

function TCommonEngine.GetByID: Boolean;
begin
//Result:= True;
with TADOQuery.Create(nil) do
try
Connection:= Fconnection;
SQL.Text:= SelectString;
Parameters.ParseSQL(SQL.Text, True);
FillParametersSelectOrdelete(Parameters);
Open;
Result:= GetSelectedFields(Fields);
finally
Free;
end;
end;

function TCommonEngine.GetSelectedFields(AFields: TFields): Boolean;
begin
with AFields do
begin
ID:= FieldByName('id').AsInteger;
end;
end;

function TCommonEngine.Insert: Boolean;
begin
Result:= True;
with TADOQuery.Create(nil) do
try
Connection:= Fconnection;
SQL.Text:= InsertString+' set :id = IDENT_CURRENT(:tmpTableName)';
Parameters.ParseSQL(SQL.Text, True);
FillParamsInsertOrUpdate(Parameters);
Parameters.ParamByName('id').Direction:= pdInputOutput;
ExecSQL;
ID:=  Parameters.ParamByName('id').Value;
finally
Free;
end;
end;

function TCommonEngine.Save: Boolean;
begin
if isDeleted then
Result:= Delete
else
if ID > 0 then  Result:= Update
else Result:= Insert;
end;

procedure TCommonEngine.SetDeleteString(const Value: string);
begin
FDeleteString := Value;
end;

procedure TCommonEngine.SetID(const Value: Integer);
begin
FID := Value;
end;

procedure TCommonEngine.SetInsertString(const Value: string);
begin
FInsertString := Value;
end;

procedure TCommonEngine.SetisDeleted(const Value: Boolean);
begin
FisDeleted := Value;
end;

procedure TCommonEngine.SetSelectString(const Value: string);
begin
FSelectString := Value;
end;

procedure TCommonEngine.SetSID(const Value: Integer);
begin
FSID := Value;
end;

procedure TCommonEngine.SetTableName(const Value: string);
begin
FTableName := Value;
end;

procedure TCommonEngine.SetUpDateString(const Value: string);
begin
FUpDateString := Value;
end;

function TCommonEngine.Update: Boolean;
begin
Result:= True;
with TADOCommand.Create(nil) do
try
Connection:= Fconnection;
CommandText:= UpDateString;
Parameters.ParseSQL(CommandText, True);
FillParamsInsertOrUpdate(Parameters);
Execute;
finally
Free;
end;
end;

{ TCommonListEngine }

procedure TCommonListEngine.ClearItems;
var
lCommonEngine: TCommonEngine;
i: Integer;
begin
for i:= 0 to FItems.Count - 1 do
begin
lCommonEngine:= FItems[i];
lCommonEngine.Free;
end;
end;

constructor TCommonListEngine.Create;
begin
FItems:= TList.Create;
end;

function TCommonListEngine.Delete: Boolean;
var
i: Integer;
begin
for i:= 0 to FItems.Count - 1 do
begin
Result:= DeleteItem(i);
end;
ClearItems;
end;

function TCommonListEngine.DeleteItem(AIndex: Integer): Boolean;
var
lCommonEngine: TCommonEngine;
begin
Result:= True;
lCommonEngine:= FItems[AIndex];
FItems.Delete(AIndex);
lCommonEngine.Free;
end;

destructor TCommonListEngine.Destroy;
begin
ClearItems;
FItems.Free;
inherited;
end;

function TCommonListEngine.FindByID(AID , ASID: Integer): TCommonEngine;
var
lCommonEngine: TCommonEngine;
i: Integer;
begin
Result:= nil;
for i:= 0 to FItems.Count - 1 do
begin
lCommonEngine:= FItems[i];
if (lCommonEngine.FID = AID)
and ((lCommonEngine.FSID = ASID) or (ASID = -1)) then begin
Result:= lCommonEngine;
Break;
end;
end;
end;

function TCommonListEngine.GetAll: Boolean;
var
lQuery: TADOQuery;
begin
Result:= True;
lQuery:= TADOQuery.Create(nil);
with lQuery do
try
try
ClearItems;
Connection:= FConnection;
SQL.Text:= SelectAllString;
Parameters.ParseSQL(SQL.Text, True);
if Assigned(FOnFillParams) then
OnFillParams(Parameters, ExternalParams);
Open;
GetSelectedFields(lQuery);
Self.RefreshItem;
Except on e: Exception do
begin
Result:= False;
Raise;
end;
end;
finally
lQuery.Free;
end;
end;

function TCommonListEngine.GetSelectedFields(AQuery: TADOQuery): Boolean;
begin
Result:= True;
end;

function TCommonListEngine.RefreshItem: Boolean;
var
lCommonEngine: TCommonEngine;
i: Integer;
begin
Result:= True;
for i:= 0 to FItems.Count - 1 do
begin
lCommonEngine:= FItems[i];
lCommonEngine.Fconnection:= FConnection;
lCommonEngine.GetByID;
lCommonEngine.GetAdditionalInfo;
end;
end;

function TCommonListEngine.Save: Boolean;
var
lCommonEngine: TCommonEngine;
i: Integer;
begin
for i:= 0 to FItems.Count - 1 do
begin
lCommonEngine:= FItems[i];
lCommonEngine.Save;
end;
end;

procedure TCommonListEngine.SetExternalParams(const Value: Pointer);
begin
FExternalParams := Value;
end;

procedure TCommonListEngine.SetOnFillParams(const Value: TFillParamsProc);
begin
FOnFillParams := Value;
end;

procedure TCommonListEngine.SetSelectAllString(const Value: string);
begin
FSelectAllString := Value;
end;

end.

Share

Tags: , , , , ,

3 Responses to “Delphi Базовый класс для Model-view-controller”

  1. Никита пишет:

    А можнор сделать ещё внедрение зависимостей и создавать один экземпляр на всё время работы программы! Ждём следующую статью на эту тему! ;)

  2. О каких зависимостях идет речь?
    Насчет одного экземпляра объекта стоит задуматься. А зачем иметь один универсальный объект который будет делать все что угодно? Если при этом в нем необходимо будет описать все поля из базы из все таблиц. Тогда если надо будет сделать мелкое действие, то надо будет выделять память под все объекты, если со сложными структурами данных можно сделать вагон и маленькую тележку проверок, то с простыми типами данных такой номер не пройдет, память под них мы не очистим – она будет занята.

    Этот базовый класс и базовый класс списка имеет смысл использовать для того что бы создавать объекты соответствующие записям в таблицах баз данных, но при этом не дублировать один и от же код , как например создание объекта для выполнения запроса к базе данных и прочие вещи по шаблону. Конечно на много проще и быстрее скопировать код, чем про наследоваться и корректно перегрузить нужные функции, но когда надо будет вносить правки или изменения, то искать по исходникам куда еще ты чего на копировал дело мало приятное и если нет тестов автоматических еще и много часовое отлаживание внесенных изменений. А так поменял в одном месте и порядок корректно работает везде.

    Это шаблон по которому можно быстро сделать класс для конкретной записи в таблице, а на его основе сделать список объектов ддля последующего использования в прикладной программе. Кое кто может возразить, что не надо делать объекты, а все проделывать в хранимых процедурах и вызывать их в формах, не вопрос можно так делать, но отладка хранимых процедур в MS SQL дело весьма не приятное, и до 2008 MS SQL там в принципе можно только в консоль писать текстовые сообщения, которые программист наполняет великим смыслом :).

    Ну и самое главное преимущество объектов заключается в том , что приложение меньше зависит от СУБД, ведь мы не привязаны к таблицам использует только поля класса, а и все равно из какой базы они пришли, главное что они есть и логика обработки их не изменилась.

  3. [...] При работе базой данных часто происходит обращение к одним и тем же таблицам из разных форм и программных модулей. Во избежание дублирования SQL – запросов, а так же удобства работы с сущностями в программном коде был создан общей класс (шаблон), который реализует работу как с одиночными записями, списками одиночных записей и объединениями одиночных записей и списков одиночных записей, привязанных или не привязанных к единичным записям. Как правило, такими структурами являются документы, в которых заголовок и строки принадлежат к различным типам единичных записей. В общем случае это есть Object-relational mappin. Данная статья существенная переработка шаблона предложенного в пред идущей статье Delphi Базовый класс для Model-view-controller. [...]

Leave a Reply