Главная страница
    Top.Mail.Ru    Яндекс.Метрика
Форум: "Потрепаться";
Текущий архив: 2004.09.26;
Скачать: [xml.tar.bz2];

Вниз

Ошибки БД и клиентское ПО   Найти похожие ветки 

 
kaif ©   (2004-09-07 17:27) [40]

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

 1)В отношении первой группы я считаю эффективным предварительный запрос и осмысленные сообщения, сильно завязанные на контекст того, что собирается сделать пользователь. Например, прежде, чем добавить документ с каким-то номером, отдельный SQL-запрос проверяет его на уникальность номера. Это не избавляет от необходимости уникального ключа и не гарантирует, что такая предварительная проверка всегда сработает. Но она сработает в 99% случаев и даст очень информативное и точное сообщение, например, "Счет фактура с таким номером уже существует (от 01.01.2003, фирма "Такая-то
"). А в 1% случаев (когда 2 пользователя одновременно, каждый в своей транзакции вводят документ с одним номером) достаточно будет и стандартного сообщения "Попытка нарушения уникального ключа".
 2) Вторая группа (и те ситуации в первой группе, которые не удалось разрулить предварительным запросом) нуждаются в автоматической обработке (вплоть до парсига текста сообщения). Обычно по коду ошибки (сервер IB, например, всегда присылает код ошибки и этот код доступен) можно выявить, что это за ошибка (нарушение уникальности или ссылочной целостности), затем уже парсинг текста ошибки с разрешением имен упомянутых констрейнтов в имена объектов (справочник такой-то, документ такой-то) позволяет соорудить более или менеее понятное сообщение. Информативность для пользователя здесь особенная и не требуется. Пользователь должен всего лишь понять, что он только что пытался удалить элемент, используемый другим объектом и система ему этого не позволила сделать (он и так рад, что ничего страшного сделать не сумел благодаря "умной системе"), а особенного выбора в дальнейшем поведении у него здесь, собственно, и нет.

3) "специально запрограммированные" ошибки должны обрабатываться в блоках try except end с проверкой типа ошибки сервера и очень осмысленным сообщением, например: "Конфликт одновременной работы двух менеджеров с одним остатком. Попробуйте еще раз".
--------------------
Так что я бы не стал ставить вопрос так: или-или. В каких-то случаях (уникальность) хорошо использовать предварительные проверки (эти запросы не занимают много времени, так что на них можно не экономить), зато очень важно детально понять, что здесь не так (совпадает номер паспорта или фамилия и дата рождения или еще что-то...). Часто даже приблизительные проверки здесь уместны. Например, пациента с точно такими данными в системе нет и уникальный ключ пропустит новую запись, но хитрый предварительный запрос показывает, что есть "очень похожий пациент", у которого отличается лишь номер паспорта или одна буква в отчестве...
А другие ИС (нарушение ссылочной целостности) часто неэффективно ловить с помощью предварительных запросов. Да и незачем. Пусть сервер даст ошибку, а потом - ее нужно максимально "причесать" и вывести на экран. Все равно с этим уже ничего не поделаешь.
И наконец третьи ситуации - вообще требуют, чтобы сначала сервер создал исключительную ситуацию и иначе здесь невозможно никак действовать в принципе.
Плюс к этим трем случаям - то, что собственно, следует называть ошибками и писать в "лог"-файлы (когда что-то где-то работает неправильно или вообще не работает).


 
Mystic ©   (2004-09-07 17:29) [41]

Рамиль ©   (07.09.04 17:07) [33]

под IB у меня, например, так:


Многое зависит от архитектуры. У меня этот код радостно выведет на сервере приложений сообщение, а клиент замрет пока кто-нибуть за сервером не закроет окно.

Собственно говоря, из-за архитектурных особенностей каждой конкретной реализации трудно дать общие рекомендации. Сюда же тесно примыкает проблема локализации, когда например язык сообщения зависит от локали пользователя.


 
by ©   (2004-09-07 17:41) [42]

to Sergey_Masloff   (07.09.04 16:05)
P/S У меня все действия с базой через процедуры не по этой причине.

А какая причина или надобность объясняет необходимость проводить все изменения через ХП?


 
Sergey_Masloff   (2004-09-07 17:46) [43]

by ©   (07.09.04 17:41) [42]
1) Безопасность. Никакого SQL на клиенте.
2) Большая масштабируемость.
3) Некоторые специфические фичи - в частности многие интерфейсные вещи пишут люди которые не знают структуру базы. Они просто кидают компоненты именованые в соответствии со стандартом а серверную часть пишет другой человек. И есть универсальный код который это автоматически сопрягает.


 
by ©   (2004-09-07 17:53) [44]

Т.е. на клиенте нет даже EXEC PROC SP_XXX(ID, CLIENT_NAME) а есть просто компонент типа TClient у которого свойства это поля таблички и методы типа Save Open? И сам компонент значет как и что куда сохранить?


 
Sergey_Masloff   (2004-09-07 18:06) [45]

by ©   (07.09.04 17:53) [44]
Не совсем. Хотя такое тоже делал.
Сейчас (упрощенно) есть датасет. И есть одна процедура CallProcWithDataSet() которой передается управление при инсертах апдейтах и др. махинациях. Она сама выбирает какую ХП вызвать и как ее параметры задать и как ее вызвать. Есть отдельная система управления контекстами транзакций.
 Допустим в интерфейсе понадобилось новое поле. Разработчик интерфейса смело его рисует и описывает ее поведение обращаясь к ней только как к полю датасета. Сервер про это ничего не знает и поле просто игнорирует.
 Потом разработчик серверной (СУБД) части дополняет хранимую процедуру для работы с еще одним полем. Все начинает работать с ранее скомпилированым клиентом.


 
by ©   (2004-09-07 18:16) [46]

Sergey_Masloff   (07.09.04 18:06)
Если я правильно понял, то при этом датасет это что-то типа TClientDataSet который держит запись в памяти, но не передает все автоматом серверу через ApplyUpdates а отдает все данные процедуре на клиенте, которая разбирается что с этими данными делать, так?
А сами тексты запросов вызова ХП хранятся на сервере или составляются динамически?


 
Sergey_Masloff   (2004-09-07 18:26) [47]

by ©   (07.09.04 18:16) [46]
На сервере все тексты. Клиент не может выполнить никакой SQL команды кроме вызова ХП. Все эти обращения к серверу выполняет процедура на клиенте.

Датасет это не обязательно ClientDataSet можно и обычный но с включенным режимом кэшированых изменений. Я делад в этом ключе с BDE, IBX, FIBPlus (пока "боевого" проекта не было), ODAC и DOA. В смысле с этими я делал без клиентдатасетов.
 Кстати процедуру эту очень удобно на OnApplyUpdates вызывать.


 
by ©   (2004-09-07 18:38) [48]

Sergey_Masloff   (07.09.04 17:46)
универсальный код который это автоматически сопрягает.

этот универсальный код делает маппинг полей датасета на параметры ХП?


 
Sergey_Masloff   (2004-09-07 18:42) [49]

by ©   (07.09.04 18:38) [48]
>этот универсальный код делает маппинг полей датасета на >параметры ХП?
Да, и это тоже.


 
by ©   (2004-09-07 18:47) [50]

Sergey_Masloff   (07.09.04 18:42)
Я сейчас тоже к подобному движусь, хотелось услышать подтверждение что это реально.
Единственное что я для себя не выяснил реально ли сделать универсальный доступ к разным БД, делал ли кто подобное или для каждой БД пишут свое.
Я пробовал поднять эту проблему в
http://delphimaster.net/view/14-1094475596/&web=1
но развития тема не получила.


 
Sergey_Masloff   (2004-09-07 18:52) [51]

Реально.
Насчет универсальности... Все же у каждой СУБД есть свои уникальные возможности которые лучше использовать. А универсальность к сожалению приводит к обрезанию всех фич. Так что я считаю если используешь СУБД то используй ее на полную катушку. Но это моя личная позиция. Я не верю в миграцию большой системы между разными СУБД. Идеология и технология могут быть универсальны но техническая реализация должна отличаться.


 
by ©   (2004-09-07 19:00) [52]

Sergey_Masloff   (07.09.04 18:52)
Идет речь не о миграции между СУБД, понятно что то что хорошо для Oracle не хорошо и не реализуемо для Access. Речь идет просто об обертке. Т.е. есть один класс TMyDataSet у которого есть метод Save. Так вот если этот датасет подключен к FireBird, то он внутри себя вызовет IBQuery.Post а если подключен к другому источнику данных, то его метод сохранения. Идея в том что прикладной разработчик вызывает TMyDataSet.Save для любого источника одинаково, а дело класса с этим разобраться как это делать конкретно. Вот такое и хочется реализовать.


 
Рамиль ©   (2004-09-07 22:56) [53]


> Sergey_Masloff   (07.09.04 17:14) [35]

Это было для БД с парой-тройкой клиентов, просто как раз писал этот код. А в общем случае, ИМХО, лучше как
> Mystic ©   (07.09.04 12:18) [21]
и обрабатывать исключения централизованно.

> KSergey ©   (07.09.04 17:14) [36]

А что это менеяет?

> Mystic ©   (07.09.04 17:29) [41]

Само собой.
> Насчет универсальности... Все же у каждой СУБД есть свои
> уникальные возможности которые лучше использовать. А универсальность
> к сожалению приводит к обрезанию всех фич. Так что я считаю
> если используешь СУБД то используй ее на полную катушку.
>

Согласен, если уж писать, то лучше под одну СУБД. Вон у меня под носом пример каждый день, почему не стоит делать под разные - Галактика. Она и под Betrive, и под Oracle, и под MS SQL. Парус, например, поступил в этом случае умнее, новые версии пошли только под Oracle.


 
KSergey ©   (2004-09-08 07:29) [54]

> [53] Рамиль ©   (07.09.04 22:56)
> > KSergey ©   (07.09.04 17:14) [36]
> > У вас есть явный вызов Post
> А что это менеяет?

Это меняет то, что явно известен контекст. На OnPostError - он еще более-менее тоже понятен, а вот где-нибудь на Alllication.OnException - понять его уже слишком трудно.

> [45] Sergey_Masloff   (07.09.04 18:06)
>  Допустим в интерфейсе понадобилось новое поле. Разработчик
> интерфейса смело его рисует и описывает ее поведение обращаясь
> к ней только как к полю датасета. Сервер про это ничего
> не знает и поле просто игнорирует.
>  Потом разработчик серверной (СУБД) части дополняет хранимую
> процедуру для работы с еще одним полем. Все начинает работать
> с ранее скомпилированым клиентом.

Хотелось бы уточнить техническую деталь: как понимаю, поля передаются ХП в параметрах. Новое поле - новый параметр. Однако на клиенте этот параметр уже передается, но в ХП его еще реально нет. Как отлаживать клиента на этом этапе? Или есть некий хитрый код, подсовывающий процедуре не более параметров, чем она может взять?

> [40] kaif ©   (07.09.04 17:27)

Большое спасибо за обстоятельность.


 
Sergey_Masloff   (2004-09-08 09:01) [55]

KSergey ©   (08.09.04 07:29) [54]
>Хотелось бы уточнить техническую деталь: как понимаю, поля >передаются ХП в параметрах. Новое поле - новый параметр.
Не всегда. Обычно да. Никакой проблемы нет - просто обработчик работает как итератор который обходит датасет и для каждого поля пытается найти параметр. Если не найден поле игнорируется. Все клиентское отлаживать можно - на клиенте-то поле это есть и обращаться к нему можно. Только не сохраняется.

Во всех отборах всегда количество параметров одинаково. Любое сколь угодно сложное условие передается через 1 параметр.


 
KSergey ©   (2004-09-08 10:03) [56]

> [55] Sergey_Masloff   (08.09.04 09:01)
> Любое сколь угодно сложное условие передается через 1 параметр.

Вы напрашиваетесь на вопрос "как"? ;)
Считайте, что я его задал! ;)  (псевдо/макро язык? ну не верить же мне, что строка, подставляемая во Where)


 
Sergey_Masloff   (2004-09-08 10:24) [57]

KSergey ©   (08.09.04 10:03) [56]
Довольно просто. В подробности реализации вдаваться не буду но примерно так:
pParam1=AAAA&pParam2=BBBB&pParam3=CCCC

Есть процедура (естественно одна в базовом классе) которая оббегает все контролы и заполняет эту структуру. Есть свои контролы которые допустим заполняются из справочника. Допустим сложный случай когда отбор по списку, допустим городов. Пользователь последовательно из справочника заполняет компонент внешне похожий на ListView который умеет отдать ключи выбраных объектов в виде строки (12345,634638,74656) которую можно подставить в запрос в виде IN

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

 Выглядит это (для IB например) вот так:

CREATE PROCEDURE AGREEMENT_SRCH (
   PTASKISN NUMERIC(15,4),
   PPARAMSTR VARCHAR(32000))
RETURNS (
   PSRCHSQL VARCHAR(10000))
AS
DECLARE VARIABLE VNAME VARCHAR(40);
DECLARE VARIABLE VVALUE VARCHAR(1000);
DECLARE VARIABLE VINSERT VARCHAR(100);
DECLARE VARIABLE VSELECT VARCHAR(100);
DECLARE VARIABLE VFROM VARCHAR(10000);
DECLARE VARIABLE VWHERE VARCHAR(2000);
begin
 /*
   îòáîð äîãîâîðîâ
   Àâòîð Ìàñëîâ Ñ.Ñ.
 */
 pSrchSQL = "";
 vInsert = "insert into ListIsn(TaskIsn,ObjIsn) ";
 vSelect = pTaskIsn||",a.isn ";
 vFrom   = "agreement a ";
 vWhere  = "";

 for select ParamName, ParamValue from split_param(:pParamStr) into :vName, :vValue
 do begin
   select fSelect,fFrom,fWhere from Dyn_StandartParam(:vName,:vValue,:vSelect,:vFrom,:vWhere)
   into :vSelect,:vFrom,:vWhere;
   /*-- óñëîâèÿ îòáîðà --*/
   if (vName = "pid"       ) then select pOutStr from Dyn_AddText(:vWhere,"a.id like """||UPCASE(:vValue)||"%"""," and ") into vWhere;

   if (vName = "posagoser"       ) then
   BEGIN
     vFrom = UDF_Dyn_AddText(:vFrom," left join agrid i on i.agrisn = a.isn ", "");
     select pOutStr from Dyn_AddText(:vWhere,"i.docser = """||UPCASE(:vValue)||""" and i.classisn=767683800.0 "," and ") into vWhere;
   end
   if (vName = "posagonom"       ) then
   begin
     vFrom = UDF_Dyn_AddText(:vFrom," left join agrid i on i.agrisn = a.isn ", "");
     select pOutStr from Dyn_AddText(:vWhere,"i.docno = """||UPCASE(:vValue)||""" and i.classisn=767683800.0 "," and ") into vWhere;
   end
   /* Äàòû */
   if (vName = "pdatebegs" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.datebeg >= """||:vValue||""""," and ");
   if (vName = "pdatebegf" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.datebeg <= """||:vValue||""""," and ");
   if (vName = "pdateends" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.dateend >= """||:vValue||""""," and ");
   if (vName = "pdateendf" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.dateend <= """||:vValue||""""," and ");
   if (vName = "pdateissues" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.dateissue >= """||:vValue||""""," and ");
   if (vName = "pdateissuef" ) then
     vWhere = UDF_Dyn_AddText(:vWhere,"a.dateissue <= """||:vValue||""""," and ");

   if (vName = "pdatecres" ) then vWhere = udf_dyn_addtext(:vWhere,"a.created >= """||:vValue||""""," and ");
   if (vName = "pdatecref" ) then vWhere = udf_dyn_addtext(:vWhere,"a.created <= """||:vValue||""""," and ");

   if (vName = "pemplisn" ) then vWhere = udf_dyn_addtext(:vWhere,"a.emplisn in "||:vValue||""," and ");

   if (vName = "pstatuscode"   ) then
     select pOutStr from Dyn_AddText(:vWhere,"a.status in "||UPCASE(:vValue)||""," and ") into vWhere;

   if (vName = "pruleisn" ) then select pOutStr from Dyn_AddText(:vWhere,"a.ruleisn in(select F_ISN from DICTI_DOWN(0,"||:vValue||"))","  and ") into vWhere;

   if (vName = "pclientsisn" ) then
   begin
     select pOutStr from Dyn_AddText(:vFrom, " left join agrrole r on r.agrisn = a.isn ", "") into :vFrom;
     select pOutStr from Dyn_AddText(:vWhere,"r.subjisn in "||:vValue||"","  and ") into vWhere;
   end

   if (vName = "prolesisn" ) then
   begin
     select pOutStr from Dyn_AddText(:vFrom, " left join agrrole r on r.agrisn = a.isn ", "") into :vFrom;
     select pOutStr from Dyn_AddText(:vWhere,"r.classisn in "||:vValue||"","  and ") into vWhere;
   end

   if (vName = "pobjname" ) then
   begin
     vFrom = UDF_Dyn_AddText("("||vFrom||")", " left outer join agrobject o on o.agrisn = a.isn " , "");
     vWhere = UDF_Dyn_AddText(:vWhere," o.name like """||:vValue||"%"""," and ");
   end

   if (vName = "pobjclassisn" ) then
   begin
     vFrom = UDF_Dyn_AddText("("||vFrom||")", " left outer join agrobject o on o.agrisn = a.isn " , "");
     vWhere = UDF_Dyn_AddText(:vWhere," o.classisn in (SELECT F_ISN FROM DICTI_DOWN(0,"||:vValue||"))"," and ");
   end

   if (vName = "pvin" ) then
   begin
     vFrom = UDF_Dyn_AddText("("||vFrom||")", " left outer join agrobject o on o.agrisn = a.isn " , "");
     vWhere = UDF_Dyn_AddText(:vWhere," exists (SELECT null from objcar c where c.isn = o.descisn and c.vinr like """||REVSTR(:vValue)||"%"")"," and ");
   end
   /* ~ &#243;&#241;&#235;&#238;&#226;&#232;&#255; &#238;&#242;&#225;&#238;&#240;&#224; */
 end
 if (vWhere<>"") then begin
   vWhere = vWhere||" group by a.isn";
   select fSrchSQL from Dyn_ExecSQL (:pTaskIsn,"", :vSelect, :vFrom, :vWhere, "S","AGREEMENT_SRCH") into pSrchSQL;
 end
 suspend;
end


 
Sergey_Masloff   (2004-09-08 10:30) [58]

Кстати материализация на сервере результатов запроса позволила сделать асинхронный механизм. Если запрос явно тяжелый а время у юзера есть то запрос уходит на асинхронное выполнение а потом (допустим после выходных) программа ему говорит - а помните вы искали? - так вот готово


 
by ©   (2004-09-08 12:13) [59]

Sergey_Masloff   (07.09.04 18:06)

Допустим в интерфейсе понадобилось новое поле. Разработчик интерфейса смело его рисует ...

И после того как разработчик нарисует это поле ему надо обновить программу во всех местах установок или это поле хранится в БД или конфигурационном файле каком-то?


 
Sergey_Masloff   (2004-09-08 12:30) [60]

by ©   (08.09.04 12:13) [59]
Обновляет. Все же появление новых полей это КРАЙНЕ редкая операция (в моих задачах и реализациях). Я видел реализации с хранением форм в БД но на мой взгляд тут овчинка выделки не стоит. Я анализировал этот подход но в моих задачах пользы от него особой нет. Мне чаще приходится иметь дело с изменением поведенческой стороны (скажем поле становится обязательным или наоборот) - это действительно в БД задается.


 
Карелин Артем ©   (2004-09-08 15:43) [61]

Мой подход:
1) Все проверки на правильность ввода, ссылочную целостность, и многое другое при вводе "руками" проходят в программе. Сохранить/удалить/изменить можно только в случае одобрения действий пользователя программой и проверки на корректность.
2) Целостность ключей обеспечивается генератором. Ключ формируется только сервером.
3) Вообще все ошибки отлавливаются в Application.OnException и записываются в файл со следующей инфой:
-тип ошибки
-сам текст ошибки
-активная форма, контрол
-время и дата
Ну и еще снимок экрана делается.



Страницы: 1 2 вся ветка

Форум: "Потрепаться";
Текущий архив: 2004.09.26;
Скачать: [xml.tar.bz2];

Наверх





Память: 0.62 MB
Время: 0.05 c
3-1093420208
Евген
2004-08-25 11:50
2004.09.26
фильтрация полей с русскими названиями


9-1083469676
Iraizor
2004-05-02 07:47
2004.09.26
DirectX нужна помощь по поводу заголовочных файлов


4-1092116602
Delphi5.01
2004-08-10 09:43
2004.09.26
Как получить изображение рабочего стола ...


1-1095069393
Чайник
2004-09-13 13:56
2004.09.26
Stringgrid


6-1090148252
тихий вовочка
2004-07-18 14:57
2004.09.26
Прокси





Afrikaans Albanian Arabic Armenian Azerbaijani Basque Belarusian Bulgarian Catalan Chinese (Simplified) Chinese (Traditional) Croatian Czech Danish Dutch English Estonian Filipino Finnish French
Galician Georgian German Greek Haitian Creole Hebrew Hindi Hungarian Icelandic Indonesian Irish Italian Japanese Korean Latvian Lithuanian Macedonian Malay Maltese Norwegian
Persian Polish Portuguese Romanian Russian Serbian Slovak Slovenian Spanish Swahili Swedish Thai Turkish Ukrainian Urdu Vietnamese Welsh Yiddish Bengali Bosnian
Cebuano Esperanto Gujarati Hausa Hmong Igbo Javanese Kannada Khmer Lao Latin Maori Marathi Mongolian Nepali Punjabi Somali Tamil Telugu Yoruba
Zulu
Английский Французский Немецкий Итальянский Португальский Русский Испанский