

      Динамическая фильтрация записей баз данных в системе   CLIPPER 5.0+.

        Система программирования/управления базами данных CLIPPER 5.0+
    фирмы Nantucket, несомненно, имеет целый ряд преимуществ по сравнению
    с другими dBASE - подобными системами. Желающие также могут указать на
    целый ряд недостатков, которые окажутся такими же относительными, как
    и достоинства. Выбор рабочей среды - дело вкуса и темой данного сообще-
    ния не является дискуссия о недостатках и преимуществах конкретных сис-
    тем. Пользователям системы CLIPPER 5.0+ предлагается метод, существенно
    ускоряющий доступ к данным в некоторых случаях.

       Довольно часто при конструировании схем баз данных (БД) пользователи
    используют классический метод связи нескольких файлов (таблиц, отноше-
    ний) по общему ключу (простому либо составному). Также, часто эти связи
    имеют вид многозначной зависимости типа "один - ко - многим". Возьмем
    простой пример. Отношение ФИРМЫ-ПАРТНЕРЫ содержит информацию о фирмах,
    а именно: наименование, тип, телефон, факс, адрес, банковские реквизиты
    и т.д.  Отношение СОТРУДНИКИ содержит информацию о сотрудниках каждой
    фирмы : Ф.И.О., адрес, домашний телефон, должность и т.д.  В общем слу-
    чае, каждая из фирм имеет в штате одного или более сотрудников. Схема
    связи при этом имеет следующий вид (автор заранее приносит извинения за
    случайное совпадение с реальными вымышленных названий и фамилий):

     ФИРМЫ-ПАРТНЕРЫ
        (FIRMS)
  ┌──────────────┬──────────┬──────────────
  │   Название   │   Тип    │   Телефон
  │   (fname)    │  (type)  │   (phone)
  ├──────────────┼──────────┼──────────────
  .              .          .
  .........................................
  ├──────────────┼──────────┼──────────────
  │ Redis        │  СП      │ 66-77-99       ─────┐
  ├──────────────┼──────────┼──────────────        │
  │ Ricko        │  МП      │ 202-10-09            │
  ├──────────────┼──────────┼──────────────        │
  │              │          │                      │
  .              .          .  ┌───────────────────┘
  .              .          .  │          СОТРУДНИКИ
                               │           (STUFF)
                               │    ┌──────────────┬────────────┬────
                               │    │   Название   │   Фамилия  │
                               │    │   (fname)    │    (Name)  │
                               │    ├──────────────┼────────────┼────
                               │    .              .          .
                               │    ..................................
                               │    ├──────────────┼────────────┼────
                               │┌─ │ Redis        │Поплавский  │
                               ││   ├──────────────┼────────────┼────
                               └┼─ │ Redis        │Стецько     │
                                │   ├──────────────┼────────────┼────
                                └─ │ Redis        │Хаимзон     │
                                    ├──────────────┼────────────┼────
                                    .              .            .
                                    .              .            .

        В данном случае реализована связь по ключу названия фирмы. Считаем
    также, что файл FIRMS проиндексирован по содержимому поля FNAME
    ( UPPER(FNAME) ), а файл STUFF - по сцепленному содержимому полей FNAME
    и NAME ( UPPER(FNAME+NAME) ).
        Допустим, пользователю необходимо выбрать (т.е. визуализировать на
    экране) всех сотрудников какой-либо фирмы. В системе CLIPPER версии до
    5.0 это можно было сделать несколькими способами.

       Во-первых, наложить на отношение СОТРУДНИКИ условие фильтрации типа:
                            .
                            .
                     SELECT  STUFF
                            .
                            .
                     SET FILTER TO FNAME == FIRMS->FNAME


    и воспользоваться стандартной функцией визуализации типа DBEDIT(). Допус-
    тим, однако, что отношение СОТРУДНИКМ включает около 1000 записей, а
    штат искомой фирмы насчитывает, скажем, 5 человек. При этом Вам придет-
    ся подождать, пока DBEDIT() представит нужные 5 записей. Далее, при
    нажатиях клавиш управления сменой текущей записи, приводящих к выходу
    за пределы списка сотрудников данной фирмы, Вы можете спокойно рассла-
    биться на пару минут. За это время система отфильтрует все лишние запи-
    си и вернет Вам управление клавиатурой. Напомним, ссылаясь на описание
    CLIPPER'а, что при обработке условия SET FILTER последовательно обраба-
    тываются ВСЕ записи. Это является причиной того, что время работы с
    фильтрованным и нефильтрованным файлом оказывается одним и тем же.


        Во-вторых, можно создать новое пустое отношение и "вручную" перепи-
    сать в него все записи, удовлетворяющие нашему условию. Переход к пер-
    вой искомой записи выполняется с помощью команды SEEK, далее, в цикле -
    SKIP пока выполняется условие  FNAME == FIRMS->FNAME. Сформированное
    отношение также можно визуализировать стандартными средствами. Единст-
    венным, чем может гордиться пользователь, прибегая к такому способу,
    является то, что все выполняется в точности по доктору Кодду, а именно:
    результатом операции над отношением является новое отношение.

        Наконец, можно поломать голову и написать собственную процедуру
    выборки и визуализации, выводящую только записи, удовлетворяющие зара-
    нее указанным (посредством, например, макрооператора &) условиям. Если
    есть тяга к изобретательству в области экипажей, приводимых в движение
    мускульной силой ног - пожалуйста!

        Все вышеприведенные способы, как мы видим, особым изяществом не
    отличаются. Тем более преступно применять подобные методы в системе
    CLIPPER версий 5.0+. В этих версиях пользователю предоставляются эле-
    менты объектно-ориентированного программирования, реализованные в виде
    предопределенных классов, а также кодовые блоки.
        Нас интересует класс TBROWSE, предоставляющий средства для постро-
    ения систем визуализации отношений. При работе с этим классом, среди
    прочих, предоставляется возможность подстановки пользовательских кодо-
    вых блоков для выполнения перехода к следующей/предыдущей записи
    (Skipblock), переходов к началу (Gotopblock) и к концу (Gobottomblock)
    отношения. По умолчанию TBROWSE обрабатывает весь файл, т.е. переход к
    следующей/предыдущей записи выполняется с помощью команд SKIP/SKIP -1
    с проверкой условий BOF() и EOF(). Переход к началу файла выполняется
    командой GO TOP, к концу файла - GO BOTTOM. В то же время, пользовате-
    лю предоставляется возможность самому управлять выборкой записей из
    файла, служащего источником данных для TBROWSE. Нужно только переопре-
    делить стандартные кодовые блоки, управляющие переходами в TBROWSE и
    использовать упорядоченность записей, предоставляемую индексацией.

        В нашем случае, Skipblock можно переопределить следующим образом:

//   Строим экземпляр объекта TBROWSE для работы с отношением:
                    .
                    .
         SELECT  STUFF
         userbrowse:= tbrowsedb(6,3,17,52)
                    .
                    .
                    .
         userbrowse:skipblock:= {|x|userskip(x,userbrowse)}
//  Параметр блока - x - число записей для перехода
                    .
                    .
                    .
//  Функция переходов :

         function userskip(x,browse)
         local i:=0
         if x>0 .and. !eof()
          do while i<x
           skip 1
// Проверка выхода за нижнюю границу :
           if  eof() .or. FNAME <> FIRMS->FNAME
            skip -1
            exit
           endif
           i++
          enddo
         elseif x<0
          do while i>x
           skip -1
// Проверка выхода за верхнюю границу :
           if bof()
            exit
           endif
           if FNAME <> FIRMS->FNAME
            skip 1
            exit
           endif
           i--
          enddo
         endif
         return i

 Gotopblock описывается вообще просто :

    userbrowse:gotopblock:= {||dbseek(trim(upper(FIRMS->FNAME)))}

    т.е., началом выборки является первая запись файла STUFF, имеющая
    значением поля FNAME название искомой фирмы. Пользователи CLIPPER
    версии 5.0, в отличие от 5.01 не имеющей стандартизированных низкоу-
    ровневых функций работы с БД( типа dbseek() ), могут посмотреть в
    файле STD.CH реализацию команды SEEK.

 Блок перехода к концу файла можно описать двумя способами. Во-первых,
 возможен последовательный перебор записей, начиная с текущей, до тех пор,
 пока не изменится значение поля FNAME:

   userbrowse:gobottomblock:= {||dbeval(NIL,,{||FNAME == FIRMS->FNAME},,,.T.)}

   Преимуществом является начало перебора от текущей записи (а она может
   быть и последней в выборке) и уверенность в оптимальном написании фирмой
   NANTUCKET функции dbeval(). Однако, при наличии большого числа записей в
   выборке, процесс может несколько замедлиться.

  Во вторых, можно воспользоваться возможностью "мягкого" поиска (SOFTSEEK)
  и с помощью функции индексного поиска сразу перейти к записи, непосредст-
  венно следующей за искомой выборкой:

// Сначала устанавиваем "мягкий" поиск, сохраняя текущее значение :
  userbrowse:gobottomblock:= {||r_s:= set(_SET_SOFTSEEK,.T.),;
// Строим выражение для поиска следующей записи :
  dbseek(upper(stuff(padr(FIRMS->FNAME,len(FNAME)),len(FNAME),1,;
  chr(asc(right(padr(FIRMS->FNAME,len(FNAME)),1))+1))))),;
// Восстанавливаем старое настройку "мягкого" поиска :
   set(_SET_SOFTSEEK,r_s)}

   Преимущество такого способа - переход к концу выборки по времени прак-
   тически не зависит от объема выборки. Недостатками можно считать допол-
   нительное обращение к индексному файлу. Более серъезный недостаток -
   в случае, если данному условию не удовлетворяет ни одна запись (пустая
   выборка), можно при переходе к концу оказаться у несоответствующей за-
   писи.

        После переопределения блоков можно начинать манипуляции с экземпля-
   ром объекта TBROWSE для визуализации и выбора необходимой записи.
        Собственно, подобный метод формирования выборки и назван динамичес-
   кой фильтрацией, поскольку программист сам управляет перемещением указа-
   теля в пределах границ условия селекции, в отличие от "статической" ( по
   отношению к программисту ) обработки команды SET FILTER. Напомним также,
   что данный метод эффективно применим только к выборке по индексированно-
   му либо отсортированному полю.
        Поскольку каждый раз строить экземпляры объекта TBROWSE и манипули-
   ровать ими оказывается довольно громоздской операцией, то для удобства
   пользователей, привыкших к функции DBEDIT(), предлагается функция
   MDBEDIT(), полностью совместимая с DBEDIT() и, кроме того, реализующая
   все вышеизложенные идеи. Текст этой функции приведен в файле MDBEDIT.PRG.
   Формат вызова функции следующий :

      MDBEDIT([<nTop>], [<nLeft>], [<nBottom>], [<nRight>],
            [<acColumns>], [<cUserFunction>],
            [<acColumnSayPictures> | <cColumnSayPicture>],
            [<acColumnHeaders> | <cColumnHeader>],
            [<acHeadingSeparators> | <cHeadingSeparator>],
            [<acColumnSeparators> | <cColumnSeparator>],
            [<acFootingSeparators> | <cFootingSeparator>],
            [<acColumnFootings> | <cColumnFooting>],
            [<nFrozen>], [<cSkipFunc>],
            [<bGotopblock>], [<bGobottomblock>], [<nFixed>])

        Первые 12 параметров аналогичны параметрам DBEDIT(). Далее идут
   новые параметры ( не ошибитесь в подсчете запятых ! ) :

        <nFrosen> - номер столбца, "замороженного" при горизонтальном скрол-
                    линге и содержимое которого всегда высвечивается.

      <cSkipFunc> - функция управления переходами между записями, используемая
                    в Skipblock. На вход этой функции передаются 2 параметра
                    ( см. пример выше): X - число записей для перехода и
                    Browse - текущий экземпляр объекта TBROWSE.

    <bGotopblock> - кодовый блок, используемый для перехода к первой записи.

 <bGobottomblock> - кодовый блок, используемый для перехода к последней
                    записи.

         <nFixed> - номер столбца, запрещенного для перехода к нему (но не
                    для высвечивания!). В сочетании с Frozen может использо-
                    ваться для индикации содержимого неизменяемого столбца,
                    например номеров записей.

        На вход пользовательской функции UserFunction передаются не 2, как
   в DBEDIT(), а 3 параметра. Первые 2 параметра, режим и номер столбца,
   аналогичны используемым в DBEDIT(). К списку режимов добавлен режим
   DE_INIT ( 5 ). Пользовательская функция с этим режимом на входе вызыва-
   ется только один раз, в начале работы MDBEDIT(). Может быть использован
   для настройки экранных форм либо корректировки экземпляра объекта
   TBROWSE.
        3-й параметр - BROWSE - передает текущий экземпляр объекта TBROWSE.
   Поскольку объекты передаются по ссылке, то можно производить различные
   изменения над ними, например, менять те же самые блоки управления либо
   настраивать цвета.

        В нашем случае вызов этой функции будет следующим (см. пример выше):

        MDBEDIT(6,3,17,52,{'NAME'},,,{'Фамилия'},,,,,,'userskip',;
        {||dbseek(trim(upper(FIRMS->FNAME)))},;
        {||dbeval(NIL,,{||FNAME == FIRMS->FNAME},,,.T.)})

        Возьмем столь же тривиальный, но несколько усложненный пример. Для
   каждой из фирм нужно выбрать сотрудников этой фирмы, фамилии которых на-
   чинаются на букву 'К'. Функция перехода в этом случае примет следующий
   вид:

         function userskip(x,browse)
         local i:=0
         if x>0
          do while i<x .and. !eof()
           skip 1
// Проверка выхода за нижнюю границу :
           if  eof() .or. FNAME <> 'К' .or. FNAME <> FIRMS->FNAME
            skip -1
            exit
           endif
           i++
          enddo
         elseif x<0
          do while i>x
           skip -1
// Проверка выхода за верхнюю границу :
           if bof()
            exit
           endif
           if FNAME <> 'К' .or. FNAME <> FIRMS->FNAME
            skip 1
            exit
           endif
           i--
          enddo
         endif
         return i

        Блок перехода к началу выборки примет следующий вид :

    userbrowse:gotopblock:= {||dbseek(upper(FIRMS->FNAME)+'К')}

        Блок перехода к концу выборки может принять одну из альтернативных
   форм:

   userbrowse:gobottomblock:= {||dbeval(NIL,,{||FNAME == FIRMS->FNAME;
    .and. FNAME = 'К'},,,.T.)}

    либо:

  userbrowse:gobottomblock:= {||r_s:= set(_SET_SOFTSEEK,.T.),;
   dbseek(upper(FIRMS->FNAME)+'Л'),;
   set(_SET_SOFTSEEK,r_s)}

        Далее рассмотрим более сложный пример. Работаем только с отношением
   ФИРМЫ-ПАРТНЕРЫ. Необходимо выбрать все фирмы, названия которых начинаются
   с буквы 'K' либо с буквы 'M' ( селекция по условию логического ИЛИ ).
   Функция перехода выглядит следующим образом:

         function userskip(x,browse)
         local i:=0, r
         if x>0 .and. !eof()
          do while i<x
           skip 1
// Проверка выхода за нижнюю границу :
           if  eof() .or. FNAME > 'M'
            skip -1
            exit
// Проверка выхода за букву 'K' :
           elseif FNAME > 'K' .and. FNAME < 'M'
            r:= recno()
            seek 'M'
// Если на букву 'M' вообще нет названий :
            if !found()
// Возврат на предыдущую позицию :
             goto r
             skip -1
             exit
            endif
           endif
           i++
          enddo
         elseif x<0
          do while i>x
           skip -1
// Проверка выхода за верхнюю границу :
           if bof()
            exit
           elseif FNAME < 'K'
            skip
            exit
// Проверка выхода за букву 'M' :
           elseif FNAME > 'K' .and. FNAME < 'M'
            r:= recno()
            set softseek ON
            seek 'L'
            set softseek OFF
            skip -1
// Если на букву 'K' вообще нет названий :
            if FNAME <> 'K'
             goto r
             skip
             exit
            endif
           endif
           i--
          enddo
         endif
         return i

   Громоздско ? Возможно. Однако работает быстрее, чем при наложении усло-
   вия SET FILTER TO FNAME = 'K' .or. FNAME = 'M'.

   Блок перехода к началу выборки имеет следующий вид:

    userbrowse:gotopblock:= {||dbseek('К')}

   Блок перехода к концу выборки имеет следующий вид:

  userbrowse:gobottomblock:= {||dbseek('M'),;
  dbeval(NIL,,{||FNAME = 'M'},,,.T.)}

               либо:

  userbrowse:gobottomblock:= {||r_s:= set(_SET_SOFTSEEK,.T.),;
                                dbseek('N'),;
                                set(_SET_SOFTSEEK,r_s)}

        Наконец, еще один пример. Работаем только с отношением СОТРУДНИКИ.
   Необходимо выбрать всех сотрудников, фамилии которых начинаются с буквы
   'К' и работающих в фирмах, начинающихся с буквы 'М'. Функция перехода
   выглядит следующим образом:

         function userskip(x,browse)
         local i:=0, r
         if x>0
          do while i<x
           skip
           if eof() .or. FNAME <> 'M'
            skip -1
            exit
// Если фамилия не начинается на 'К', то переходим к другой фирме :
           elseif NAME <> 'К'
            r:= recno()
            LOCATE REST FOR NAME='К' WHILE FNAME='M'
// Если фамилий на 'К' больше нет, то возвращаемся :
            if !found()
             goto r
             skip -1
             exit
            endif
           endif
           i++
          enddo
         elseif x<0
          do while i>x
           skip -1
           if bof()
            exit
           elseif FNAME <> 'M'
            skip
            exit
           elseif NAME <> 'К'
            r:= recno()
// Приходится работать "вручную":
            do while NAME <> 'К' .and. FNAME = 'M'
             skip -1
            enddo
            if FNAME <> 'M'
             goto r
             skip
             exit
            endif
           endif
           i--
          enddo
         endif
         return i

        Блок перехода к началу выборки выглядит следующим образом:

    userbrowse:gotopblock:= {||dbseek('M'),;
    if(found(),(__dblocate({||NAME='К'},{||FNAME='M'},,,.T.),;
// Если не найдено, идем на конец файла:
       if(!found(),(dbgobottom(),dbskip()),)),)}

 (Функция __dblocate() не стандартизирована и подсмотрена в файле STD.CH,
  где она используется для реализации команды LOCATE).

        Блок перехода к концу выборки выглядит следующим образом:

  userbrowse:gobottomblock:= {||r_s:= set(_SET_SOFTSEEK,.T.),;
                                dbseek('N'),dbskip(-1),;
// Приходится использовать "внешнюю" функцию:
                                skipback(),;
                                set(_SET_SOFTSEEK,r_s)}

     procedure skipback
     local r
      do while NAME <> 'К' .and. FNAME = 'M'
       skip -1
      enddo
// Если не найдено, идем на конец файла:
      if FNAME <> 'M'
       go bottom
       skip
      endif

        Все вышеперечисленные примеры реализованы в файле DYNAMIC.EXE.
   Исходные тексты находятся в файле DYNAMIC.PRG. Попробуйте, запустив
   DYNAMIC.EXE, понажимать клавиши , , PgUp, PgDn, находясь у границ
   визуализированной выборки, а также Ctrl-PgUp, Ctrl-PgDn. Если у Вас
   имеется опыт работы с выборками из базы, отселектированными с помощью
   команды SET FILTER, то разницу Вы должны заметить.
        Резюме таково. Система CLIPPER в очередной раз оправдывает замы-
   сел ее разработчиков - каждый пользователь может настраивать систему
   под свои требования, используя предоставляемые системой примитивы.




