Bardziej rozbudowany Hibernate, czyli dodajemy wpisy w ProgramBash

Po zabawach z rejestracją, uploadami plików i innymi sesyjnymi gadżetami przyszedł czas na trochę bardziej zorientowane na bazę rzeczy. Konkretnie chodzi oczywiście o użycie Hibernate i jego mechanizmu relacji. Do dzieła.

Podstawową rzeczą w serwisie będzie pojedynczy wpis. Upraszczając zagadnienie chodzi nam o wpis do bazy z pewną treścią, datą dodania, autorem, oceną (poprzez głosowanie innych użytkowników serwisu), a także by było ciekawiej przyporządkowanymi kategoriami. Mówiąc krótko mamy tutaj szereg relacji pomiędzy tabelami, no i samych tabel też trzeba będzie dorobić. Do tej pory (jak choćby w przypadku obiektu użytkownika), odwzorowania danych w bazie poprzez obiekt było proste. Mieliśmy zwykły obiekt z kilkoma własnościami – dopisaliśmy metody dostępowe i tyle. Podobną rzecz robiliśmy już przy okazji pisania HowToJava, jednakże tam relacje obsługiwał DSL z Grails, zatem rzecz była maksymalnie łatwa i uproszczona. W przypadku Hibernate rzecz jest trochę bardziej skomplikowana. Jednakże zanim weźmiemy się za kodowanie trzeba przedstawić kod SQLa, który posłuży nam do pracy. Do tej pory tabele były tworzone przez ORM, co wymagało nie raz ich poprawiania. Teraz damy Hibernate gotowe tabele do działania.

Istnieje już tabela reprezentująca użytkowników w serwisie. Użytkownicy dodają wpisy, które tym samym posiadają tylko 1 autora (wiele do jednego z perspektywy wpisu). Wpisy są również przypisane do jednej z kategorii (by było łatwiej wyszukiwać i segregować – relacja jeden do jednego). Tworzymy tabele:

-- Tabela dla wpisów
CREATE TABLE `entries` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`user_id` BIGINT NOT NULL ,
`category_id` BIGINT NOT NULL ,
`add_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,
`content` TEXT NOT NULL ,
`points` INT NOT NULL ,
INDEX ( `user_id` )
) ENGINE = MYISAM ;

-- Tabela dla kategorii
CREATE TABLE `entry_categories` (
`id` BIGINT  NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`category` VARCHAR( 64 ) NOT NULL ,
PRIMARY KEY ( `id` )
) ENGINE = MYISAM ;


INSERT INTO `entry_categories` (`id`, `category`) VALUES
(1, 'Java'),
(2, 'MySQL'),
(3, 'PostgreSQL'),
(4, 'C#'),
(5, 'PHP'),
(6, 'Groovy'),
(7, 'Python'),
(8, 'Ruby'),
(9, 'RoR'),
(10, 'C++'),
(11, 'C'),
(12, 'Bash'),
(13, 'Life');

Teraz trochę teorii, gdyż zrozumienie relacji w Hibernate wcale nie jest takie proste. Kilka rzeczy, które wypadałoby wiedzieć:

  • relacje jednostronne i dwustronne – relację uznajemy za jednostronną, kiedy tylko jedna z encji ma pojęcie o istnieniu drugiej. Prosty przykład – mam akwarium i w nim rybki. Jestem ich właścicielem i powiedzmy, że posiadam zbiór obiektów rybek i wiem dokładnie, która jest która. Natomiast moje rybki ni cholery nie wiedzą skąd się wzięły, co je to zresztą obchodzi. Relacja dwustronna ma miejsce wówczas, gdy obydwie strony relacji wiedzą o sobie. Idealnym przykładem jest rodzic, który posiada kilkanaście dzieci. Doskonale zdaje sobie sprawę z ich istnienia, potrafi je jednoznacznie rozróżnić, ale także każde z dzieci ma informacje o swoich rodzicach (w sumie jest to relacja wiele-do-wielu).
  • właściciel relacji – posiłkując się przykładami powyżej należy w każdej relacji wskazać jej właściciela. Jest to ta encja, która ma prawo do ustawiania klucza głównego w tabeli po stronie “wiele”. Czyli w przykładzie z rodzicem i dziećmi to rodzic za każdym razem ustawia swoje ID w tabeli dzieci. W naszym przypadku to użytkownik będzie właścicielem relacji z wpisem.
  • kaskadowość – bardzo przydatna rzecz, a konkretniej zapis, który pozwala na wskazanie czy przy zapisywaniu właściciela relacji utrwaleniu w bazie danych mają również być poddane wszystkie zależne encje. Jest to o tyle wygodne, iż wystarczy dodać dzieci do rodzica, zapisać rodzica w bazie i viola – informacje o dzieciach również zostały zapisane.

Rozpatrzmy teraz nasz przykład. Zaczniemy od tej łatwiejszej części, czyli relacji pomiędzy wpisem, a kategorią do której jest przypisany. Kategoria jako taka istnieje, ale nie ma pojęcia do czego jest przypisywana (albo co do niej) i zasadniczo taki stan rzeczy wszystkich zadowala. Zatem będziemy mieli do czynienia z relacją jednostronną, której to właścicielem będzie wpis – bo właśnie w tej klasie będzie zapisana informacja o kategorii. Z drugiej strony mamy użytkownika, który może być autorem pewnej ilości wpisów. Zatem wypadałoby, aby użytkownik miał dostęp do swoich wpisów (by wyświetlić je np. na stronie użytkownika). Z drugiej strony kiedy wyświetlimy jeden konkretny wpis to dobrze byłoby też poznać jego autora – skoro umieścił fajnego loga, to pewnie innego jego wpisy są/mogą być równie ciekawe. Czyli wpis musi znać swojego autora. Tym samym od razu wiadomo, że jest to relacja dwustronna. Kto ma być właścicielem? Z zaprezentowanego schematu bazy danych łatwo wyczytać, iż będzie nim użytkownik. Obiekt uzytkownika wciąż wisi u nas w sesji – i kiedy będziemy dodawać wpis, będziemy tenże obiekt aktualizować, a dzięki konfiguracji zachowania kaskadowego automatycznie aktualizacji dokonamy w tabeli z wpisami.

Dość teorii na razie – rzućmy okiem na kod:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.mappings">
  <class name="com.wordpress.chlebik.EntryCategory" table="entry_categories">
    <id column="id" name="id" type="long">
      <generator class="native"/>
    </id>
    <property column="category" name="category" type="string"/>
  </class>
</hibernate-mapping>

A oto i klasa Javy:

package com.wordpress.chlebik;

import java.io.Serializable;
import javax.persistence.Id;


/**
 * Klasa, ktora ma za zadanie byc beanem wystepujacym jako kategoria wpisu
 *
 * @author Michal Piotrowski
 */
public class EntryCategory implements Serializable {

    @Id
    private Long id;
    private String category;
    
    public EntryCategory() {  }

    public EntryCategory( String category ) {
        this.category = category;
    }

    @Override
    public String toString() {
        return category;
    }

    // Gettery
    public String getCategory()
    {
        return category;
    }
    
    protected Long getId()
    {
        return id;
    }

     // Settery - bardziej dla poprawnosci niz potrzeby
    public void setCategory( String category )
    {
        this.category = category;
    }

    protected void setId( Long id )
    {
        this.id = id;
    }
}


Rzecz już bardziej prosta być nie może. Mapowanie tej klasy jest póki co podobne do obecnego obiektu usera. Zajmijmy się teraz wpisem.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.mappings">
  <class name="com.wordpress.chlebik.Entry" table="entries">
    <id column="id" name="id" type="long">
      <generator class="native"/>
    </id>

    <!-- relacja -->
    <many-to-one name="category" class="com.wordpress.chlebik.EntryCategory" column="category_id" />

    <!--  zwykle kolumny -->
    <property column="content" name="entry" type="string"/>
    <property column="points" name="points" type="integer"/>
    <property column="add_date" name="adddate" type="timestamp"/>
  </class>
</hibernate-mapping>

No i jej kod:

package com.wordpress.chlebik;

import java.io.Serializable;
import java.sql.Timestamp;
import javax.persistence.Id;


/**
 * Klasa, ktora ma za zadanie byc beanem wystepujacym jako wpis
 *
 * @author Michal Piotrowski
 */
public class Entry implements Serializable {

    @Id
    private Long id;
    private EntryCategory category;
    private Timestamp adddate;
    private String entry;
    private Integer points;

    /**
     * Bezargumentowy konstruktor
     */
    public Entry() {  }

    /**
     * Konstruktor z danymi
     *
     * @param id
     * @param adddate
     * @param entry
     * @param points
     * @param category
     */
    public Entry( Long id, Timestamp adddate, String entry, Integer points, EntryCategory category ) {
        this.id          =   id;
        this.category    =   category;
        this.adddate     =   adddate;
        this.entry       =   entry;
        this.points      =   points;
    }

    @Override
    public String toString() {
       return entry + "\n Kategoria: " + category.toString();
    }

    // Gettery
    public EntryCategory getCategory()  {
        return category;
    }
    
    public Long getId()  {
        return id;
    }

    public String getEntry()  {
        return entry;
    }

    public Timestamp getAdddate()  {
        return adddate;
    }

    public Integer getPoints()  {
        return points;
    }

     // Settery 
    public void setCategory( EntryCategory category )  {
        this.category = category;
    }

    public void setId( Long id )  {
        this.id = id;
    }

    public void setPoints( Integer points )  {
        this.points  = points;
    }

    public void setAdddate( Timestamp adddate ) {
        this.adddate = adddate;
    }

    public void setEntry( String entry ) {
        this.entry  =  entry;
    }

}

Nie zapomnijmy również dodać stosownych wpisów w pliku hibernate.cfg.xml:

    <mapping resource="/com/wordpress/chlebik/mapping/EntryCategory.hbm.xml"/>
    <mapping resource="/com/wordpress/chlebik/mapping/Entry.hbm.xml"/>

Encja Entry posiada ID kategorii, dzięki czemu wrzucając mapowanie do pliku konfiguracyjnego Hibernate bez problemu wie, co i skąd wyciągnąć. Jak widać w encjach nadpisałem metodę toString, dzięki czemu dodając do bazy danych przykładowy wpis mogłem po małej magii otrzymać w widoku takie oto cudo:

Od razu postanowiłem podpiąć formatowanie kodu, o którym pisałem niedawno. Zważywszy na edukacyjny charakter aplikacji,a tym samym małe obciążenie postanowiłem linkować do zewnętrznych źródeł. Podpięcie biblioteki, wrzucamy wyciągnięty z bazy kod i oto taki widok:

Ślicznie. Wróćmy jednakże do głównego wątku naszych rozważań. Nasz wpis posiada przypisaną kategorię. Posiada również autora, którym jest jeden z użytkowników w bazie danych. Zajmiemy się teraz stworzeniem odpowiedniego mapowania dla pokazania tej relacji. Jak napisałem powyżej jest to relacja dwustronna – wpis zna swojego autora, zaś użytkownik również jest świadom swoich wpisów.

W pliku Users.hbm.xml dopisujemy taki oto kod:

<set table="entries" name="entries">
         <key column="user_id"/>
         <one-to-many class="com.wordpress.chlebik.Entry" not-found="ignore" />
</set>

Mówi on tyle, że w przypadku obiektu User mamy dostęp do zbioru unikalnych wpisów, które są składowane w tabeli entries. Identyfikującym kluczem, który pozwoli wyciągnąć tylko te wpisy, które przynależą do naszego użytkownika jest kolumna user_id. No i ostatecznie obiekty, które będą w tej kolekcji są obiekty typu Entry. Rzecz dość czytelna. Oczywiście dopisujemy również metody dostępowe do tej kolekcji w klasie User!!!.

Z kolei by obiekt wpisu zdawał sobie sprawę z istnienia swego autora i mógł się do niego odwoływać należy do pliku Entry.hbm.xml dorzucić taką deklarację:

<many-to-one name="author" class="com.wordpress.chlebik.User" column="user_id"/>

W klasie Entry również trzeba dopisać metody dostępowe dla właściwości author. Uruchamiamy naszą aplikację i testowo gdzie nam wygodnie umieszczamy takie coś:

        try
        {
            org.hibernate.SessionFactory sessionFactory = com.wordpress.chlebik.util.ProgramBashUtil.getSessionFactory();
            Session session = sessionFactory.openSession();
            Entry wpis = (Entry) session.get( Entry.class, new Long(1) );
            return wpis;
        }
        catch( Exception e )
        { };

Używam metody get, gdyż nie rzuca ona wyjątku przy wyciąganiu obiektu z bazy tylko zwraca NULL w przypadku braku poszukiwanego rekordu. Akurat jego istnienia jestem pewny (wklepałem go ręcznie), ale dobrze jest wyrabiać sobie dobre nawyki. Zmodyfikowałem trochę metodę toString w klasie Entry i po uruchomieniu aplikacji pokazał się mym oczom taki widoczek:

Ślicznie. Od strony pojedynczego wpisu działa. Teraz testowo zamieniamy kod wyciągający pojedynczy wpis na taki oto kawałek:

 User author = (User) session.get( User.class, new Long(49) );
 return author;

Zaś w pliku Users.hbm.xml zmieniamy definicję na:

 <bag table="entries" name="entries" order-by="add_date">
         <key column="user_id"/>
         <one-to-many class="com.wordpress.chlebik.Entry" not-found="ignore" />
 </bag>

W widoku wrzuciłem taki kod dla spróbowania:

<rich:dataList var="elem" value="#{testowyBean.author.entries}">
    <h:outputText value="#{elem.entry}" />
</rich:dataList>

I pokazuje się nam piękna lista wpisów, które stworzył nasz użytkownik (na razie jego ID wklepane na sztywno, ale przerobienie tego to za chwilkę). Pytanie czemu użyłem znacznika bag, a nie np. Set lub List. Odpowiedź na to pytanie można znaleźć pod tym adresem – nie będę tworzył dodatkowego taga dla operacji, które powinny być oczywiste i powszechnie dostępne w widoku. Co do List dostawałem błąd parsowania XMLa – pomimo wklapania struktury znacznika prosto z dokumentacji Hibernate. Dopiero ww. konstrukcja z bag dała radę i posłusznie współpracuje ze znacznikiem rich:dataList.

Wpis trochę się zrobił rozwlekły zatem część widoku oraz samej logiki w następnym odcinku.

Advertisements

4 thoughts on “Bardziej rozbudowany Hibernate, czyli dodajemy wpisy w ProgramBash

  1. Piotr Nowicki

    Polecam Ci zapoznanie się z automatyczną generacją tabel w bazie danych na podstawie utworzonych klas 😉
    Obczaj właściwość hibernate.hbm2ddl.auto
    Bardzo wygodna sprawa 😉

  2. chlebik Post author

    1. Hibernate generowal mi tabele przy okazji pracy z Grailsami, jak i w przypadku newsow w tej aplikacji i zawsze czegos tam brakowalo (choc to pewnie kwestia taka, ze konfig nie byl uszczegolowiony – np. wrzucal TEXT zamiast VARCHAROW).

    2. O adnotacjach bedzie potem oddzielny wpis. Wole wpierw pokazac XMLa, ktory explicite pokazuje co chcemy osiagnac, niz prosty zapis – @Id, @OneToMany(name=”costam”) – gdzie do konca nie wiadomo co z czym 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s