ex01: Datu piekļuve ar Hibernate

NEW! JUnit testi 1.praktiskajam darbam tagad pieejami - sk. ex01_junit.zip.

Dota UML klašu diagramma (tā ir veidota reālam uzdevumam - tekstu konkordancei, kas apkopo literatūras tekstu fragmentus, tajos atrastos vārdus, kā arī uztur konkordances sistēmas lietotājus). Izveidot šim klašu domēnam datu piekļuves slāni (DAO). Darbību secība:

  1. Instalēt J2SDK (v 1.5), Ant, Eclipse (vai citu piemērotu integrēto programmēšanas vidi (IDE)). Instalēt MySQL un izveidot tajā tukšu datubāzi un vienu lietotāja kontu (piemēram, root:root). Sk. instalāciju instrukcijas.
  2. Lai rastos iespēja no Ant'a izsaukt "junit" tasku, vajag iekš %ANT_HOME%/lib iekopēt junit.jar failu. To var dabūt no http://www.junit.org. Par Ant'a bibliotēku atkarībām var apskatīties šeit: http://ant.apache.org/manual/install.html#librarydependencies.
  3. Pirms sākt veidot savu projektu, ieteicams iepazīties ar paraugu - sk. ex01_hibersample.zip.
    • atarhivēt šo ZIP failu
    • uztaisīt atbilstošajā direktorijā (exercise01-sagatave) Eclipse projektu
    • savākt visus JAR failus "lib" apakšdirektorijā, kuri uzskaitīti "lib/readme.txt" failā
    • Konfigurēt MySQL lietotāju root:root; un izveidot tukšu datubāzi iekš MySQL ar nosaukumu "hibersample" (aplikācijai nepieciešamo datubāzes nosaukumu var atrast Hibernate konfigurācijas failā hibernate.cfg.xml). Tukšu datubāzi iekš MySQL var iegūt, atverot MySQL klientu - t.i. EXE failu %MYSQL_HOME%\bin\mysql.exe ar parametriem "-u root -p"; ievadot paroli (arī "root"), un datubāzes konsolē ierakstot šādas komandas:
      drop database databaseName
      create database databaseName
      
    • Palaist testus ar komandu "ant junit".
    • Izpētīt Hibernate uzģenerēto relāciju datubāzes struktūru.
  4. Uzprogrammēt domēnobjektu klases pēc dotās UML diagrammas. (Sk. Domēna klašu diagramma)
  5. Izveidot Hibernate object-relational (O2R) attēlojumus (mappings).
  6. Ģenerēt MySQL datubāzes tabulas, izmantojot šos attēlojumus. Hibernate konfigurācijas failā var izmantot šādu datubāzes konekcijas stringu:
    jdbc:mysql://localhost:3306/dictionary?useUnicode=true&characterEncoding=UTF-8&useOldUTF8Behavior=true
    
    Pēc var palaist kādu vienkāršu JUnit testiņu no dotajiem piemēriem, pie tam failā hibernate.cfg.xml atkomentējot rindiņu
    <property name="hbm2ddl.auto">create</property>
    
    Vēlākajos praktiskajos darbos, kad vēlēsieties, lai datubāze netiktu ikreizi izdzēsta un izveidota no jauna, šo rindiņu vajadzēs aizkomentēt (datubāzi ir nepieciešams pārģenerēt tikai tad, ja ir izmainīti attēlojumu/mapping faili).
  7. Pēc dotajām UML diagrammām uzprogrammēt datu piekļuves objektus (DAO) un testēt tos ar pievienotajiem JUnit testiem. (Sk. DAO klašu diagramma).
  8. Testēt risinājumu ar dotajiem JUnit testiem.
  9. Dažas nesenas izmaiņas klašu diagrammās un prasībās ir apzīmētas ar zilu krāsu - lai būtu tās vieglāk atrast. Ja ir papildināta klašu diagramma (teiksim, DAO klases ir papildinātas ar zilām metodēm "getInstance()", "delete()", "findXxx()"; tad šo metožu implementācijas būs Jums piedāvātajās koda sagatavēs, lai jūtami nepalielinātos darba apjoms. Visos gadījumos Jūs varat šīs piedāvātās implementācijas izmainīt pēc saviem ieskatiem; svarīgi ir vienīgi tas, lai izpildītos JUnit testi.

Komentāri diagrammām

Gan domēna, gan DAO diagrammas ir domātas tekstu konkordances izveidošanai (konkordancei lietotāji var pievienot tekstus, pēc tam katrai vārdformai var apskatīties, kādos tekstos tā ietilpst, aplūkojot šai vārdformai apkārt esošo kontekstu). Konkordances aplikācijas funkcionalitāte ir attēlota šajā Use-case diagrammā.

  • Visas insert() metodes saņem ierakstam nepieciešamās kolonnu vērtības atbilstošajā domēna objektā (piemēram, UserDAO.insert(u:User)). Šī metode arī atgriež objektu ar attiecīgo tipu (piemēram, User), kurš no argumenta atšķiras ar to, ka tam ir uzstādīts id atribūts.
  • Metodes listAll() un list(offset,length) atgriež sakārtotus objektu sarakstus - ar tipu java.util.List (pēc lietotājvārdu alfabētiskā secībā, vai tekstu pievienošanas hronoloģiskā kārtībā)
  • orderType var pieņemt vienu no 4 dažādām vērtībām - 0 (alfabētiska secība), 1 (leksikogrāfiska secība - īsākie vārdi vispirms, garākie pēc tam), 2 (inversa alfabētiska secība - t.i. sakārto alfabētiski vārdu spoguļattēlus), 3 (inversa leksikogrāfiska secība).
  • Ja rodas datubāzes kļūda, DAO metodēm ir jāmet izņēmums.
  • WordDAO.regexFilter aprobežo metodes izvadi ar tiem vārdiem, kuri atbilst dotajai regulārajai izteiksmei. Piemēram, "^a+" - regulāras izteiksmes filtrs, kurš izraudzīsies tikai vārdus, kuri sākas ar burtu "a".
  • TextDAO.getContexts() ir konkordancei visbūtiskākā metode - atgriež sarakstu ar String tipa mainīgajiem, kuri satur meklēto vārdu. Stringi uz abām pusēm satur norādīto garumu konteksta

Prasības DAO slānim

Vispārīga piezīme: Ja DAO slānis saņem kādu izsaukumu, kura ir pārkāpts kāds no izklāstītajiem datu integritātes nosacījumiem un ja nav īpaši prasīta cita uzvedība, tad attiecīgajai datu piekļuves metodei ir jāmet java.lang.IllegalArgumentException; izņēmuma konstruktorā norādot String parametru - saturīgu kļūdas paziņojumu.

Prasības UserDAO metodēm

  1. Datubāzes lauki USER.USERNAME un USER.PASSWORD ir obligāti (not NULL); to garumi ir intervālā [2,12].
  2. "userName" un "password" ir alfanumeriski ASCII simboli, t.i. tie sastāv no baitiem 0x21(!), ..., 0x7E(~). Tukšumi, vadības simboli vai ne-ASCII simboli šajos laukos nav atļauti.
  3. Atribūti User.firstName un User.lastName var būt NULL; to garums nepārsniedz 25 simbolus; šie stringi var saturēt jebkurus Unikoda 1.plaknes simbolus.
  4. User.email ir alfanumeriska e-pasta adrese (tā nesatur ne-ASCII simbolus, tukšumus vai vadības simbolus); tās garums nepārsniedz 50 simbolus.
  5. Lietotāja reģistrācijas datums parasti ir tekošais timestamp (datums+laiks milisekundēs), bet var arī atšķirties no tekošā laika uz datora (ko atgriež konstruktora izsaukums new java.util.Date(). Reģistrācijas datums nevar būt laiks, kurš ir pirms 1970.gada 1.janvāra.
  6. Metode UserDAO.insert(u:User) pirms lietotāja ierakstīšanas datubāzes tabulā pārliecinās, vai jau neeksistē lietotājs ar šādu userName. Ja tāds eksistē, tad met lv.lu.mii.luweb2005.dictionary.dao.DuplicateException un jauno ierakstu nepievieno.
  7. Metode update(u:User) izlabo datubāzē esošā lietotāja datus (izņemot userName), kuram ir tāds pats id kā metodes argumentā norādītajam. Ja šāda lietotāja datubāzē nav, tad metode update met izņēmumu lv.lu.mii.luweb2005.dictionary.dao.RecordNotFoundException. Pat tad, ja metodes update argumentā ir norādīts cits lietotāja vārds (userName), šo lietotāja vārdu datubāzē nevajag mainīt.
  8. Metode delete(id:int) izdzēš no datubāzes lietotāju ar norādīto id. Ja šāda lietotāja nav, met izņēmumu lv.lu.mii.luweb2005.dictionary.dao.RecordNotFoundException.
  9. Metode listAll() atgriež sarakstu ar visiem lietotājiem, kuri ierakstīti datubāzē. Lietotāji sarakstā sakārtoti lietotājvārdu (userName) alfabētiskā secībā. Ja lietotāju datubāzē nav, jāatgriež tukšs saraksts (t.i. inicializēts saraksts ar 0 elementiem), bet nedrīkst atgriezt null referenci.
  10. Metode list(offset,length) uzvedas līdzīgi kā metode listAll() - tikai atgriež noteiktu sakārtotā saraksta fragmentu, izlaižot "offset" lietotājus no sākuma un iekļaujot sarakstā "length" lietotājus. Piemēram list(0,10) atgriezīs pirmos 10 lietotājus. Ja kāds no "offset" vai "length" ir negatīvs, tad jāmet IllegalArgumentException. Ja saraksts nav pietiekami garš, lai varētu tajā dabūt length lietotājus ar vajadzīgo offset, tad var atgriezt īsāku sarakstu (vai arī tukšu sarakstu, ja offset >= ierakstuSkaits.
  11. Metodes findByID(int) un findByUserName(String) atgriež lietotāju, kurš eksistē datubāzē ar atbilstošo id/userName. Vai arī null, ja šāda lietotāja datubāzē nav. Piedāvātajā koda sagatavē ir dotas šo metožu implementācijas, bet jūs varat tās mainīt, ja atrodat par vajadzīgu.
  12. Visas "find" un "list" metodes uzvedas slinki attiecībā pret User asociācijām - t.i. kopa User.ownedTexts ir null pēc šo metožu izsaukšanas. (Atgādinu, ka implementējot domēna klasi User, tajā ir jānorāda Set tipa atribūts ownedTexts, kurš atbilst klases User relācijai ar klasi Text. Šī prasība izriet no UML diagrammas semantikas un attiecas uz visām relācijām, kuras ir iezīmētas domēna UML diagrammā)
  13. Ar "list" un "find" metodēm tiek atrasti arī administratori, bet šo Javas objektu izpildes laika tips (runtime type) ir nevis "User", bet "Administrator".
  14. Izdzēšot lietotāju, viņa teksti netiek izdzēsti; to "owner" kļūst "null". (Ja vēlaties izmēģināt, varat implementēt arī kaskādes izdzēšanas uzvedību - līdz ar lietotāju tiek izdzēsti visi viņa teksti. Darbu labojot, to neuzskatiis par kļūdu, tomeer Jums jāizvēlas viena no abām alternatīvām un konsekventi tā jārealizē.)

Prasības TextDAO metodēm

  1. Text.urlOrigin satur tikai alfanumeriskus ASCII simbolus (bez tukšumiem un vadības simboliem). Tas ir obligāts lauks; tā garums ir intervālā [3,100].
  2. content var saturēt jebkādus Unikoda simbolus. Tā garums atbilst tam, ko var ierakstīt MySQL datu tipā MEDIUMTEXT vai MEDIUMBLOB - t.i. tā garums nepārsniedz 16777215 baitus. (Ievērojiet, ka pārkodējot ne-ASCII simbolus, teksta garums UTF-8 kodējumā var izstiepties - viens modificētais latviešu, vai kirilicas burts var aizņemt divus baitus.)
  3. Metodes update(Text) un delete(id:int) uzvedas tāpat kā atbilstošās User metodes - ja ieraksts neeksistē, tiek mests lv.lu.mii.luweb2005.dictionary.dao.RecordNotFoundException izņēmums.
  4. Ar update(t:Text) metodi var izlabot datubāzes ieraksta laukus CONTENT, PRIV un OWNER_ID (t.i. norādi uz lietotāju - teksta īpašnieku). Citi datubāzes lauki (TEXT_ID, ADDED, URLORIGIN) paliek tādi paši kā Text ieraksta veidošanas brīdī.
  5. Koda sagatavē, klasē TextDAO ir implementētas divas metodes - findById(id:int) un findByUrlOrigin(url:String). Jūs tās varat mainīt pēc saviem ieskatiem.
  6. Metodes listAll() (vai listAll(User)) atgriež pilnīgi visu (vai visu dotajam lietotājam piederošo) tekstu sarakstu. Šajos sarakstos visām Text instancēm lauks content satur tikai pirmos 255 simbolus. Ja teksti ir garāki, tad aiz šiem simboliem liek daudzpunktu. (Šis dīvainais ierobežojums vajadzīgs tādēļ, lai pievienoto tekstu sarakstu caurskatīšana nebeigtos ar atmiņas pārpildīšanos.)
  7. Metodes listAll() un listAll(User) atgriež sarakstus added laiku dilšanas secībā (t.i. jaunākie teksti ir sarakstā pirmie).
  8. Metode getContexts(word:Word,length:int) atgriež sarakstu ar stringiem, kuros ir tekstu fragmenti, kuri satur doto vārdu; turklāt pirms un pēc dotā vārda ir dots arī "length" garumā konteksts. (Tātad, visi stringi ir ar vienādu garumu: w.getW()+2*length.) Ja izrādās, ka vajadzīgais vārds ir tuvu teksta sākumam vai teksta beigām, un "length" simbolus uz vienu vai otru pusi dabūt nevar, tad stringam pieliek atbilstošu skaitu tukšumu, lai panāktu to, ka vajadzīgais vārds atrodas visu kontekststringu vidū.
  9. Metodes getContexts(word:Word,length:int) atgrieztie stringi ir sakārtojami tekstu "added" (pievienošanas) datumu dilstošā secībā (t.i. jaunākie ir pirmie). Ja vārds tai pašā tekstā ieiet vairākkārt, tad agrāk tekstā sastaptie vārdi ir norādāmi agrāk.
  10. Visām "list" un "find" metodēm ir "non-lazy" veidā jāinicializē Text.owner lauks, t.i. jāatrod lietotājs - teksta īpašnieks.
  11. Ja tekstu dzēš, tad kaskādes veidā no datubāzes dzēš arī atbilstošos Position ierakstus. Šī nav rekursīva kaskāde, jo vārdus, kuriem vairs neatbilst neviena pozicija tekstos, nedzēš, bet patur datubāzē.

Prasības WordDAO metodēm

  1. Metodes insert() un update() uzvedas līdzīgi TextDAO klases atbilstošajām metodēm.
  2. Ja insert(word:Word) metodē word.w ir null vai garāks par 25 simboliem, tad metode met IllegalArgumentException. Ja word.wCorrect ir garāks par 25 simboliem, tad metode met IllegalArgumentException. Ja, savukārt, word.wCorrect ir null, tad datubāzē ieraksta vērtību wCorrect=w. Ja basic ir null, tad to kā NULL ieraksta arī datubāzē.
  3. Nevar pievienot vairākus vārdus, kuriem ir vienādi gan w, gan wCorrect, gan basic. Ja ar insert() mēģina pievienot šādu vārdu otrreiz, tad metode neiesprauž datubāzē dubultnieku, nemet nekādu izņēmumu, bet atgriež datubāzē jau esošo vārdu.
  4. Ar update() nevar izmainīt id un w laukus datubāzē (ja šīs vērtības update argumentam atšķiras no datubāzes, tad tās tiek ignorētas). Visus citus laukus update izmaina saskaņā ar norādīto argumentu.
  5. Ja eksistē divi vārdi, kuriem ir vienāds "w", bet atšķiras "wCorrect" vai "basic", un ja ar "WordDAO.update()" tos mēģina mainīt tā, ka visas vērtības (w,wCorrect,basic) sakriit ar kādu datubāzē jau esošu ierakstu (t.i. izpildās visi trīs "equals()"), tad "update()" met "DuplicateException" un neko nemaina datubāzē.
  6. Visiem vārdiem, kuri pieder klasei BasicWord jābūt spēkā vienādībai word.basic = word (t.i., ja kāda vārdforma ir pamatforma citai vārdformai, tad tā ir pamatforma arī pati sev). Ja ar insert() mēģina iespraust BasicWord instanci, kurai šī vienādība neizpildās, tad met IllegalArgumentException, norādot atbilstošu kļūdas paziņojumu šī izņēmuma stringa konstruktorā.
  7. Ja "regexFilter" nav "null", tad abas metodes "list" un "listAll" atgriež tikai tos vārdus, kuri atbilst regulāras izteiksmes filtram. Regulāras izteiksmes tiek norādītas atbilstoši MySQL sintaksei (sk. http://dev.mysql.com/doc/mysql/en/regexp.html).
  8. Vārdi tiek kārtoti pēc savām "wCorrect" lauku vērtībām alfabētiskā secībā atbilstoši MySQL servera noklusētajam kolatoram (sk. http://dev.mysql.com/doc/mysql/en/charset-server.html). Visticamāk, ka tas ir "latin1_swedish_ci". Ja "wCorrect" lauki sakrīt, tad kārto pēc "basic.wCorrect". Ja sakrīt gan "wCorrect", gan "basic.wCorrect", tad kārto pēc "w".
  9. orderType vērtību WordDAO klasei var uzstādīt, bet šī uzvedība netiks testēta. Visa kārtošana notiks pēc normāla alfabētiska sakārtojuma (nevis pēc leksikogrāfiska, inversa, utml.). Varat pieņemt, ka JUnit testos orderType vienmēr būs 0.
  10. Ar "find" un "list" atrastajos vārdos ir jābūt inicializētai kopai "alternative" un arī norādītai vērtībai "basic", ja tās eksistē datubāzē. BasicWord instancēm piesaistītā vārdu kopa wordForms (kas ir inversā relācija many-to-one relācijai basic) var uzvesties "lazy" veidā - t.i. wordForms var būt null, ja vārds ir atrasts ar "find" vai "list" metodi.
  11. Ar "list" un "find" metodēm tiek atrastas gan Word gan BasicWord instances, un tām ir pareizie izpildes laika tipi.

Prasības PartOfSpeechDAO metodēm

  1. "listAll()" atgriež sarakstu ar visām vārdšķirām sakārtots pēc "id".
  2. "initializeTable()" izdzēš iepriekšējo "PARTOFSPEECH" tabulas saturu un ieraksta sekojošas vērtības:
    idname
    1noun
    2adjective
    3pronoun
    4numerical
    5verb
    6adverb
    7preposition
    8conjunction
    9particle
    10interjection

Daži jautājumi

Kā ielasīt tikai gabaliņu BLOBa?

Jautājums:

Praktiskajā darbā minēta šāda prasība: "TextDAO metožu atgrieztajos sarakstos visām Text instancēm lauks content satur tikai pirmos 255 simbolus. Ja teksti ir garāki, tad aiz šiem simboliem liek daudzpunktu."
Kā to var realizēt ? Vai var izmantot lazy loading content atribūtam, list() metodēs ielādēt Text objektu sarakstu, un tad katram saraksta elementam ciklā nolasīt šo atribūtu un tad ņemt pirmos 255 simbolus ? Vai ir kāds skaistāks veids, kā to izdarīt ?

Atbilde: Šādu risinājumu var lietot. Tas vismaz pasargās no nepieciešamības vienlaicīgi ielādēt visus tekstus operatīvajā atmiņā. To var darīt arī labāk, piemēram, ar parastu SQL pieprasījumu, kur uzreiz izgriež no BLOB objekta vajadzīgo prefiksu. (Jūs varat izmantot tiešu SQL pieprasījumu, ja vajag kādas MySQL īpatnības nokodēt tieši - to pieraksta, iegūstot JDBC konekciju:

Session session = lv.lu.mii.luweb2005.dictionary.util.HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();		
Connection con = session.connection();		
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("...");
...// apstrādā ResultSet atgrieztos ierakstus
tx.commit();
HibernateUtil.closeSession();  // šoreiz nevajag izsaukt con.close(). 

Augstākminētais koda piemērs rāda, kā lietot JDBC kopā ar Hibernate.

Par equals() un hashCode()

Jautājums: Kāds ir kritērijs, lai noteiktu, vai divi Word objekti ir vienādi (equals()), kādu izteiksmi var izmantot hashCode() metodē ?
Atbilde:

Divi Word objekti ir vienādi, ja sakrīt to "w" un "wCorrect" (String.equals nozīmē) un arī "basic" atribūti (rekursīvi izsauktas Word.equals() nozīmē). Rekursija nākamajā solī beidzas, jo "BasicWord" gadījumā vienmēr ir spēkā "w.basic == w". Metode "equals(Object o)" nedrīkst mest izņēmumu tad, ja arguments ir kāds objekts, kurš nav ar runtime-tipu Word vai BasicWord - tai jāatgriež vērtība "false".

Lai rēķinātu hasCode() ir jāgarantē, ka diviem objektiem, kuriem "equals()" ir "true", sakritīs arī heškodu vērtības. Toties NAV vēlams, lai dažādiem objektiem bieži sakristu heškoda vērtības (citādi visas datu struktūras, kur izmanto hešingu būs ļoti neefektīvas). Labu hešfunkciju var iegūt, ja ar "^" (XOR operators jeb bitveida saskaitīšana pēc 2 moduļa) saskaita visu objekta atribūtu heškodu vērtības. Mūsu gadījumā to var darīt šādi:

w.hashCode() ^ wCorrect.hashCode() 
^ getBasic().getW().hashCode() ^ getBasic().getWCorrect().hashCode();

Vai arī kaut kas cits līdzīgs pēc Jūsu gaumes. Teoriju sk. piemēram šeit: http://www.javapractices.com/Topic28.cjp

Literatūra


Lapa mainīta 2005-12-13 08:10:49