Kuidas vältida korduvate andmete sisestamisega seotud vigu

16.08.2007  |  Gunnar

Sattusin kunagi lugema huvitavat diskussiooni, kus oma ala profid vaidlesid andmete olemasolu kontrollimise teemadel. Lihtsas keeles öeldes käis vaidlus selle ümber, kas andmebaasi andmete sisestamisel on mõttekas enne sisestamist kontrollida sisestatavate andmetele vastavate kirjete olemasolu. Probleemile pakuti välja väga hea lahendus.

Klassikaline lahendus

Et probleemist paremini aru saada, siis oletame, et meil on näiteks klass, mis mille üks meetod salvestab toote andmed andmebaasi.

public void InsertProduct(Product product)
{
    // loo andmebaasi käsk
    // väärtusta selle parameetrid
    // käivita käsk
}

Olgu märkusena öeldud, et sõna "klassikaline" ei tähenda käesolevas kontekstis korrektset lahendust, vaid annab pigem aimu sellest, et massides möllab omamoodi arusaam probleemi lahendusest. 

SKU on toote unikaalne omadus ja kui andmebaasis juba eksisteerib taolise SKU-ga toode, siis tekib viga. Tavaliselt lisavad programmeerijad selle tarbeks kontrolliva meetodi, mis kutsutakse välja enne salvestamist ja mis ütleb, kas taolistele atribuutidele vastav rida on andmebaasi vastavas tabelis olemas või ei.

public bool ProductExists(string sku)
{
    // loo andmebaasi käsk
    // väärtusta selle sku parameeter
    // käivita käsk ning loe tagastatud väärtus
    // tagasta kas tagastatud väärtus oli nullist suurem
}

Seega saaksime lühidalt umbes sellise koodi:

...
if(!productManager.ProductExists(product.sku))
    productManager.InsertProduct(product);
...

Klassikalise lahenduse nõrk koht

Klassikalise lahenduse nõrgale kohale viitas üks tugevatest professionaalidest, kes väitis, et eeltoodud lahendus pole siiski piisavalt kuulikindel ning võib tekkida olukord, kus salvestamise hetkel on siiski samadele unikaalsuse tingimustele vastav kirje juba andmebaasi tabelis olemas.

Asi, millega klassikaline lahendus ei arvesta, on see, et andmebaasiserver tegeleb paralleelselt mitme ühendusega ning seega mitme kliendiga, kes kõik võivad tegutseda samade andmetega. Pole võimalik aimata, kas meie andmetega juhtub midagi kahe päringu vahel, mis me andmebaasi poole teele saadame või ei.

Illustreerin probleemi ühe lihtsa joonisega, kus kaks erinevat kasutajat püüavad samaaegselt salvestada samat toodet (ehk siis sama SKU-ga toodet).

andmebaas-sisesta-sku
Kaks kasutajat püüavad samaaegselt salvestada sama SKU-ga toodet.
Klassikaline lahendus probleemile.

Algul andmebaasis sellist SKU-d pole, mida sisestama hakatakse. Nii nagu meie, saab ka teine kasutaja, kes samat toodet sisestab, kontrollimise tulemuseks, et taolist SKU-d pole. Et aga teine kasutaja istub näiteks kiirema arvuti või vähem koormatud ühenduse taga, jõuab tema salvestuskäsk kohale meie omast enne. Meie salvestuskäsk on sellel ajal alles saatmisel.

See on üks võimalus, kuidas tekib olukord, kus me teame, et taolist rida veel andmebaasis pole ning suundume salvestama, kuid äkitselt on see rida kuskilt tekkinud. 

Hirmus mõelda, et mõned arendajad, kui taoline asi arenduskeskkonnas kogemata juhtub, otsivad nädal aega viga ning püüavad koodi igati siluda ja optimeerida, seejuures arvestamata eeltoodud situatsiooniga.

Korrektne lahendus

Korrektne lahendus on see, et töötleme salvestamisel tekkinud vigu. Andmebaasiserver annab meile alati teada, kui midagi valesti läks ning tagastatud veainfo põhjal saame ka teada, mis täpselt juhtus.

Pakutud lahenduse korral muutub me kood meetodite arvult lühemaks, seejuures pääseme ühest üleliigsest pöördumisest andmebaasi, mis omakorda tõstab nii meie süsteemi kui ka meie andmebaasi jõudlust.

Tulemuseks oleks umbes selline kood.

public void InsertProduct(Product product)
{
    // loo andmebaasi käsk
    // väärtusta selle parameetrid
    // käivita käsk
    // püüa kinni viga
    // töötle viga
}

Kokkuvõtteks

Arenduskeskkonnas selliseid nähtusi tihti ei esine, sest süsteemidel pole peal reaalset koormust ega sellist arvu kasutajaid nagu päris kasutamise puhul. Seega tundub klassikaline lahendus paljudele programmeerijatele igati korrektne. Ja see läheb kenasti läbi ka unit test-idest. Seega võivad arenduskeskkonnas jääda klassikalise lahenduse nõrkkoht märkamata.

Samuti ei pruugi see tükk aega välja tulla päris kasutajatega, sest pole just suur tõenäosus, et kaks kasutajat samal ajal samu andmeid salvestavad. Või vähemasti selliselt liigub paljude programmeerijate mõttekäik.

Samas aitab selle probleemi tekkeks ainult sellest, et andmebaasiserveril on äkki oodatust oluliselt suurem koormus peal ning kõik kasutajate päringud jooksevad pikemates ajalistes lõikudes paralleelselt.

Igal juhul on tegemist probleemiga, mida tihti valesti lahendatakse ja mida tihti alahinnatakse seetõttu, et selle tekke tõenäosus tundub algul äärmiselt väike. Samas, kui see probleem hakkab kord kasutajaid kummitama, tuleb süsteemis kõik klassikalisel lahendusel baseeruvad osad ümber kirjutada, mis tähendab arendajate jaoks täiendavat ajalist kulu.

4 kommentaari sissekandele “Kuidas vältida korduvate andmete sisestamisega seotud vigu”

  1. Marek

    Tegelikult ei püüa ükski komponenditest (unit test) kinni midagi sellist, mida ei testita. Sellise juhuse jaoks annaks kirjutada selline test (pseudokood):

    Test
    {
    connection = new Mock();
    Expect.On(connection).Call(HasSku).Return(false);
    Expect.On(connection).Call(Execute).Throw(SqlException);

    target = new Client(connection);
    Assert.IsFalse(target.InsertSku(new Sku));
    }

    Selline test tekitab olukorra, kus algul antakse teada, et SKU-d pole ja kui üritatakse lisada, siis tekitab vea. Kui keegi seda kinni ei püüa, siis test ei läbi. Peale selle kontrollime ka, et meetod InsertSku tagastaks vea korral väära tõeväärtuse.

    Samas tuleb arvestada ka sellega, et pole võimalik kõikide vigade peale tulla ja kui selline juhtum tekkib, siis tuleks sellest õppida.

  2. Anton

    Selline korrektne lahendus on üsna lühinägelik ja ei arvesta vist keerukate toimingutega mis võivad palju ressurssi nõuda. Kuna vea teket/olemasolu tuleb niiehknaa alati kontrollida, siis ennetav kontroll annab teada kas ressursse on üldse mõtet kulutama hakata (juhul muidugi, kui selline kontroll võtab vähem ressursse või omab koormust alandavat kasutegurit).

  3. Gunnar

    Anton, tihti just kontrollitakse kõiki muid vigu :) Teise teema on näiteks transaction dead-lockid. Mõlema juhusega olen pidanud oma lühikese elu jooksul maadlema ja tihtipeale on eelmise progeja tehtu olnud midagi sellist, millele delete ja alates nullist on parim ravim.

  4. Gunnar

    Marek, sul tegelikult hea idee, et kirjutada teste ka selliste juhtumite jaoks. Teine koht, mida testida võiks, on transaction dead-lockid, millega võiks vähemalt kuhugi maale andmekiht ise maadelda enne kui otsad annab. Dead-lockide tahtlik tekitamine, kusjuures, pole midagi keerukat. Testimisega peaks sama olema.

    Magusam küsimus on see, et kuidas erinevate andmebaaside vead mapperi kontekstis saaks kenasti mingiteks ühtseteks koodideks või exceptioniteks tõlkida. :)

Kommenteeri

sulge
Saada link e-postiga

© DT 2012 | Creative Commons Attribution-Noncommercial 3.0 License | WordPress