Historia zmian na formie - jak to sprytnie robicie?

0

Cześć,
w program w których czasami muszę udowadniać że klient w trakcie używania sam narozrabiał a nie jest to problem systemu stosuję zapisywanie zmian jakie zaszły na formie. Robię to już od dawna i jakoś to działa chociaż kosztuje mnie to dużo pracy.

Wygląd to mnie więcej tak że w bazie tworzę tabelę o nazwie historia a w niej kilka kolumn mniej więcej takich jak:

  • data_utworzenia (typu timestamp),
  • uzytkownik (zalogowany do programu),
  • uzytkownik_system (uzytkownik zalogowany do Windows),
  • komputer (nazwa stanowiska Windows),
  • panel (numer formy która "wpisuje" rekord do bazy),
  • operacje (czyli zapisane otwartym tekstem historie zmian lub ich brak jeśli klient wycofa się ze zmian).

Tak naprawdę wiem wszystko i wiem jakie dane były wcześniej. W ten sposób mogę dokładnie prześledzić co się działo z danymi i kto rozrabiał.

Oczywiście wiem że takie rozwiązanie powoduje bardzo duży przyrost danych w bazie ale robię to z pełną świadomością i nie o to tutaj chodzi.

Gdy tworzony jest rekord taka operacja wygląda mniej więcej tak (przykład z ostatniego programu):

procedure Tnowy_model.dodaj;
var
  licznik: integer;
  historia: string;
begin
  try
    with DM.qrytemp, SQL do
    begin
      Close;
      Clear;
      Add('INSERT INTO modele (id_marka, model, opis, utworzyl)');
      Add('VALUES (:id_marka, :model, :opis, :utworzyl)');
      ParamByName('id_marka').AsInteger := id_marka;
      ParamByName('model').AsString := edtnazwa.Text;
      ParamByName('opis').AsString := mmoopis.Text;
      ParamByName('utworzyl').AsString := main.uzytkownik;
      ExecSQL;
    end;

    with DM.qrytemp, SQL do
    begin
      Close;
      Clear;
      Add('SELECT LAST_INSERT_ID()');
      Open;
      First;
      licznik := DM.qrytemp.Fields[0].AsInteger;
    end;
  except
    ShowMessage('Błąd! Nie dodano modelu. Sprawdź dane!');
  end;

  try
    historia := 'Utworzenie modelu' + #13#10;
    historia := historia + 'Nazwa: ' + edtnazwa.Text + #13#10;
    historia := historia + 'Marka: ' + cbbmarka.Text + #13#10;
    historia := historia + 'Opis: ' + mmoopis.Text + #13#10;

    with DM.qryhistoria, SQL do
    begin
      Close;
      Clear;
      Add('INSERT INTO historia (panel, utworzyl, rekord, operacja, stanowisko) VALUES (:panel, :utworzyl, :rekord, :operacja, :stanowisko)');
      ParamByName('panel').AsInteger := 2;
      ParamByName('utworzyl').AsString := main.uzytkownik;
      ParamByName('rekord').AsInteger := licznik;
      ParamByName('operacja').AsString := historia;
      ParamByName('stanowisko').AsString := main.komputer;
      ExecSQL;
    end;
  except
    ShowMessage('Błąd! Nie zapisano zmian w historii operacji!');
  end;

  with DM.qrymodele, SQL do
  begin
    Clear;
    Close;
    Add('select mo.id, mo.id_marka, mo.model, mo.opis, mo.usun, mo.data_utworzenia, mo.utworzyl, ma.marka from modele mo');
    Add('inner join marki ma on mo.id_marka = ma.id where mo.usun = :usun order by ma.marka, mo.model');
    ParamByName('usun').AsInteger := 0;
    Open;
  end;

  DM.qrymodele.Locate('id', licznik, []);
end;

Nie kosztowało to zbyt wiele pracy ale też i panel zawierał tylko kilka pól edycyjnych.

W przypadku edycji już jest troszkę gorzej. Pokazując panel muszę najpierw uzupełnić dane w kontrolkach:

procedure Tmodele.btnedycjaClick(Sender: TObject);
begin
  edit_model.cbbmarka.Items.Clear;

  with DM.qrytemp, SQL do
  begin
    Close;
    Clear;
    Add('select marka from marki order by marka ASC');
    Open;
    First;
  end;

  while not DM.qrytemp.Eof do
  begin
    edit_model.cbbmarka.Items.Add(DM.qrytemp.FieldByName('marka').AsString);
    DM.qrytemp.Next;
  end;

  edit_model.cbbmarka.ItemIndex := edit_model.cbbmarka.Items.IndexOf(DM.qrymodele.FieldByName('marka').AsString);


  edit_model.id := DM.qrymodele.FieldByName('id').AsInteger;
  edit_model.id_marka := DM.qrymodele.FieldByName('id').AsInteger;
  edit_model.edtnazwa.Text := DM.qrymodele.FieldByName('model').AsString;
  edit_model.mmoopis.Text := DM.qrymodele.FieldByName('opis').AsString;
  edit_model.lbltworca.Caption := DM.qrymodele.FieldByName('utworzyl').AsString;
  edit_model.lbldata_utworzenia.Caption := DateTimeToStr(DM.qrymodele.FieldByName('data_utworzenia').AsDateTime);
  edit_model.Left := modele.Left + 20;
  edit_model.Top := modele.Top + 20;
  edit_model.Show;
end;

Następnie na formie edit_model tworzę zmienne dla historii:

  private
    h_marka, h_nazwa, h_opis: string;

Następnie w zdarzeniu OnShow zapamiętuję aktualny stan kontrolek:

procedure Tedit_model.FormShow(Sender: TObject);
begin
  h_marka := cbbmarka.Text;
  h_nazwa := edtnazwa.Text;
  h_opis := mmoopis.Text;
  edtnazwa.SetFocus;
end;

No i teraz jest już z górki, wystarczy w trakcie update rekordu użyć if-ów i wszystko staje się jasne:

procedure Tedit_model.btnzapiszClick(Sender: TObject);
var
  historia: string;
begin
  try
    with DM.qrytemp, SQL do
    begin
      Close;
      Clear;
      Add('UPDATE modele SET id_marka=:id_marka, model=:model, opis=:opis WHERE id=:id');
      ParamByName('id_marka').AsInteger := id_marka;
      ParamByName('model').AsString := edtnazwa.Text;
      ParamByName('opis').AsString := mmoopis.Text;
      ParamByName('id').AsInteger := id;
      ExecSQL;
    end;
  except
    ShowMessage('Błąd! Nie zaktualizowano modelu. Sprawdź dane!');
  end;

  try
    historia := 'Aktualizacja modelu' + #13#10;
    if cbbmarka.Text <> h_marka then
      historia := historia + 'Marka: było: ' + h_marka + ' jest: ' + cbbmarka.Text + #13#10;
    if edtnazwa.Text <> h_nazwa then
      historia := historia + 'Model: było: ' + h_nazwa + ' jest: ' + edtnazwa.Text + #13#10;
    if mmoopis.Text <> h_opis then
      historia := historia + 'Opis: było: ' + h_opis + ' jest: ' + mmoopis.Text + #13#10;
    if ((edtnazwa.Text = h_nazwa) and (cbbmarka.Text = h_marka) and (mmoopis.Text = h_opis)) then
      historia := historia + 'Nic nie zmieniono!';

    with DM.qryhistoria, SQL do
    begin
      Close;
      Clear;
      Add('INSERT INTO historia (panel, utworzyl, rekord, operacja, stanowisko) VALUES (:panel, :utworzyl, :rekord, :operacja, :stanowisko)');
      ParamByName('panel').AsInteger := 2;
      ParamByName('utworzyl').AsString := main.uzytkownik;
      ParamByName('rekord').AsInteger := id;
      ParamByName('operacja').AsString := historia;
      ParamByName('stanowisko').AsString := main.komputer;
      ExecSQL;
    end;
  except
    ShowMessage('Błąd! Nie zapisano zmian w historii operacji!');
  end;

  with DM.qrymodele, SQL do
  begin
    Clear;
    Close;
    Add('select mo.id, mo.id_marka, mo.model, mo.opis, mo.usun, mo.data_utworzenia, mo.utworzyl, ma.marka from modele mo');
    Add('inner join marki ma on mo.id_marka = ma.id where mo.usun = :usun order by ma.marka, mo.model');
    ParamByName('usun').AsInteger := 0;
    Open;
  end;

  DM.qrymodele.Locate('id', id, []);

  Close;
end;

Jak napisałem - jakoś to działa ale powiem szczerze że wyjątkowo mi się nie podoba. Po pierwsze te ify po drugie wszystko jest ok gdy pól edycyjnych jest nie więcej niż paręnaście (właśnie mam na pulpicie program w którym jest prawie 200 checkbox-ów).

Podzielcie się pomysłem jak to zrobić bardziej zgrabnie, może jakaś uniwersalna klasa która sama "wykryje" wszystkie pola edycyjne i zapamięta ich stan, następnie porówna przed opuszczeniem formy czy coś się zmieniło i zapisze zmiany? W taki powiedzmy sprytniejszy sposób ustawiam stan kontrolek na tej 200 elementowej formie:

  for i := 0 to ComponentCount - 1 do
  begin
    if components[i] is TCheckBox then
      TCheckBox(Components[i]).Checked := False;

    if components[i] is TEdit then
      TEdit(Components[i]).Text := '';
  end;

A może jest jakiś gotowiec do tego a ja go nie znam?

Zapraszam do dyskusji.
Robert

1

Rozważ DataBinding i operuj na abstrakcji, nie na komponentach bezpośrednio.

3
robertz68 napisał(a):

Cześć,
w program w których czasami muszę udowadniać że klient w trakcie używania sam narozrabiał a nie jest to problem systemu stosuję zapisywanie zmian jakie zaszły na formie. Robię to już od dawna i jakoś to działa chociaż kosztuje mnie to dużo pracy.

Wygląd to mnie więcej tak że w bazie tworzę tabelę o nazwie historia a w niej kilka kolumn mniej więcej takich jak:

  • data_utworzenia (typu timestamp),
  • uzytkownik (zalogowany do programu),
  • uzytkownik_system (uzytkownik zalogowany do Windows),
  • komputer (nazwa stanowiska Windows),
  • panel (numer formy która "wpisuje" rekord do bazy),
  • operacje (czyli zapisane otwartym tekstem historie zmian lub ich brak jeśli klient wycofa się ze zmian).

Ten zapis otwartym tekstem mi się nie podoba, a jeśli jest to tak, jak mi się wydaję że jest, to hmmm... do wyrzucenia :D
Może pokaż jak ten log wygląda?

Tak naprawdę wiem wszystko i wiem jakie dane były wcześniej. W ten sposób mogę dokładnie prześledzić co się działo z danymi i kto rozrabiał.

Tak, ale ten otwarty tekst to jest dobry co najwyżej do czytania.
Ale jak byśmy chcieli coś takiego parsować, bo trzeba zrobić cokolwiek z przetwarzaniem takich danych (np. wyszukiwanie) to jest słabo.
Wolałbym trzymać te dane w jakiś strukturyzowany sposób (relacyjny, XML albo JSON), byleby nie czystym tekstem.

Oczywiście wiem że takie rozwiązanie powoduje bardzo duży przyrost danych w bazie ale robię to z pełną świadomością i nie o to tutaj chodzi.

Gdy tworzony jest rekord taka operacja wygląda mniej więcej tak (przykład z ostatniego programu):

procedure Tnowy_model.dodaj;
var
  licznik: integer;
  historia: string;
begin
  try
    with DM.qrytemp, SQL do
    begin
      Close;
      Clear;
      Add('INSERT INTO modele (id_marka, model, opis, utworzyl)');
      Add('VALUES (:id_marka, :model, :opis, :utworzyl)');
      ParamByName('id_marka').AsInteger := id_marka;
      ParamByName('model').AsString := edtnazwa.Text;
      ParamByName('opis').AsString := mmoopis.Text;
      ParamByName('utworzyl').AsString := main.uzytkownik;
      ExecSQL;
    end;

/ciach ten koszmar.../
Dlaczego koszmar?
To chyba jasne, nakład pracy na utrzymanie tego potworka musi być znaczny.
Poza tym - boziu, jak ty obsługujesz błędy? Naprawdę pokazujesz wyjątek w ShowMessage?

Jak napisałem - jakoś to działa ale powiem szczerze że wyjątkowo mi się nie podoba. Po pierwsze te ify po drugie wszystko jest ok gdy pól edycyjnych jest nie więcej niż paręnaście (właśnie mam na pulpicie program w którym jest prawie 200 checkbox-ów).

200 checkboxów?
Nieważne co to jest, ale to zdecydowanie porażka.
Miałem do czyniania dawno temu z pewnym ERPem, który miał takie UI.
To jest nie do używania.

Podzielcie się pomysłem jak to zrobić bardziej zgrabnie, może jakaś uniwersalna klasa która sama "wykryje" wszystkie pola edycyjne i zapamięta ich stan, następnie porówna przed opuszczeniem formy czy coś się zmieniło i zapisze zmiany? W taki powiedzmy sprytniejszy sposób ustawiam stan kontrolek na tej 200 elementowej formie:

  for i := 0 to ComponentCount - 1 do
  begin
    if components[i] is TCheckBox then
      TCheckBox(Components[i]).Checked := False;

    if components[i] is TEdit then
      TEdit(Components[i]).Text := '';
  end;

Zupełnie nie tak!
Piszesz o historii zmian danych, to co tu robią kontrolki?
Po co i dlaczego?
Zakłam, że używasz DataSetów (zresztą pełno ich w przykładowym kodzie, to pewnie są) i kontrolek dbAware (czyli DataSet->DataSource->Kontrolka) - nie LiveBindings.
Skoro tak, to dlaczego nie śledzisz zmian na poziomie zmiany danych?
To jest stosunkowo proste...
Zauważ, ze kontrolka musi się łączyć z datasetem przez TDataSource.
A TDataSource ma ciekawe zdarzenie TDataSource.OnDataChange
I tam w parametrze Field dostaniesz referencję na pole, w którym właśnie zmieniły się dane.
Uważaj, bo to zdarzenie rzuca się często i w różnych przypadkach - np. zmiana aktywnego rekordu.
W takim przypadku parametr Field będzie równy nil.
Musisz potestować.

Pozostaje zabranie tych danych, zapisanie do lokalnej struktury i jeśli user potwierdzi zmiany - zapis zmian do logu.

Robiłem kiedyś coś podobnego w oparciu o triggery w bazie danych.
Sprowadziło się to do napisania generatora triggerów, czyli miałem jakieś GUI w którym mogłem wyklikać co dokładnie chcę śledzić z dokładnością do tabeli i pola.
Gdybyś używał FireDAC'a, to są jeszcze inne sposoby, niektóre na tyle ciekawe, że da się doprowadzić do stanu, aby program pokazał stan dowolnego datasetu z dowonego punktu w historii zmian.
Innymi słowy - możemy sobie np. zobaczyć jak wyglądał jakiś konkretny dokument w danym punkcie czasu.
Ale to oczywiście trochę skomplikowane...

A może jest jakiś gotowiec do tego a ja go nie znam?

Pewnie by się znalazł.. Ale to chyba nie o tu tu chodzi :)
Nie jestem do końca pewny, ale chyba Context Database Extensions pozwalał na coś takiego.

0

odpowiadając @wloochacz

dzięki za dołączenie do dyskusji, odpowiadam na twoje pytania i sugestie:

Może pokaż jak ten log wygląda?

Log w bazie łatwo sobie wyobrazić - zwykły rekord ale na pewno nie o to pytałeś. Jeśli chodzi o użyteczność to pomysł zaczerpnąłem z jednego z programów na rynku. Wydaje mi się że całkiem dobrze się sprawdza.
Polega to na tym że mamy podgląd operacji dla wybranego rekordu, czyli nawet w ekstremalnych sytuacjach ile razy można edytować ten sam rekord? 10 - 20 - 50? To i tak jest później do ogarnięcia.
Przykładowo wygląda to tak:
screenshot-20190315123232.png

Oczywiście, historia operacji dostępna jest tylko dla użytkownika o odpowiednich uprawnieniach ale to nie ma znaczenia.

Ale jak byśmy chcieli coś takiego parsować, bo trzeba zrobić cokolwiek z przetwarzaniem takich danych (np. wyszukiwanie) to jest słabo.
Wolałbym trzymać te dane w jakiś strukturyzowany sposób (relacyjny, XML albo JSON), byleby nie czystym tekstem.

Jak pisałem, nie ma potrzeby pracować na tych danych. Wyszukanie jest proste, prawy klawisz myszy na rekordzie i wszystko widać. No może chwila przewijania.

Poza tym - boziu, jak ty obsługujesz błędy? Naprawdę pokazujesz wyjątek w ShowMessage?

Wiem że istnieje inny sposób ale uwierz mi że zamiast wielkiego komunikatu z mnóstwem informacji o numerze wyjątku itp. znacznie lepiej jest powiadomić klienta że coś po prostu się nie udało. To akurat rozwiązanie jest dla mechaników samochodowych dla których przeczytanie komunikatu dłuższego niż jedno zdanie po polsku graniczy z cudem - nie obrażając nikogo.

Mam klienta u którego jest jakaś optima i czasami jak wyskakuje błąd dzwonią do mnie że coś poszło nie tak. Proszę o odczytanie komunikatu a oni mi mówią że już kliknęli i zamknęli komunikat zanim go przeczytali ale na pewno jest coś nie tak. Powód - po prostu komunikat jest zbyt duży.

200 checkboxów?

Tutaj nie będę dyskutował, takie jest zlecenie, tak było w przypadku ręcznej rejestracji zleceń i tak ma być w programie. Chodzi o to że mechanicy chcą po prostu wyklinać wszystko co mają zrobić na jednej formie. Na szczęście tylko kilka pól podlega jakiejkolwiek walidacji.

Nieważne co to jest, ale to zdecydowanie porażka.

Jak pisałem, nie moja decyzja - klient jest zadowolony i to jest najważniejsze.

  for i := 0 to ComponentCount - 1 do
  begin
    if components[i] is TCheckBox then
      TCheckBox(Components[i]).Checked := False;

    if components[i] is TEdit then
      TEdit(Components[i]).Text := '';
  end;

Zupełnie nie tak!
Piszesz o historii zmian danych, to co tu robią kontrolki?
Po co i dlaczego?

Ja chyba po prostu źle zacząłem. Niepotrzebnie wtrąciłem o bazach, kontrolkach, wyjątkach itp.

Jakbym miał zacząć od nowa to napisał bym temat: Jak rejestrować zmiany na kontrolkach. Bo właśnie to mnie interesuje a że później zapisuję to do bazy to już rzecz wtórna.

Zakłam, że używasz DataSetów (zresztą pełno ich w przykładowym kodzie, to pewnie są) i kontrolek dbAware (czyli DataSet->DataSource->Kontrolka) - nie LiveBindings.
Skoro tak, to dlaczego nie śledzisz zmian na poziomie zmiany danych?

Jak powyżej, interesuje mnie zmiany na kontrolkach. Zapisuję nawet że klient wszedł w edycję ale nic nie zmienił - taka trochę totalna inwigilacja.

To jest stosunkowo proste...
Zauważ, ze kontrolka musi się łączyć z datasetem przez TDataSource.
A TDataSource ma ciekawe zdarzenie TDataSource.OnDataChange
I tam w parametrze Field dostaniesz referencję na pole, w którym właśnie zmieniły się dane.
Uważaj, bo to zdarzenie rzuca się często i w różnych przypadkach - np. zmiana aktywnego rekordu.
W takim przypadku parametr Field będzie równy nil.
Musisz potestować.

Dzięki za sugestię, na pewno zerknę na to. Czuję że może mi się w przyszłości przydać.

Podsumowując:
Szukam prostego sposobu na zapisanie co się zmieniło na formie od momentu OnShow do OnClose (czy tam OnHide)?

1

Chyba szukasz rozwiązania o które pytałem jakiś czas temu w tym wątku
Raportowanie interakcji użytkownika.
Możesz sprawdzić proponowane rozwiązania, ja niestety odłożyłem temat i pewnie koło wakacji będe do tego wracał.

0
robertz68 napisał(a):

Piszesz o historii zmian danych, to co tu robią kontrolki?
Po co i dlaczego?

Ja chyba po prostu źle zacząłem. Niepotrzebnie wtrąciłem o bazach, kontrolkach, wyjątkach itp.

Jakbym miał zacząć od nowa to napisał bym temat: Jak rejestrować zmiany na kontrolkach. Bo właśnie to mnie interesuje a że później zapisuję to do bazy to już rzecz wtórna.

Nie zrozumiałeś mnie.
Nie pisałem o tym, jak zapisać dane do bazy, tylko jak śledzić zmianę wartości na warstwie danych.
Nie na kontrolce, bo ta co do zasady ma zapewnić interakcję z użytkownikiem.
Poza tym, zawsze możesz wymienić kontrolkę, a wtedy...

Jak rozumiem, w tym zrzucie ekranowym (pok oknem z informacją o logu), widzimy jakiegoś DBGrida, który jest podpięty przez TDataSource do TDataSet?
Jeżeli tak, to po kiego interesują cię kontrolki?

0
robertz68 napisał(a):

Poza tym - boziu, jak ty obsługujesz błędy? Naprawdę pokazujesz wyjątek w ShowMessage?

Wiem że istnieje inny sposób ale uwierz mi że zamiast wielkiego komunikatu z mnóstwem informacji o numerze wyjątku itp. znacznie lepiej jest powiadomić klienta że coś po prostu się nie udało. To akurat rozwiązanie jest dla mechaników samochodowych dla których przeczytanie komunikatu dłuższego niż jedno zdanie po polsku graniczy z cudem - nie obrażając nikogo.

Mam klienta u którego jest jakaś optima i czasami jak wyskakuje błąd dzwonią do mnie że coś poszło nie tak. Proszę o odczytanie komunikatu a oni mi mówią że już kliknęli i zamknęli komunikat zanim go przeczytali ale na pewno jest coś nie tak. Powód - po prostu komunikat jest zbyt duży.

Zupełnie nie o to chodzi...
Powiedz co tobie, jako programiście odpowiedzialnym za ten system, daje informacja o tym, że coś się nie udało?
Niewiele, a praktycznie nic.
Mi chodziło raczej o jakiś mechanizm łapania błędów, jak. EurekaLog, MadExcept czy inne.

Ja np. używam Slacka do monitorowania serwerów on-line u klientów. Ale to jest informacja dla nas, nie dla klienta.
I też dostaję info o tym, ze coś się nie udało.
a wygląda to np. tak (no fuckup normalnie :D):
dfSlack.png

0

Ja mam rozwiązane logowanie na dwa sposoby, jeden podobny jak podał autor tego wątku. Natomiast drugi to mam pozakładane „trigery” na bd oracle które to logują zmiany do tablicy audytującej.
Postaram się później zamieścić przykład.
Oczywiści logowanie konfigurowane na parametr.

0

No i zapomniałem o ważnej sprawie która na pewno mocno zmieni sytuację.

Od dość dawna pisząc aplikację nie używam kontrolek bazodanowych (poza DBGridami). Wszystko inne robię na kontrolkach zwykłych. Dlaczego zapytacie? A no dlatego że jak zaczynałem pisać swoje pierwsze programiki lata temu to nawet nie słyszałem o transakcjach itp. Powodowało to że otwarcie formy edycyjnej z tym samych rekordem na dwóch stanowiskach robiło wielki bałagan.
Teraz po prostu pobierając rekord do edycji robię kopię jego danych w zwykłych kontrolkach edycyjnych i pracuję na nimi jak długo chcę.
Inny użytkownik także może pobrać ten rekord do edycji (mogę go powiadamiać że już gdzieś jest edytowany lub nie, w zależności czy tego potrzebuję czy nie). W każdym razie każdy z nich może zapisać swoją kopię danych a obowiązywać będzie i tak ostatnia zmiana. Oczywiście, historia zapisana w logu bardzo wtedy się przydaje.

Tutaj uwaga, ten sposób nie do końca się sprawdza z tabelami podrzędnymi. Tam po prostu blokuję edycję już otwartego dokumentu. Pozwalam tylko na podgląd.

Uwaga dodatkowa, nigdy nic nie usuwam z bazy, rekordy usunięte po prostu oznaczam jako usunięte ale je pozostawiam. Tak więc nawet otwarcie rekordu na jakimś stanowisku i trzymanie takiej formy przez kilka dni nic nie zmieni nawet gdy ktoś inny go w tym czasie "niby" usunie. Użytkownik zapisze swoją kopię bez żadnego wyjątku, tylko że już go nie zobaczy w gridzie po odświeżeniu danych.

Takie rozwiązanie jest bardzo bezpieczne w moim mniemaniu dla danych (w końcu tylko przez chwilę wykonuję na nich operacje zapisu) ale kosztuje trochę pracy niestety. Już się do tego przyzwyczaiłem ale nie mogę przeżyć tej historii operacji że zajmuje mi tyle pracy.
Dlatego właśnie proszę o pomoc.

1
robertz68 napisał(a):

No i zapomniałem o ważnej sprawie która na pewno mocno zmieni sytuację.

Od dość dawna pisząc aplikację nie używam kontrolek bazodanowych (poza DBGridami). Wszystko inne robię na kontrolkach zwykłych. Dlaczego zapytacie? A no dlatego że jak zaczynałem pisać swoje pierwsze programiki lata temu to nawet nie słyszałem o transakcjach itp.

No to ja się wypisuję.
Do drutem wiązanych problemów, nadają się tylko drutem wiązane rozwiązania.

Ale tak sobie to czytam i...
Powiedz, Ty nigdy nie używałeś CachedUpdates?
Wiesz co to jest i do czego służy?
Gdybyś wiedział, to raczej nie zrobiłbyś tego tak, jak to zrobiłeś...

Powodowało to że otwarcie formy edycyjnej z tym samych rekordem na dwóch stanowiskach robiło wielki bałagan.

Nie do pojęcia, jak to możliwe?

Teraz po prostu pobierając rekord do edycji robię kopię jego danych w zwykłych kontrolkach edycyjnych i pracuję na nimi jak długo chcę.

A na Datasetach nie możesz?
Oczywiście, ze możesz.
Możesz pracować całkowicie w trybie bez połączenia z bazą danych.
Ba, możesz mieć nawet undo/redo.

Inny użytkownik także może pobrać ten rekord do edycji (mogę go powiadamiać że już gdzieś jest edytowany lub nie, w zależności czy tego potrzebuję czy nie). W każdym razie każdy z nich może zapisać swoją kopię danych a obowiązywać będzie i tak ostatnia zmiana. Oczywiście, historia zapisana w logu bardzo wtedy się przydaje.

To jest, oczywiście, standardowe podejście z blokadami optymistycznymi. Czyli ten lepszy, kto ostatni.
Ale to nie jest dobre podejście dla wielodostępu do edycji danych.
Chyba nie trzeba tłumaczyć dlaczego?

Tutaj uwaga, ten sposób nie do końca się sprawdza z tabelami podrzędnymi. Tam po prostu blokuję edycję już otwartego dokumentu. Pozwalam tylko na podgląd.

Moim zdaniem, to się w ogóle nie sprawdza, jeśli do tej samej instancji danych, może mieć dostęp wielu użytkowników w tym samym czasie, którzy chcą ją zmienić.
Czy to są tabele podrzędne, to bez znaczenia.

Uwaga dodatkowa, nigdy nic nie usuwam z bazy, rekordy usunięte po prostu oznaczam jako usunięte ale je pozostawiam. Tak więc nawet otwarcie rekordu na jakimś stanowisku i trzymanie takiej formy przez kilka dni nic nie zmieni nawet gdy ktoś inny go w tym czasie "niby" usunie. Użytkownik zapisze swoją kopię bez żadnego wyjątku, tylko że już go nie zobaczy w gridzie po odświeżeniu danych.

Nigdy nie rozumiałem takiego podejścia, ale OK.
Ja pozwalam na usuwanie danych, ale tylko takich, które z punktu widzenia logiki biznesowej nie mają (najczęściej - jeszcze nie mają) żadnego znaczenia.

Takie rozwiązanie jest bardzo bezpieczne w moim mniemaniu dla danych (w końcu tylko przez chwilę wykonuję na nich operacje zapisu) ale kosztuje trochę pracy niestety. Już się do tego przyzwyczaiłem ale nie mogę przeżyć tej historii operacji że zajmuje mi tyle pracy.
Dlatego właśnie proszę o pomoc.

To nie jest ani wygodne, ani jakieś tam bezpieczne.
Tak naprawdę zrobiłeś sobie kawał roboty zupełnie niepotrzebnie.

A teraz inaczej - pokaż proszę jak uzupełniasz te kontrolki wartościami z bazy?
Ale jeśli po prostu czytasz dane z datasetu i mozolnie jest przepisujesz do poszczególnych kontrolek, to jak dla mnie nic nie musisz pokazać.
Z takim "patentem" da się zrobić dwie rzeczy:

  1. Wywalić to w cholerę, bo jest to istny horror-szoł.
  2. Dalej drutować w podobny sposób...
0

Cóż mam powiedzieć. Robię wszystko dokładnie w taki sposób jaki zganiłeś w poście powyżej. Nie mogę nawet nic napisać na swoją obronę no może poza tym że sam do tego kiedyś doszedłem i wyrobiłem sobie pewne reguły, ale jak widać nie zrobiłem tego zgodnie z zasadami.

Mógłbym poprosić o jakieś sugestie, jak robić to prawidłowo ale ... chyba już sobie odpuszczę - nie mam siły :).
W każdym razie dziękuję za to co napisałeś.

1
robertz68 napisał(a):

Cóż mam powiedzieć. Robię wszystko dokładnie w taki sposób jaki zganiłeś w poście powyżej.

Nie mogę nawet nic napisać na swoją obronę no może poza tym że sam do tego kiedyś doszedłem i wyrobiłem sobie pewne reguły, ale jak widać nie zrobiłem tego zgodnie z zasadami.

Mógłbym poprosić o jakieś sugestie, jak robić to prawidłowo ale ... chyba już sobie odpuszczę - nie mam siły :).

A ja się dopiero rozkręcałem... ;-)

W każdym razie dziękuję za to co napisałeś.

A proszę bardzo. Ale i tak napiszę co nieco od siebie...
Na dzień dzisiejszy masz dwa wyjścia:

  1. Stare (dobre?) podejście Delphi oparte o Datasety. Naprawdę da się pisać apki bazo-danowe szybko i w miarę wygodnie. Wszelkiej maści DataEntry czy CRUDy wręcz legendarnie ekspresowo.
  2. Nowe podejście, oparte o Live Bindings. Wymaga nowego Delphi (gdzie nowe, to powiedzmy nie starsze jak XE7. Co prawda LiveBindings jest od wersji XE2, ale...).

ad. 1
To jest stosunkowo proste do nauczenia i efektywnego używania. Problem polega na dwóch sprawach:
a) Jest bardzo, ale to od groma, materiałów na ten temat. Tylko, że absolutnie zdecydowana większość z nich pokazuje jak zrobić potworki (jakieś komponenty niewizualne na jakiś krzywych DataModule... Zupełnie bez sensu. Ale to taki RAD-way...) tj. szybko i przyjemnie wyklikane rozwiązanie, które działa.
b) Niestety takie rozwiązania nie nadają się dla aplikacji, które działają na skomplikowanym modelu. Inaczej - w aplikacjach napisanych w ten sposób, najczęściej, w ogóle nie można mówić o tym, że model istnieje. Rozwój takiego oprogramowania polega na dodawaniu kolejnych funkcjonalności... Niestety to się prędzej czy później źle kończy.
Poza tym, niespecjalnie wygodnie (doprecyzuje - niespecjalnie wygodnie to jest przerośnięty eufemizm w tym przypadku) się utrzymuje i rozwija nowe wymagania w takiej aplikacji.

Tak czy siak, jeśli to standardowa aplikacja klient-serwer z niespecjalnie fikuśnym modelem danych, to oczywiście bez problemu da radę. poza tym, w stosunku do tego jak ty to robisz - to kosmos wygoda! :)

ad. 2)
Rozwiązanie oparte o LiveBindings, które umożliwiają binding (wiązanie) dwukierunkowe typu obiekt.właściwość <--> obiekt.właściwość. To znaczy (w skrócie), że mając wiązanie pomiędzy kontrolką (np. TEdit) a właściwością obiektu, ten edit automatycznie reaguje na zmiany wartości w obiekcie, a obiekt reaguje na zmianę wartości w kontrolce. Zresztą, identycznie to działa przy kontrolkach dbAware (czyli TDBEdit->DataSource->TDataSet), z tym że wiązanie jest tylko i wyłącznie do TDataSet.
Oczywiście istnieje też możliwość wiązania kontrolek z TDataSet, nie tylko z obiektem.

Tego typu rozwiązanie pokazuje siłę, ale dopiero wtedy, gdy zaprzęgniemy do pracy ORMa (np. TMS Aurelius). Niestety Embercadero jakby "zapominało" tym fakcie i nic podobnego do ORMa nie istnieje w standardzie.

Problem polega na tym, że to zdecydowanie bardziej złożone od zwykłych DataSetów. A zatem krzywa wejścia i uczenia się jest zdecydowanie bardziej stroma.
Ale daje pełne wsparcie dla OOP, szalenie wygodnie się koduje i stosunkowo łatwo napisać skomplikowaną reaktywną logikę.
Różnica pomiędzy tymi podejściami jest taka, że teoretycznie na DataSetach też da się osiągnąć podobne efekty w zakresie przetwarzania skomplikowanego modelu. Ale koszt ich wprowadzenie i utrzymania jest znacznie wyższy, poza tym musimy liczyć się z większą liczbą błędów.

Posłużę się takim wykresem:
Delphidb.png

Proszę traktować to jako umowny przykład, a nie wynik jakiś tam badań. Jak ten wykres będzie wyglądał w danym projekcie, to po prostu zależy od klasy problemu, który projekt ma rozwiązać.
Ale upraszczając, można powiedzieć że nie ma co się pchać w modelowanie OOP dla aplikacji bazodanowych, kiedy wiemy że ta aplikacja jest stosunkowo prosta pod katem modelu.
I odwrotnie, jeśli mamy model, który:

  • jest powodem tworzenia projektu
  • jest sercem systemu i jego największą wartością
  • a modelowanie jest największym wyzwaniem w danym projekcie
    To zdecydowanie warto przysiąść i zrobić to dobrze.

I jeszcze jedno; baza danych nie jest modelem, ponieważ reprezentuje statyczny schemat, który w żadne sposób nie odzwierciedla logiki zachowania i działania danego systemu. Baza danych, de-facto, przechowuje stan obiektów biznesowych.

0

@wloochacz czy była by szansa abyś zamieścił proste przykłady jak Ty stosujesz w praktyce wyżej opisane przez Ciebie zagadnienia ?
Chylę czoła za wiedzę jaką posiadasz.

0

Ciężko mi cokolwiek teraz napisać.
Oczywiście pisząc że sobie odpuszczę nie byłem szczery i od razu zacząłem szukać wiedzy w necie. Nie jest to jednak takie proste. Opis jakiś sposobów użycia pewnych technologii można znaleźć w internecie ale nie otrzymam tam informacji o tym co jest dla mnie kluczowe i czego być może dowiem się tutaj dzięki dobrym ludziom.
A chodzi o to, jaką technologię wybrać do aplikacji które najczęściej piszę. Są to zazwyczaj proste programy bazodanowe, korzystające z bazy MS SQL lub Firebird, najczęściej jest to kilkanaście tabel, maksymalnie kilkanaście tysięcy rekordów. Jest trochę relacji i to wszystko.
Większość moich programów to coś w rodzaju katalogów, a to części elektronicznych, a to pracowników i ich identyfikatorów drukowanych na drukarkach kart.
Takie proste programiki.

Aktualnie jak już wiadomo jakich rozwiązań używam, chciałbym to zmienić. Nie wiem czy od razu przyjrzeć się bindowaniu bo widzę że mocno zachwalasz to rozwiązanie (unikałem tego jak ognia bo myślałem że to jakaś fanaberia o której za chwilę wszyscy zapomną), czy może jednak na razie pomyśleć o cachedupdate, ponieważ będzie to chyba dla mnie prostsze do moich prostych aplikacji a i widzę pewne ułatwienie w pisaniu programów.

Poradzisz coś?

0
robertz68 napisał(a):

Ciężko mi cokolwiek teraz napisać.
Oczywiście pisząc że sobie odpuszczę nie byłem szczery i od razu zacząłem szukać wiedzy w necie. Nie jest to jednak takie proste. Opis jakiś sposobów użycia pewnych technologii można znaleźć w internecie ale nie otrzymam tam informacji o tym co jest dla mnie kluczowe i czego być może dowiem się tutaj dzięki dobrym ludziom.
A chodzi o to, jaką technologię wybrać do aplikacji które najczęściej piszę. Są to zazwyczaj proste programy bazodanowe, korzystające z bazy MS SQL lub Firebird, najczęściej jest to kilkanaście tabel, maksymalnie kilkanaście tysięcy rekordów. Jest trochę relacji i to wszystko.
Większość moich programów to coś w rodzaju katalogów, a to części elektronicznych, a to pracowników i ich identyfikatorów drukowanych na drukarkach kart.
Takie proste programiki.

To są kluczowe informacje i bardzo dobrze, że to napisałeś.
Nie chodzi o to przecież aby kogoś/coś deprecjonować, że robi to czy tamto.
Ale możemy pojechać za to, jak to robi :D

Aktualnie jak już wiadomo jakich rozwiązań używam, chciałbym to zmienić. Nie wiem czy od razu przyjrzeć się bindowaniu bo widzę że mocno zachwalasz to rozwiązanie (unikałem tego jak ognia bo myślałem że to jakaś fanaberia o której za chwilę wszyscy zapomną), czy może jednak na razie pomyśleć o cachedupdate, ponieważ będzie to chyba dla mnie prostsze do moich prostych aplikacji a i widzę pewne ułatwienie w pisaniu programów.

Poradzisz coś?

Z radością! ;-)
W Twoim przypadku, zdecydowanie DataSety.
Wdrożysz się szybko i będziesz Pan zadowolony, ale... to też trochę zależy :D
ChechedUpdates jest bardzo dobre, ale jego jakość zależy tak naprawdę od biblioteki (a więc inaczej to działa w ADO, FireDAC, ZEOS, itd.) której używasz. Po prostu ten element nie jest zaimplementowany na poziomie TDataSet, a każda biblioteka przynosi własną jej implementację.

Oczywiście nie znam wszystkiego, ale to co znam to mogę się wypowiedzieć.

  1. **FireDAC **(dawniej AnyDAC) - chyba zdecydowanie the best. Ciężko znaleźć obszar (oczywiście mówimy o DataSetach...) w którym FireDAC nie jest co najmniej w czubie. A sama obsługa CachedUdpates jest niedoścignionym wzorem dla innych. Ale to wynika architektury tej biblioteki. Jeśli używasz zamiennie czy równocześnie MSSQL/Firebird, to zdecydowanie polecam.
  2. **ADO **- największa męczarnia z tym była. Słaba implementacja w standardzie Delphi, można by rzec, że podstawowa. Co do zasady ADO samo z siebie złe nie jest, ale ADO dostarczone w Delphi (jako dbGO) jest po prostu słabe.
  3. Używałem kiedyś jeszcze **FIBPlus **(tylko dla IB/Firebird). To był krzywo napisany kod, który działał wyśmienicie :) Niestety, chyba już nie żyje.

Miałem jeszcze incydentalne spotkania z IBX (nie polecam) i SDAC (polecam).
Testowałem też kiedyś ZEOS'a, ale to było bardzo dawno temu i nie ma co o tym pisać Wiem tylko, że nowsze wersje są na pewno szybsze, ale jak z jego funkcjonalnością, to po prostu nie wiem.

Łatwiej by też było coś doradzić, gdybyś powiedział jakiego Delphi używasz i czego używasz do baz danych?

0
PrzemysławWiśniewski napisał(a):

@wloochacz czy była by szansa abyś zamieścił proste przykłady jak Ty stosujesz w praktyce wyżej opisane przez Ciebie zagadnienia ?

W sumie nie wiem co Ci powiedzieć... Konkret by się przydał, to znaczy - jakbyś opisał swój problem, albo zdefiniował dziedzinę problemu, to łatwiej by było coś pokazać czy wyjaśnić.
Już nie raz coś tam opisywałem i pokazywałem...
Pierwsza część postu jest w miarę "prosta" (do używania), druga była nawet propozycją - ale bez odzewu.
Ale ja naprawdę nie mam czasu, aby pisać elaboraty.
Także bardzo proszę - chcesz o czymś porozmawiać, spoko - ale określ granice ;)
Poza tym, jak piałem wcześniej, bardzo wiele zależy od biblioteki, której używamy.
No chyba, że skupimy się na ogólnych zagadnieniach, ale... mam trudność, bo nie rozumiem co tam jest niejasnego czy na tyle ciekawego, aby to omawiać?
Zatem zadaj konkretne pytanie, a postaram się ustosunkować.

Chylę czoła za wiedzę jaką posiadasz.

Dzięki.

0

Przyznam że ja do nauki programowania Delphi nie maiłem dobrych wzorców zostałem wdrożony do utrzymania i dewelopeki istniejącego już od wielu lat systemu informatycznego i tak naprawdę przekwalifikowałem się po blisko 5 latach pracy w automatyce i plc.
System jaki zastałem w obecnej firmie był rozwijany prawdopodobnie od Delphi 4.
Kod pod zdarzeniami przycisków to była normalna rzecz. Za mojej kadencji z BDE przeszliśmy przez ADO do Firedac-a spora ilość kodu została przeniesiona pod metody prywatne/publiczne do sekcji Formatek i DataModule. Niestety nadal nie czuję aby to wyglądało tak jak wyglądać powinno :/
Byłem już kilka razy na seminariach jakie prowadził Bogdan Polak, jedno z nich było w temacie programowania SOLID. Byłem trochę zawiedziony tymi wykładami bo nie zobaczyłem praktycznego zastosowania w kodzie. Tak mi już chyba pozostało że najlepiej łapę na przykładach kodu. Fajnie by było jak byś zamieścił przykład jak powinno się budować aplikację od podstaw, pierwsza formatka – tylko z kodu … czy np. z „dizajnera” np. dwa pola login-hasło przycisk
walidacja wprowadzonych danych czy „zainsertowanie” jakiś danych do bazy i wyświetlenia ich na formatce. Jak to zrobić typowo obiektowo z zasadami SOLID.

0

@wloochacz: ależ idealnie się składa bo po kilku latach pracy przy BDE (później chwilę ADO) przeszedłem całkowicie na FireDAC - jak @PrzemysławWiśniewski . Aktualnie mam dostęp do Delphi XE8 Ent (firmowe) i oczywiście 10.3 Community. Jak mogę to używam Community, chyba że chcę połączyć się z MS SQL to muszę przejść na XE8 bo niestety nie ma drivera FireDAC do MS SQL w Community - ale to zapewne wszyscy wiedzą.

Wracając do projektu, myślę że korzystając z twojej pomocy utworzę bardzo prosty projekt do nauki / zabawy, będzie to książka adresowa.
Podstawowe założenia:

  • aplikacja sieciowa w technologii klient-serwer z bazą na serwerze MS SQL,
  • dostęp do bazy bezpośrednio, nie przez rest-a czy soap,
  • oczywiście DataSet-y bo tak się umówiliśmy,
  • główna forma będzie się składała z grida ze wszystkimi rekordami w bazie,
  • pod gridem dwa buttony do pokazania nowej formy na której będzie można utworzyć / edytować dane. Dzięki komponentom bazodanowym chcę zrobić to na jednej formie a nie na dwóch osobnych jak to robiłem zwykle - mam nadzieje że nie jest to niezgodne z zasadami?.

baza danych to dwie tabele. Pierwsza to **uzytkownicy **w której są dwie kolumny:

  • [id] typu [int] IDENTITY(1,1) NOT NULL,
  • [uzytkownik] typu varchar NULL,

Druga tabela to **adresy **w której są następujące kolumny:

  • [id] [int] IDENTITY(1,1) NOT NULL,
  • [nazwa] varchar NULL,
  • [ulica] varchar NULL,
  • [kod_poczt] varchar NULL,
  • [miejscowosc] varchar NULL,
  • [id_uzytkownik] [int] NOT NULL,
  • [wspolny] [bit] NOT NULL,

No dobrze, tworzę formę główną, na niej grid.
screenshot-20190315221741.png

Po naciśnięciu na btnNowy pokaże się nowa forma
screenshot-20190315222547.png

Żeby nic nie mieszać już teraz (bo mam przecież pełno moich pomysłów) zapytam jak to zrobić porządnie aby:

  • forma pokazała się z pustymi polami (normalnie pojawią się wartości z aktualnego rekordu),
  • pod combobox-a była podłączona kolumna uzytkownik z tabeli uzytkownicy,
  • w czasie dodawania rekordu do bazy powinna zadziałać auto inkrementacja na kolumnie id,
  • do tabeli adresy wpisujemy id uzytkownika (z tabeli uzytkownicy) wybranego w combobox-ie a nie jego imię i nazwisko

To tak na początek.

1

uff, wrzuciłem zarys projektu na githuba https://github.com/RobertZawadzki/adresy

Zapraszam do zabawy

0
robertz68 napisał(a):

uff, wrzuciłem zarys projektu na githuba https://github.com/RobertZawadzki/adresy

Zapraszam do zabawy

Jakby to kogoś interesowało, to coś tam gmeram i na pewno coś tam będzie.
Niestety nie będzie to szybko, ponieważ mam huk innej roboty, ale...
Skoro to FireDAC, to poszalejmy w kontekście sprytnego mechanizmu zmian na formie "na formie".
Ale nie będzie to forma, tylko śledzenie zmian na poziomie gdzie te zmiany są wykonywane, czyli na pewno nie na formie ;-)

1 użytkowników online, w tym zalogowanych: 0, gości: 1