Размышления на тему обращения к полям DataSet
Читать чужой код – занятие, порой, весьма увлекательное. Иногда узнается нечто новое, а иногда появляется иное видение, казалось бы хорошо знакомых вещей.
В этом посте речь, пойдет о том как в коде приложения удобнее всего обращаться к полям DataSet. Вариантов, казалось бы не так уж и много, да и все они давно и хорошо изучены. Но, все-равно есть о чем задуматься…
В некоторой степени этот вопрос уже был затронут в болге Delphi Notes.
В начале немного теории. По умолчанию, при размещении экземпляра TDataSet на форме, Delphi автоматически создает наследников TFields для каждого поля в наборе данных, с учетом типов этих полей. В режиме проектирования (design mode) можно заменить эти поля на постоянные с помощью редактора полей. Лично я всегда предпочитаю всегда создавать поля в редакторе. Кроме того, мы можем добавить вычисляемые (calculated) и выпадающие (lookup) поля.
В коде мы можем обращаться к полям следующим образом:
MyDataSet.FieldByName(‘MyFirstField’).asString :=’Any Value';
или так:
MyDataSet.Fields[1].AsString:=’Any Value';
или же так:
MyDataSetMyFirstField.asString:=’Any Value'; – если мы сами определили набор полей.
Здесь MyDataSetMyFirstField – имя, присваиваемое созданному в редакторе полей экземпляру TField по умолчанию. Если быть точным, то тип MyDataSetMyFirstField – TStringField, который наследуется от TField. Однако, если вы переименуете DataSet, то название поля уже не измениться.
Какой из способов обращения к полю использовать – дело личное. Понятно, что обращение по индексу довольно не удобно, да и не безопасно, уж сильно легко ошибиться.
В своей практике я практически всегда обращался к полю по его названию, не используя FieldByName. Почему? Да очень просто. CodeInsight убирает необходимость постоянно смотреть название полей в таблицах. Как говориться – нажимай да дуй. Процесс разработки заметно ускоряется.
Но относительно недавно я увидел примерно такой код:
procedure TfMain.btn1Click(Sender: TObject);
var
DataSet: TDataSet;
begin
DataSet:=MyDBGrid.DataSource.DataSet;
DataSet.Insert;
DataSet.FieldByName(fldTitle).AsString:= ‘New Title';
DataSet.Post;
end;
Где fldTitle – константа, содержащая название поля.
Улавливаете замысел автора?
Мне показалось, что изначально была попытка организовать приложение следующим образом… Мы можем поменять БД (например, вместо Access использовать Interbase) и поменять набор компонентов доступа к БД. Но единственное, что нам останется сделать, это создать новые компоненты доступа к данным и переподключить к ним уже имеющиеся DataSourc’ы. Код не потребует изменений.
Конечно же речь идет все о том же приложении, в котором я менял не компоненты доступа к БД, а, как раз наоборот, гриды. И мало того, что пришлось искать соответствующие обработчики событий для cxGrid и GridEh, но и каждый раз переписывать рассмотренную выше строку.
Хотели как лучше, получилось как всегда (с).
Тем не менее, код действительно заставил призадуматься. Поверьте, этот код писал довольно грамотный разработчик, и он действительно хотел как лучше. И то, что гриды будут меняться он предвидеть не мог, а скорее допускал смену СУБД (… Access, я бы на его месте делал те же самые допущения).
Мне совершенно не понятно зачем выносить в константы имена полей. Если уж копировать структуру базы, то разумно это делать “один в один”. И уж переименование поля в самой базе – полная бессмыслица. Да и если выносить имена полей в константы, то делать это надо в отдельном модуле, а не в месте со строковыми константами, которые будут переводиться в Translation Manager’е.
Что мы выигрываем от такой конструкции: DataSet:=MyDBGrid.DataSource.DataSet;?
Проще DataSet:=dsTbl1.DataSet;
dsTbl1 в данном случае это объект TDataSource, к которому подключен некий DataSet.
Если речь идет об обработчике события в гриде, то теоритически мы можем к разным гридам цеплять разные DataSet’ы, но обрабатывать события с помощью одного обработчика:
dbgrd2.OnDrawDataCell:= dbgrd1.OnDrawDataCell;
Но тогда мы не сможем работать со значениями полей. Ведь у нас разные наборы данных с разными наборами полей. Проще использовать
DataSet:= dsTbl1.DataSet;
Но даже и это нам ничего не даст, если мы полностью скопируем имя DataSet’а вместе с именами полей (сделать это не так уж и сложно).Соответствено, в коде мы можем смело использовать конструкции типа
MyDataSetMyFirstField.asString:=’Any Value';
и особо не напрягаться. При этом используется Code Insight и прочие блага цивилизации.
А в чем же тогда премущества использования FieldByName?
Мне кажется FieldByName удобно использовать только тогда, когда изначально набор полей не известен. Т.е. его нельзя строго задать в режиме редактирования. Такая ситуация может возникнуть, если селектирующий запрос создается непосредственно в коде приложения. Например так:
var
qry : TADOQuery;
begin
qry := TADOQuery.Create(Self);
try
qry.Connection := cnDatabase;
qry.SQL.Add(‘SELECT * ‘FROM tblExpenseTaxes ‘);
qry.Open;
ShowMessage(qry.FieldByName(‘Name’).asString);
finally
FreeAndNil(Qry);
end;
end;
Если же DataSet’ы создаются в режиме проектирования, то никакого смысла использовать FieldByName и нет (по крайней мере, я его не вижу).
Собственный пост мне изрядно напомнил анекдот о сортировщике апельсинов. Тем не менее, резюме…
Если есть возможность создать список объектов – наследников TField, то лучше сделать это и использовать эти объекты для обращения к значениям полей. Во всех остальных случаях – FieldByName.
Интересно, как вы подходите к данному вопросу? А главное, какой логикой руководствуетесь при этом?
Другие статьи серии:
Редизайн интерфейса приложения. #0
Редизайн интерфейса приложения. #1
Редизайн интерфейса приложения. #2
Редизайн интерфейса приложения. #3
Редизайн интерфейса приложения. #4
Редизайн интерфейса приложения. #5
Редизайн интерфейса приложения. #6
Редизайн интерфейса приложения. #8
1. Designtime вообще (в прямом смысле этого слова, касаемо вообще Delphi) не юзать. Оный есть от лукавого.
2. Написать хелпер к датасету, чтобы можно было обращаться к полям DataSetName['FieldName']
1. В принципе, и от использования VCL можно отказаться. Юзать APIшные заголовки и все.
Только в чем смысл? Если удобнее и быстрее…
2. FieldName откуда брать? Опять подглядывать в структуру таблицы? CodeInsight его же не покажет.
Обновление данные через грид->датасорс->датасет — правильно. Необходимо в ситуациях, когда источники данных для грида меняются. Например, в режиме поиска будет один источник, в режиме просмотра — другой.
Это же касается и использование FieldByName — бОльшая часть запросов из базы идет со своими полями и иногда может часто меняться (поменяется название поля или его тип, добавится новое или удалится старое). В этом случае постоянно приходится лезть в ДатаСет и вручную его править. Очень неудобно. А уж если у поля поменялся тип данных — пиши пропало.
1. А в чем проблема переподключить DataSourse к другому DataSet’у?
2. >поменяется название поля или его тип, добавится новое или удалится старое
А как же в таком случае быть с настройками грида? Надо ведь и ширину столбцов и заголовки задавать. Что, анализировать поля подключенного DataSet’а и менять это все в коде?
Как раз, по моему проще при изменении структуры таблицы поменять DataSet (это делается вообще мышкой)
А если речь идет о том, что к одному гриду цепляются датасеты разной структуры, в ходе выполнения программы, то такой подход, имхо, совсем плох. Впрочем, в cxGrid для этого есть представления…
Но я сторонник того, что бы в рантайме не создавать датасеты, которые отображаются в гриде.
Только FieldByName – созадавать поля это плодить лишние сущности.
А лукапы и вычисляемые поля это вообще ужас ужас – лучше все это делать в запросе.
Я не совсем понял относительно “лишних сущностей”.
Поля создадутся в датасете вне зависимости от того, хотите Вы этого или нет.
Вопрос только в том, сможете ли Вы обращаться к ним по имени в коде.
procedure TfMain.Button2Click(Sender: TObject);
var
qry: TADOQuery;
i: integer;
begin
try
qry:= TADOQuery.Create(self);
Qry.Connection:= con1;
Qry.SQL.Add(‘SELECT * FROM feeds’);
Qry.Open;
ShowMessage(IntToStr(Qry.Fields.Count));
for I := 0 to Qry.Fields.Count – 1 do
begin
ShowMessage(Qry.Fields[i].Name);
ShowMessage(Qry.Fields[i].ClassName);
ShowMessage(Qry.Fields[i].FieldName);
end;
finally
FreeAndNil(Qry);
end;
end;
Замените селектирующий запрос для своей таблицы и попробуйте выполнить этот код
Ну в коде то они есть
Шучу.
У вас вполне обоснованные аргументы к созданию полей в дизайнмоде. Спорить смысла нету, можно только, что то противопоставить.
Я например хочу, что бы в проекте все обращения к полям выглядели одинаково. А т.к. у меня много ДС создаются динамически, то использование FieldByName выглядит наилучшим образом.
Когда я писал заметку в DelphiNotes я особо не акцентировал внимание, но при создании полей в Design-Time есть ещё одна коварная вещь – это размер строковых полей.
Вот пример: я, как проектировщик, вдруг решил: “для поля Name 64 символов пользователю наверняка хватит”. Затем написал приложение, объект для поле Name создался в DesignTime, и в одном из его свойств сохранилось значение 64 (длина текстового поля). После этого – приложение компилируется и выпускается, заказчик доволен. Но в один прекрасный день заказчик пишет: “А не могли бы вы увеличить размер поля, у меня тут много похожих названий, и если их обрезать до 64 символов, то они все выглядят одинаково… мне бы хотя бы 80 символов…”.
И что мне приходится делать? А вот что:
1. alter table modify name varchar2(80). Но этого ещё мало, приложение не узнает о том, что размер поля в БД реально может вместить 80 символов. Поэтому надо:
2. Найти это поле в датамодуле, поменять его размер, пересобрать приложение.
3. Попросить заказчика скачать новую версию приложения.
И вот ведь не задача: заказчиков, использующих это приложение, много. Если другие заказчики возьмут новую версию приложения, то это новое приложение позволит ввести в поле name 80 символов, но в БД у этих заказчиков это поле до сих пор ограничено 64 символами (ну не накатили им ещё это несущественное обновление БД). В итоге будет ошибка при insert или update записи. А пользователи скажут: “Вот скачал я новую версию приложения, а оно глючит :(. Не буду больше скачивать ваши обновления”.
Кроме длины строкового поля, может ещё поменяться тип (например с Integer на Int64 или на Float). Ну и ещё я в своей практике очень часто допускаю, что некоторых полей в БД может и не быть (приложение должно работать и на старых версиях БД, и на новых). Поэтому для себя я чётко определил: в серьёзных приложениях никаких созданий полей в DesignTime быть не должно.
Хотя в простых случаях, когда вы используете локальные базы данных (например Accsess) и скрипт обновления БД поставляется заказчику вместе с новым приложением – это уже не принципиально.
Проблема обновлена БД, не обновлено приложение у меня решена просто. Есть версия БД, есть версия приложения. Они контролируются на совместимость.
Поправить поля в DataSet в режиме проектирования – не велика проблема. Сколько у Вас DataSet’ов обращается к одной таблице?
Это же делается мышкой удалил поле- добавил недостающее…
А вот как без фиксированного набора полей рисовать гриды я представляю слабо. Это же работы – мрак.
К слову. В моем приложении, как раз набор полей создан для всех датасетов, данные из которых выводятся в контролы. Но запросы к этим датасетам формируются динамически… Т.е. в грид попадет всегда один и тот же набор полей. Главное правильно селектирующий запрос написать.
Те же датасеты, данные из которых не отображаются, создаются в коде.
> Сколько у Вас DataSet’ов обращается к одной таблице?
Чаще всего: одна таблица – один датасет. Но в нашем основном проекте – примерно к 10% от общего количества таблиц (а это около 50 таблиц из примерно 500) используется по несколько select-запросов (до 3х-4х, иногда больше) вместо одного.
> Это же делается мышкой удалил поле- добавил недостающее…
Да, допускаю такое в относительно небольших проектах, тут спорить глупо.
> А вот как без фиксированного набора полей рисовать гриды я представляю слабо
)
Эээ, а зачем их “рисовать”? Структуру полей в гриде можно создать в RunTime. Можно написать свой компонент-наследник от стандартного грида, который будет делать какую-то специфику. Мы, к примеру, используем свою обёртку над TVirtualTreeView (очень рекомендую этот компоненет
В тех случаях, когда надо дополнительно подсветить ячейки или каким-то образом сгруппировать данные, или вывести свою прорисовку – это решается либо через обёртку, либо через обработчики событий. Причём, если поля в наборе данных нет, то и столбца в гриде для этого поля не будет, а значит и в обработчики прорисовки эти столбцы не будут передаваться.
Немного сложнее с формами редактирования записей, но и там есть варианты: а) форма может строиться полностью автоматически, б) для полей, которых нет в наборе данных, визуальные элементы ввода данных скрываются.
> Т.е. в грид попадет всегда один и тот же набор полей. Главное правильно селектирующий запрос написать.
Если я правильно Вас понял… было и у меня такое когда-то… сейчас я понимаю, что это от лукавого.
Вообще со временем у меня пришло понимание, что любая универсальность хороша в меру. Хорошо, например, создать базовую фрейму, которая содержит грид и… ну скажем property TDataSet, при назначении которого НД сразу же отображается в гриде (а в гриде создаются столбцы и их расположение/размеры/видимость загружаются из реестра/файла конфигурации).
Хорошо, когда в этой базовой фрейме можно определить набор стандартных действий, типа “добавить запись”, “удалить запись”, “редактировать запись”, “обновить”. И хорошо, когда для каждого (ну или почти каждого) НД создавать свой наследник от базовой фреймы и в этом наследнике уже прописывать дополнительную логику, свойственную конкретному НД.
И плохо, ну ОЧЕНЬ ПЛОХО, когда один модуль с гридом может работать с разными НД – в этом случае в коде появляются проверки, с каким именно НД мы работаем в данный момент времени, что приводит к большому кол-ву смешанного кода…
Впрочем, я не знаю что там у вас за приложение, возможно такой подход очень даже оправдан.
P.S.: честно говоря, мне кажется, что стандартные датасеты довольно устаревшее на сегодня явление. Как BDE. Однако заменить движок BDE на альтернативы в существующем уже приложении гораздо проще (поэтому BDE сейчас почти никто и не использует), чем уйти от DataSet’ов. Поэтому люди и пишут свои DataSet’ы (ну или скачивают/покупают компоненты, совместимые с DataSet’ами) и пишут код по старинке.
Я тоже стараюсь избегать создавать поля в Design-time. Sw выше привёл очень хороший пример. И размер поля может увеличится, и тип может изменится. У одного клиента в одной и той же таблице может быть 10 полей, у другого – 15. Динамическое создание полей, позволяет избежать массы проблем.
>Мне совершенно не понятно зачем выносить в константы имена полей. … И уж переименование поля в самой базе – полная бессмыслица.
Всё же случаются ситуации, когда без переименования поля в БД не обойтись. При обновлении используемой версии СУБД, может возникнуть ситуация, когда название поля в таблице стало совпадать с вновь появившимся зарезервированным словом. У меня возникала. Приходилось переименовывать.
>Да и если выносить имена полей в константы, то делать это надо в отдельном модуле, а не в месте со строковыми константами, которые будут переводиться в Translation Manager’е.
Согласен. Для строк, которые надо переводить обычно используются resourcestring. Для строк, которые не надо переводить – используются константы.
>У одного клиента в одной и той же таблице может быть 10 полей, у другого – 15.
Не уловил мысль.
>Всё же случаются ситуации, когда без переименования поля в БД не обойтись.
Алексей, это скорее несчастный случай, чем закономерность. Достаточно придерживаться простых правил именования полей и все будет ОК. Я мало -мальски подозрительное слово в структуре БД всегда сопровождаю подчеркиванием. Во избежание, так сказать…
Да и в чем проблема-то переименовать поле в DataSet?
2 щелчка мышкой.
Боитесь, что не вспомните, а какие же DataSet’ы используют это поле? Напишите тестовый код и откройте все DataSet’ы в модуле. Сразу увидите.
>>У одного клиента в одной и той же таблице может быть 10 полей, у другого – 15.
>Не уловил мысль.
Специфика такова, что БД индивидуально
допиливается под каждого клиента.
Поэтому и наборы полей в одной и той же
таблице у разных клиентов может быть разным.
И приложение приходится проектировать,
учитывая такую особенность программы.
Я так полагаю, что всё во многом зависит от
специфики приложений. Всё же, shareware-
программы массового потребления, делаются
по другим законам, чем корпоративные программы.
В первом случае, все пользователи имеют
примерно одинаковую структуру БД. И чем меньше
пользователь будет беспокоить суппорт, тем лучше.
Во-втором случае, каждый клиент вместе с
программой получает индивидуальную поддержку,
и специалиста который настроит всё под
нужды клиента. Т.е. по сути, клиент покупает
не просто программу, а конструктор на базе
которого ему соберут продукт. (а ля 1С).
2 sw
>Чаще всего: одна таблица – один датасет. Но в нашем основном проекте – примерно к 10% от общего количества таблиц (а это около 50 таблиц из примерно 500) используется по несколько select-запросов (до 3х-4х, иногда больше) вместо одного.
———–
И у меня примерно так. Но набор полей возвращается один и тот же.
> Структуру полей в гриде можно создать в RunTime.
Можно
Но зачем?
У меня просто все гриды настраиваемые. Я в дизайн тайме его 1 раз настроил, и выкинул приложение с этими настройками. Дальше юзер сам его строит как ему удобно (в том числе может прятать “лишние поля”), а настройка грида запоминается.
Набор полей один и тот же…
И про “автоматические” формы редактирования… Знаем, плавали. Не понравилось
> ОЧЕНЬ ПЛОХО, когда один модуль с гридом может работать с разными НД
100%
В таких случаях отдельный датасет и отдельный грид.
Приложение мое, ну скажем так, средней сложности…
около 50 таблиц. Проблема в том, что туда давно просится нормальная СУБД, но пока там Access, по ряду причин.
> И у меня примерно так. Но набор полей возвращается один и тот же.
А, кажется я наконец уловил мысль… Я понимаю так, что есть НД с полями A, B, C, D. В одном случае используются поля A, B, D, в другом A, B, C, в третем – все поля. Так? И, т.к. мы работаем с одной сущностью и большинство полей у нас совпадают, то и используется одно представление (грид).
Это логически верный подход, примерно так оно и используется у меня, с той лишь разницей, что порядок следования полей, их видимость и ширины столбцов по умолчанию хранятся в xml-описании. А это позволяет: один-два запроса отображать в одной фрейме, другие запросы – в другой фрейме (а каждая фрейма неиспользуемые поля скрывает из списка доступных пользователю).
Кстати, как Вы предлагаете (и предлагаете ли?) пользователям сбросить настройки грида в значения по-умолчанию? Как это реализуется (перечитывается dfm на лету, или устанавливается флаг и при следующем открытии формы игнорируется загрузка пользовательских значений)?
У меня просто стоит кнопка Reset Settings. Прибиваются все ini файлы и приложение перезапускается в исконном виде.
Кроме этого, у меня в опциях есть возможность переключить с “простого режима” на “продвинутый” и наоборот. При переключении видимость столбцов принудительно меняется.
Думаю, что нужно использовать все варианты. Использовать всегда, везде, один вариант (ради красоты одинаковости кода) это глупость проводящая к потери гибкости. т.к. у каждого варианта есть свой плюс и минус. И даже если создать классы-обвёртки (или их создаст за Вас кодо-генератор), это не будет панацеей, по любому где то оптимальней будет FieldByName или Fields[i].Value
В таком случае, как вы считаете, будет-ли востребовано решение, которое позволит вставлять элементы структуры БД (названия полей, таблиц и т.д.) в код одним щелчком мышки?
>решение, которое позволит вставлять элементы структуры БД (названия полей, таблиц и т.д.) в код одним щелчком мышки
Если я правильно понял – кодо-генератор который не генерит структуру СУБД а берёт её как ‘модель’ и генерит по ней паскаль..
Не знаю, наверно да, хотя трудно сказать, смотря как это будет реализовано.
>2. Написать хелпер к датасету, чтобы можно было обращаться к полям DataSetName['FieldName']
Товарищ, можешь не напрягаться, это сделал Борланл лет ~10 назад: qQuery['FieldName'] := vVariant;