modifiedField() a konzistence databáze

V Expense Management modulu jsem narazil na jednu dá se říct klasickou chybu v metodě modifiedField(). Je tedy zřejmě vhodné na tento typ chyb upozornit.

Jak asi víte, metoda modifiedField() je umístěná na tabulce a je volána formulářem (nebo datasetem) při změně jakéhokoli pole. Jejím parametrem je ID pole, takže je velmi snadné psát kód reagující na změnu konkrétního pole. Je to dokonce až tak snadné, že se často metoda modifiedField() používá i tehdy, kdy by se používat neměla.

Typický (chybový) scénář vypadá takto: “Změní-li se hodnota pole X v tabulce A, aktualizuj pole Y v B na stejnou hodnotu. V modifiedField() na A tedy zavolám update B.”

Co je špatně? Tabulka B je v databázi aktualizována, ale tabulka A ještě ne. A třeba ani nikdy nebude. Uživatel může odejít z formuláře bez uložení záznamu, dojde k výpadku sítě atd. Intergrita databáze je narušena a následky nesprávných dat mohou být… všelijaké.

Původ těchto chyb je v podstatě nemožné vysledovat v databázi. Na druhou stranu je to většinou snadná kořist pro revizi kódu.

Správný postup je používat modifiedField() pro aktualizaci měněného záznamu (vyplnění souvisejích polí, přepočty), ale veškeré změny v databázi musí být v transakci s ukládáním záznamu. I v update() lze volat určitý kód jen při změně konkrétního pole (pomocí podmínky buffer.Field != buffer.orig().Field).

3 Comments

  1. Bylo by možné vytvořit krátkou ukázku zdrojového kódu, jak by to mělo správně být?

    • Martin Dráb

      The Real Person!

      Author Martin Dráb acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.

      Nemám momentálně po ruce AX, ale nějak to zkusím.

      Řekněme, že původní řešení vypadá nějak takto:

      public void modifiedField(FieldId _fieldId)
      {
          switch (_fieldId)
          {
              case fieldNum(Table1, ProjId):
                  this.projIdChanged();
                  break;
          }
      }
      private void projIdChanged()
      {
          ttsbegin;
          Table2 t2 = this.table2(true); //nějaká logika vybírající související záznam v Table2 pro update
          t2.ProjId = this.ProjId;
          t2.update();
          ttscommit;
      }

      Pokud se v tabulce změní pole ProjId, je ihned zavolána metoda projIdChanged(), která aktualizuje ProjId i v související tabulce. Mohla by tam být i komplexnější logika, ale nechme příklad jednoduchý.

      Jak jsem zmínil v článku, při volání modifiedField() není původní tabulka uložená a po updatu Table2 už je databáze nekonzistentní. Do konzistentního stavu se může dostat později, ale také nemusí.

      Teď k řešení. Odeberme z modifiedField() veškerou logiku aktualizující jiné tabulky (naopak třeba aktualizaci jména projektu ve stejném bufferu by samozřejmě byla v pořádku), metodu projIdChanged() ponechme.

      Aktualizace Table2 musí proběhnout společně s aktualizací Table1, tudíž to musí být v jedné transakci. Implementujme update() následovně:

      update()
      {
          boolean projIdChanged = (this.ProjId != this.orig().ProjId);
       
          ttsbegin;    
       
          super();
       
          if (projIdChanged )
          {
              this.ProjIdChanged();
          }
       
          ttscommit;
      }

      Důležité je, že podmínka (this.ProjId != this.orig().ProjId) musí být před super() – po uložení jsou v orig() stejné hodnoty jako v bufferu samotném.

      Snad je to teď jasnější, kdyžtak se ještě zeptejte.

Comments are closed.