18. 8. 2021, Vladimír Klaus, navštíveno 105x

Delphi
SQL
SQL Server

Nedávno jsem v jednom projektu přidal funkci mazání aktuální položky v mřížce Tasky. Tedy zdánlivě zcela běžnou záležitost, která se ale záhy ukázala jako hodně problematická. Smazání položky probíhalo přes funkci mřížky TaskyGridView.DataController.DeleteSelection, což vede na volání Query.Delete, tedy opět zcela běžnou a hojně používanou funkci. Specialitou tohoto případu byl ale dotaz využívající spojení dvou tabulek přes INNER JOIN.

Očekával jsem, že dojde k odstranění aktuálního Tasku, ale cílový uživatel viděl mnohem katastrofálnější chování:

  • byl odstraněn aktuální task
  • byla odstraněna zakázka, pod kterou task patří
  • a byly odstraněny i všechny další tasky dané zakázky

Zde je malý příklad, který ukazuje problematické chování!

MyQuery:=TADOQuery.Create(nil);
try
  MyQuery.Connection:=ADOConnection;
  MyQuery.SQL.Text:=
    'SELECT Tasky.*, Zakazky.* FROM Tasky ' +
    'INNER JOIN Zakazky ' +
    'ON Tasky.IdZakazky=Zakazky.ID ' +
    'WHERE Tasky.ID=1234 ';
  MyQuery.Open;
  if MyQuery.RecordCount=1 then MyQuery.Delete; //!!! Smaže záznamy z obou tabulek

finally
  FreeAndNil(MyQuery);
end;

Následně se ukázalo, že byl odstraněn aktuální task a odpovídající zakázka. Další tasky odstraněny nebyly, jen se staly nepřístupnými, protože zmizela právě zakázka, pod kterou patřily.

Rozhodl jsem se tedy přidat constraint (byl bohužel odstraněn při vývoji a už nevrácen), abych znemožnil odstranění zakázky, když má pod sebou ještě nějaké tasky.

Jak z ADOQuery smazat ten správný záznam, pokud se používá JOIN, obr. 1

Stejný pokus o smazání už částečně spadne. Požadovaný task je smazán, ale odpovídající zakázka už nikoliv, protože jsou na ní závislé další tasky. Kdyby nebyly, byla by taktéž smazána.

Řešením, jak se tohoto ne zrovna ideálního výchozího chování zbavit je použití dynamické vlastnosti 'Unique Table' ADO komponenty. Nastavuje se tím tabulka, které se mazání bude týkat. Jen je třeba tuto vlastnost nastavit až po otevření Query!

MyQuery:=TADOQuery.Create(nil);
try
  MyQuery.Connection:=ADOConnection;
  MyQuery.SQL.Text:=
    'SELECT Tasky.*, Zakazky.* FROM Tasky ' +
    'INNER JOIN Zakazky ' +
    'ON Tasky.IdZakazky=Zakazky.ID ' +
    'WHERE Tasky.ID=1234 ';
  MyQuery.Open;
  //Řeknu, které tabulky se případný DELETE bude týkat (nutné přiřadit až po otevření Query)
  MyQuery.Properties['Unique Table'].Value:='Tasky';
  //Zkusím smazat a dělá to přesně co má - odstraní ten jeden požadovaný záznam z tabulky Tasky
  if MyQuery.RecordCount=1 then MyQuery.Delete;

finally
  FreeAndNil(MyQuery);
end;

Výchozí chování bylo tedy vlastně logické - aktuální záznam odkazuje na záznamy dvou tabulek, pochopitelně jsou známy i jejich ID, tak je příkaz Delete smaže. Také je třeba připomenout, že při spojení dvou tabulek se málokdy něco maže, spíše jde o sloučený pohled na data. Nicméně, mazání je stále možné a je třeba si toto opravdu dobře hlídat. Pochopitelně, je mnohem lepším řešením se úplně vyhnout vestavěného mazání a požadovaný záznam odstranit čistým SQL.

IdTasku:=TaskyGridView.DataController.GetItemByFieldName('ID').EditValue;
MyQuery.SQL.Text:=Format('DELETE FROM Tasky WHERE ID=%d', [IdTasku]);
MyQuery.ExecSQL;

Zdroj: