среда, 13 июля 2011 г.

Размышления на тему: недостатки датасетов

Данный пост представляет из себя размышления на тему: недостатки TDataSet. Я предполагаю, что читатель знаком с тем, что TDataSet – это базовый класс для работы с наборами данных (НД). Так же я предполагаю, что читатель знаком с основами программирования приложений для работы с базами данных (БД).

Delphi предлагает такую схему работы с НД:

а) TDataSet – для работы с самим НД;

б) TDataSource – источник данных, используется для связи НД с визуальными компонентами;

в) связь компонент с НД (для отображения и редактирования) через DataSource.

Вроде всё красиво.

Самое простое – это создание формы для редактирования конкретной записи: каждый контрол на форме связывается с конкретным полем НД. Перед редактированием пользователь выбирает запись, а после редактирования – либо сохраняет изменения, либо отменяет. Работа с конкретной записью не вызывает нареканий.

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

  Bookmark := DataSet.Bookmark;
  DataSet.DisableControls;
  try
    // work with DataSet
  finally
    DataSet.Bookmark := Bookmark;
    DataSet.EnableControls;
  end;

DisableControls/EnableControls необходимо делать, чтобы контролы не реагировали на изменение текущей записи и не вызывали мерцания, Bookmark нужен, чтобы после работы с НД вернуться к той записи, с которой работал пользователь.

У DataSet’ов есть свойство – текущее состояние. Есть промежуточное состояние – dsBrowse, и много разных – запись редактрируется, ищется, и т.п. Причём переход из одного состояния в другой возможен только через dsBrowse. А это означает, что если НД открыт на редактирование, то поиск по этому же НД уже не возможен. Более того, если я работаю, например, с иерархической структурой, то открыв запись на редактирование, я не могу использовать этот же НД для указания родительской записи.

В зависимости от реализации наследника от DataSet’а, можно наткнуться на дополнительные сюрпризы. Например, я сталкивался с тем, что работа с текущей записью осуществляется через некоторый буфер. Т.е. переход от записи к записи вызывает копирования полей записи в буфер, и программист работает с данными из буфера (а не непосредственно с данными записи). С одной стороны – это защита от случайной записи в набор данных. Но с другой – это приводит к неприятным задержкам при копировании.

Вобщем, интерфейс класса TDataSet не позволяет открыть сразу несколько записей на редактирование, приходится создавать модальные формы, что не очень красиво по отношению к пользователю.

 

Описанные проблемы – это не панацея. Все указанные неприятности либо решаются обходными путями, либо их можно игнорировать: задержки копирования сегодня настолько незначительны, что простому пользователю не заметны; чтобы не писать DisableControls/EnableControls можно создать шаблон кода.

 

Самым главным неудобством в датасете для меня является понятие текущей (активной) записи. На мой взгляд, это понятие надо отнести к гриду (либо, возможно, к DataSource’у). Если бы так было сделано изначально, то мы (программисты) получили бы возможность прозрачной навигации по НД. А работа с конкретной записью – она ведь определяется пользователем: именно пользователь выбирает строку в таблице (или даже несколько (!) строк), программисту логично запросить у грида выделенную строку (список выделенных строк) и работать с ними.

В подтверждение сказанного я могу привести простой пример: вспомните, сколько существует визуальных компонент для работы с гридам; самые удобные перед работой делают полную копию датасета и не работают с датасетом непосредственно, либо предлагают использовать “свои” датасеты.

 

Наличие этих недостатков привело к созданию собственных механизмов для работы с НД. Подробно я их пока описывать не буду, приведу только такой пример:

TableName['FieldName'].AsString[Row] := SomeValue;

Здесь: TableName – имя таблицы, FieldName – имя поля, Row – ссылка на запись в НД. Так выглядит обращение в наших библиотеках к полю в общем случае. А благодаря автогенерации кода, о которой я попробую рассказать попозже, такой же код мы давно уже пишем так:

TableName.FieldName[Row] := SomeValue;

 

Ну и на последок скажу, что изобретение своего велосипеда у нас зашло довольно далеко – написаны механизмы для работы с БД, отдельно обёртки над компонентом VirtualTreeView для визуализации данных в виде таблицы, и отдельно обёртки над обычными компонентами ввода данных типа TEdit, TCombobox и т.п. С одной стороны, много самописного кода – обо всём этом за один раз не рассказать. Но с другой стороны нам это даёт много бонусов, что в конечном итоге (и я в это верю) положительно отражается на пользовательских интерфейсах.

12 коммент.:

Юрий комментирует...

Проблема с 1-й текущей строкой решается
cds2.CloneCursor(cds1, True);
cds2 и cds1 будут иметь оно пространство данных, но разные тек. строки, фильтрации, гриды ...
Любой ds в cds преобразовывается 1 действием (можно без кода)

Николай Зверев комментирует...

Да, о ClientDataSet'ах я уже, если честно, и забыл :)

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

Т.е. что предлагает нам Borland: DataSet - DataSource - DBGrid. Ну или вот Вы указали: DataSet - ClientDataSet (по сути - тот же DataSet, но только с некоторыми бонусами) - DataSource - DBGrid; ClientDataSet позволяет расшарить один НД в разные представления. Однако DataSource, как связующее звено, содержит лишь ссылку на DataSet.
А мне бы хотелось видеть в DataSource и активную запись, и набор выделенных записей, и параметры сортировки, фильтрации и т.п., чтобы всем этим управлять, не трогая сам DataSet.

Чорны кашак комментирует...

В finally вы забыли еще высвободить ресурсы
DataSet.FreeBookmark(Bookmark)

Николай Зверев комментирует...

хм.. сначала хотел поправить с оговоркой, что это только для старых версий Delphi, а потом заглянул в хелп: в Delphi 7 можно использовать два варианта закладок:
- TBookmark (Pointer), для неё надо использовать GetBookmark/GoToBookmark/FreeBookmark
- TBookmarkStr (string) - св-во Bookmark.

Stalker комментирует...

ИМНО если ваша основная проблема с TDataSet, это невозможность навигации по нему по время редактирования данных конкретной строки, то просто не надо редактировать данные в DB контролах типа TDBEdit, TDBComboBox, TDBMemo и т.п.
Редактируйте данные в обычных контрола типа TEdit, TComboBox, TMemo и тогда проблем с навигацией по TDataSet будет гораздо меньше.

Хотя конечно, если бы у TDataSet была бы возможность бегать по нему без изменения текущей записи - это было бы очень удобно.

Такая возможность есть например в TMemTableEh из библиотеки EhLib. Там можно бегать и искать по внутреннему буферу TMemTableEh без изменения текущей записи.

Юрий комментирует...

По идеи это возможно и у TDataSet, ведь у него есть метод

function Lookup(KeyFields, KeyValues, ResultFields): Variant; virtual;

и он вытаскивает данные из ДС не передёргивая тек.запись и статус. Как то он это мутит...

Алексей Тимохин комментирует...

Спасибо за пост. Интересные проблемы озвучены. Сам не раз думал, как избавится от постоянного использования Dataset.FieldByName('') но так и не решился заменять на что-то.

С интересом жду продолжения.

2 Stalker:
Интересная инфа про TMemTableEh. Спасибо за наводку.

Coriolis комментирует...

Здравствуйте, Николай. Разрешите поинтересоваться, вы в своих обёртках над DataSource реализовывали подгрузку данных "по запросу" - для больших таблиц, чтобы не грузить всё на клиент при открытии таблицы? Если да, то борьбой с фризами (из-за сети) боролись, и как? Передо мной стоит задача реализовать свой DataSet для NoSQL бд (очень старой и очень не SQL ;) ).

Особенность бизнес-логики системы в том что частенько открываюстя таблицы с большими списками. Хочется грузить помалу, но так чтобы клиент не чувствовал задержки, т.е. в другом потоке. Вот, собираю инфу в интернете, наткнулся на эту статью.

Николай Зверев комментирует...

Coriolis, доброй ночи.
У нас есть понятие "Справочник" - это таблица с ограниченным кол-вом записей. Скажем, до 200. Такие таблицы подгружаются при первом обращении полностью (причём за один fetch, т.е. за одно обращение к БД), на такие таблицы можно спокойно создавать лукапы, по ним можно строить комбобоксы. Их можно показывать пользователю полностью.

Иногда "Справочник" разрастается и становится "таблицей". В этом случае лукапы делать не правильно и грузить целиком на клиент - тоже нельзя. Фоновый поток, не особо спасёт, вообще нет смысла пользователю показывать большое кол-во записей. Надо реорганизовать как UI, так и запросы к БД. Да, это порой муторно, но пользователя надо уважать.
Для SQL-баз принципиально сложного ничего нет (в простых случаях - добавляется where-условие, в сложных - временные таблицы и сохранённые процедуры).

Для NoSQL - не помогу, если действительно надо грузить всё в оперативную память и порциями показывать пользователю и не "вешать" UI... ну да, второй поток и промежуточный буфер. Т.е. из БД в фоне порция попадает в буфер, затем буфер копируется в клиентский (локальный) датасет, с которым уже работает UI. Затем снова в буфер-датасет-UI и так далее. Организовывать очередь чтения я бы не стал (или отложил это на потом), копирования в оперативной памяти происходят незаметно для пользователя, и если используются "правильный" датасет (типа MemTable) и грид (типа VirtualTreeView), то задержек на создание отображаемых записей в гриде пользователь не почувствует.

Coriolis комментирует...

Спасибо, Николай.
На счет UI полностью согласен, подход пора менять, но в силу разных причин пока не могу занятсья перелопачиванием всего интерфейса, да и не везде получится, если честно.
Из моей базы не проблемы выдёргивать нужные куски, так же как в SQL.

Последний обзац - именно моя "текущая" версия. Буду писать обёртку над MemTable, при открытии спрашивать у базы сколько строк в таблице, выделять это количество в MemTable, и добавлю колонку isLoaded (условно), которая будет показывать загружены ли уже данные по этой строке. А грузить уже по мере того как у моей обертки будут запрашивать незагруженные строки. Сортировку-группировку-фильтрацию на сервере если есть незагруженные строки либо локально если всё в памяти.
В целом громоздко конечно, придётся как-то думать над инструментом который будет ограничивать ввод в DB поля если их DataSet стоит на строке с isLoaded=FALSE. Но по другому как показывать произвольную таблицу максимально быстро и с адекватным скролом я не придумал.

Спасибо что подтвердили мои мысли, мне постоянно кажется что я упускаю что-то важное, это от нехватки опыта работы с DB компонентами - у меня всё самописное, но я понял что трачу слишком много времени на поддержку и допиливание, решил подвинутсья ближе к мейнстриму :)

Николай Зверев комментирует...

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

Анонимный комментирует...

Для восстановления положения курсора все гораздо проще.

procedure ....
var nRec : integer;
begin
nRec := ClientDataSet1.RecNo; // сохраняем положение курсора, т.е. номер записи на которой стоим
ClientDataSet1.DisableControls; // отключаем контроль
// Ваша обработка и прочие действия...


ClientDataSet1.RecNo := nRec; // возвращаемся на запись
ClientDataSet1.EnableControls; // включаем контроль
end;

Безо всяких BookMark-ов

Отправить комментарий

.

.