Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4473.html
 

Практика #5. Конечная реализация

Опубликовано 16.05.2011 г. 20:12

Последняя часть из серии постов о решении практической задачи редактирования данных.

Начнем статью с того, на чем закончили предыдущую часть: реализуем базовый класс для сохранения значений используя БД. Как яхту назовешь так она и поплывет (: Начнем с именования нашего класса: TCustomDBDataItem<T>. Предполагаем, что все потомки данного класса будут работать с БД. Нам видимо потребуется два объекта запросов: первый - запрос выборки данных. Его всегда надо держать открытым, чтобы иметь возможность делать refresh() данных. Запросы же на изменение/удаление/вставку можно проводить через второй объект, т.е он будет таким "рабочим" запросом. Что нам тут понадобится. Во-первых, место где мы будем хранить сам тексты запросов; во-вторых сами объекты запросов. Третье - функция для получения коннекта (будем предполагать что коннект у нас один на все приложение). Методы для получения и записи текстов запросов. Все это мы добросовестно определяем в private секцию, которая будет выглядеть так:
    TCustomDBDataItem<T> = class(TCustomDataItem<T>)
      strict private
        //FSQLQueries : array[TValueAction] of string;
        FSQLQueries : array of string;

        FQuery : TADOQuery;
        FSelectQuery : TADOQuery;

        function getConnection():TADOConnection;

        function  getSQL(action : TValueAction) : string;
        procedure setSQL(action : TValueAction; value : string);
Тексты запросов будут располагаться в массиве, для их индексации прекрасно подходит TValueAction (vaNone будем использовать для select запроса). Вы можете заметить что массив FSQLQueries определен как array of string, а не array[TValueAction] of string, что было бы логичным. Причина этому описана здесь. Можно подумать что getSQL & setSQL используются для свойства типа Queries[action], но это не совсем так, здесь опять используется свойство с индексом. Выглядят они следующим образом:
function TCustomDBDataItem<T>.getSQL(action: TValueAction): string;
begin
    result := FSQLQueries[byte(action)];
end;

procedure TCustomDBDataItem<T>.setSQL(action: TValueAction; value: string);
begin
    FSQLQueries[byte(action)] := value;

    if action = vaNone then
        FSelectQuery.SQL.Text := value;
end;
Теперь посмотрим на protected секцию. Этот функционал у нас для классов потомков:
      strict protected
        procedure InitQueries(); virtual;
        procedure Insert(data : TChangedValueItem<T>); virtual; abstract;
        procedure Update(data : TChangedValueItem<T>); virtual; abstract;
        procedure Delete(data : TChangedValueItem<T>); virtual; abstract;

        property Query : TADOQuery read FQuery;
        property SelectQuery : TADOQuery read FSelectQuery;
        //vaNone, vaInsert, vaUpdate, vaDelete
        property SelectSQL : string index 0 read getSQL write setSQL;
        property InsertSQL : string index 1 read getSQL write setSQL;
        property UpdateSQL : string index 2 read getSQL write setSQL;
        property DeleteSQL : string index 3 read getSQL write setSQL;
Во первых нам нужна функция, в которой дочерний класс будет устанавливать свои тексты запросов - InitQueries(). Конечно это всегда можно сделать в конструкторе. Но мы специально вынесем этот код в отдельную процедуру, тогда в большинстве случаев дочернему классу не понадобится переопределять конструктор, да и вообще выделить его стоит лишь потому, что у него есть конкретная задача - установить запросы, вот и решать ее надо отдельно от всего. Далее нам необходимы 3 абстрактных процедуры - вставка, обновление и удаление данных. Когда мы будем сохранять измененные данные, которые хранятся в кэше, то будем вызывать их, передавая значение из кэша, как параметр. Конечно нужно предоставить дочерним классам доступ к самим запросам - Query & SelectQuery, доступ на чтение. И напоследок добавим 4 свойства для записи SQL запросов, через индекс свойства. Этот индекс и является параметром get/setSQL. Все эта секция никакой реализации не требует. Разве что InitQueries, но она пустая. Последняя секция - public:
      public
        constructor Create();
        destructor Destroy(); override;
        procedure  Refresh(); override;
        procedure  Save(); override;
И тут нечего неожиданного быть не должно. В конструкторе инициализируем наш массив текстов запросов. Вызываем метод получения запросов. Создаем объекты запросов, и получаем для них коннект к БД:
constructor TCustomDBDataItem<T>.Create();
begin
    inherited;

    FSelectQuery := TADOQuery.Create(nil);
    FSelectQuery.DisableControls();
    FSelectQuery.Connection := getConnection();

    FQuery := TADOQuery.Create(nil);
    FQuery.DisableControls();
    FQuery.Connection := getConnection();


    SetLength(FSQLQueries, 4);
    InitQueries();
end;
Если вдруг стандартный коннект в конечном классе не устраивает, то функцию getConnection() можно перенести в protected секцию и сделать виртуальной. В деструкторе выполняются обратные действия. Что делает Refresh() ? Помните, когда мы на форме переключаем "элемент данных" в комбобоксе,то вызываем метод refresh? вот это он и есть. Все что он делает, это обновляет select запрос:
procedure TCustomDBDataItem<T>.Refresh();
begin
    if FSelectQuery.Active then
        FSelectQuery.Requery()
    else
        FSelectQuery.Open();
end;
Теперь самая интересность - сохранение данных. Но и тут ничего необычного. Мы пробегаем по нашему кэшу и вызываем нужную операцию, попутно запоминая все сообщения об ошибках, которые могли возникнуть:
procedure TCustomDBDataItem<T>.Save();
var i : integer;
    cvItem : TChangedValueItem<T>;
    errMsgs : string;

begin
    errMsgs := '';

    for i :=  FChangedValues.Count - 1  downto 0do begin
        cvItem := FChangedValues.items[i];
        try

            FQuery.SQL.Text := FSQLQueries[byte(cvItem.action)];
            case cvItem.action of
                vaInsert :  insert(cvItem);
                vaUpdate :  update(cvItem);
                vaDelete :  delete(cvItem);
            end;
            FChangedValues.Delete(i);
        except
            on e : Exception do begin
                errMsgs := errMsgs +
                           'class: '  + self.ClassName + '/' +
                           'action: ' + GetEnumName(TypeInfo(TValueAction), ord(cvItem.action)) + ': ' +
                           e.Message + #13#10;
            end;
        end;
    end;

    Refresh();

    if errMsgs  '' then
        raise Exception.Create(errMsgs);
end;
Обратите внимание на две вещи: первая - кэш мы перебираем с конца к началу. Причина этому в том, что мы удаляем элементы из кэша, и индексация сдвигается. Второе - элемент из кэша мы удаляем после успешного выполнения операции изменения/удаления/вставки. Таким образом, если, например, при выполнении update запроса возникнет исключительная ситуация (что обычно свидетельствует о том, что запрос не был успешно выполнен), то значение останется в кэше и будет все также отображаться в таблице на форме. Все ошибки мы записываем, и в случае если они таки были, то после обработки всех значений кэша мы возбуждаем исключение, и где то на верхнем уровне оно обрабатывается, например, показом диалога об ошибке (в нашей форме такой обработки нет, но она должна быть (: ). Здесь мы могли бы удачно реализовать собственный класс исключений, в который могли бы добавить TStringList для хранения списка сообщений об ошибке. Нам этом сей последний Custom-класс завершен. В дополнение мы можем добавить парочку алиасов, для последующего использования.
    TCustomDBIntDataItem   = TCustomDBDataItem<integer>;
    TCustomDBFloatDataItem = TCustomDBDataItem<double>;
Теперь мы можем реализовать самый последний класс, в первой статье он назывался TBirthrateDataItem. Посмотрим как выглядит его интерфейсная часть:
    [TItemDescription(dicDemography, 'Рождаемость')]
    TBirthrateDataItem = class(TCustomDBIntDataItem)
      strict private
        const StartYear = 1990;
              EndYear   = 2020;

        procedure InitHeaders(); override;
        procedure InitQueries();  override;

        function  getValue(colId,rowId : integer): TCellValue ;  override;
        function  ValueExists(colId, rowId: integer) : boolean; override;

        procedure Insert(data : TChangedValueItem);   override;
        procedure Update(data : TChangedValueItem);   override;
        procedure Delete(data : TChangedValueItem);   override;
      public
        procedure Validate(colId, rowId, value : integer); override;
    end;
Быстренько пробежимся по всем методам. Не сложно заметить что все они объявлены как override. Т.е перекрывают какой то небольшой функционал. Создание заголовком для таблицы - тут мы можем использовать вспомогательный класс TDefaultHeaderCollection, для этого мы его и создавали:
procedure TBirthrateDataItem.InitHeaders;
var hItem : THeaderItem;
begin
    FColHeaders := THeaderCollection.Create();
    hItem := THeaderItem.Create(0, 'Значение' );
    FColHeaders.Add(hItem);

    FRowHeaders := TDefaultHeaderCollections.getYearCollection(StartYear, EndYear);
end;
Очивидно, что в нашей таблице будет только один столбец данных - "значение". InitQueries просто заполняет текст запросов. Процедуры Update, Delete, Insert соответственно подставляют в запрос параметры и выполняют его. ValueExists проверяет наличие нужных данных с помощью locate:
function TBirthrateDataItem.ValueExists(colId, rowId: integer): boolean;
begin
    result := SelectQuery.Locate('years', IntToStr(rowId), []);
end;
Этот метод мы всегда вызываем перед вызовом getValue (см. TCustomDataItem<T>.getCellValue), так что в getValue нужно всего лишь вернуть активную запись:
function TBirthrateDataItem.getValue(colId,rowId: integer): TCellValue;
begin
    with SelectQuery do begin
        result.value  := FieldByName('avalue').AsString;
        result.source := FieldByName('source').AsInteger;
        result.owner  := FieldByName('owner').AsInteger;
    end;
end;
Для отсечения отрицательных значений можем использовать процедуру валидации:
procedure TBirthrateDataItem.Validate(colId, rowId, value: integer);
begin
    if value < 0  then
        raise Exception.Create('Значение не може быть меньше нуля');
end;
Собственно вот и все. Если вы дочитали до этого момента, то можете посмотреть исходный код сего модуля (код документирован):

Скачать исходный код

Что мы могли еще использовать, но не использовали? Первым в голову приходят события. Простейшая идея того как можно здесь их использовать: на нашей форме есть две кнопки - сохранить и отменить. Логично сделать так, что если в кэше есть данные, то они активны, если нет, то неактивны. Следовательно при записи данных в кэш, мы могли бы вызывать событие, а-ля OnCacheStateChanged. Параметром события было бы булево значение равное значению ( FChangedValuesItems.Count = 0 ), определяющее состояние кнопок. Не было здесь и использования интерфейсов, например. В какой то мере их заменяет первый базовый абстрактный класс. В общем, если кому то эта серия заметок принесла пользу, то значит статьи были написаны не зря (: Использоваться прямо в таком виде это наверное не может, ибо немного специфично, но тем не менее как пример реализации может быть и неплохой. Содержание:
  1. Практика #1. Постановка задачи.
  2. Практика #2. Настройка таблицы.
  3. Практика #3. Абстракция
  4. Практика #4. Обобщения и кэширование.
  5. Практика #5. Конечная реализация
Метки:  ООП 

Комментарии

Андрей
18.05.2011 в 08:26
Спасибо, здорово.
ter
18.05.2011 в 11:43
да как бы не за что (:
по ходу написания статей нашел пару косяков в коде (: так что в процессе написания проводится неплохой анализ самого кода, так что полезно и самому (:
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно