czwartek, 26 marca 2009

Hibernate - reprezentacja dziedziczenia

Pierwszy problem niedopasowania świata obiektowego i relacyjnego pojawia się podczas modelowania (mapowania) hierarchii klas. Dziedziczenie jest jednym z paradygmatów programowania obiektowego i zrezygnowanie z niego, z powodu relacyjności warstwy danych, byłoby uwstecznieniem. Używając Hibernate można odwzorowywać dziedziczenie na trzy sposoby:
1. tabela na każdą klasę (table per concrete class)
2. tabela na każdą hierarchię klas (table per class hierarchy)
3. tabela na każdą podklasę (table per subclass)

We wszystkich przypadkach będę wykonywał metodę:


public class Main {
public static void main(String[] args) {
Session session = null;
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();
Subcategory1 cat = new Subcategory1();
cat.setName("sub1");
cat.setSize(1);
Subcategory2 cat2 = new Subcategory2();
cat2.setName("sub2");
cat2.setVolume(2);
session.beginTransaction();
session.save(cat);
session.save(cat2);
session.flush();
session.getTransaction().commit();
session.close();
session = sessionFactory.openSession();
session.beginTransaction();
List<Category> cats = session.createQuery("from Subcategory1").list();
System.out.println(cats.size());
cats = session.createQuery("from Subcategory2").list();
System.out.println(cats.size());
}
}


public class Category {
Integer id;
String name;
public Integer getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public class Subcategory1 extends Category {
Integer id;
Integer size;
public Integer getId() {
return id;
}

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

public Integer getSize() {
return size;
}

public void setSize(Integer size) {
this.size = size;
}
}

public class Subcategory2 extends Category {
Integer id;
Integer volume;

public Integer getId() {
return id;
}

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

public Integer getVolume() {
return volume;
}

public void setVolume(Integer volume) {
this.volume = volume;
}
}


Ad 1.
Podejście najprostsze - baza danych w ogóle nie zdaje sobie sprawy z dziedziczenia.

Tworzę DWA pliki mapowań: Subcategory1.hbm.xml, Subcategory2.hbm.xml:


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<class name="Subcategory1">
<id name="id" unsaved-value="null" column="category_id">
<generator class="sequence">
<param name="sequence">sub1_category_seq</param>
</generator>
</id>
<property name="name"/>
<property name="size"/>
</class>
</hibernate-mapping>


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<class name="Subcategory2">
<id name="id" unsaved-value="null" column="category_id">
<generator class="sequence">
<param name="sequence">sub2_category_seq</param>
</generator>
</id>
<property name="name"/>
<property name="volume"/>
</class>
</hibernate-mapping>


Stworzone zostaly dwie tabele:


CREATE TABLE subcategory1
(
category_id integer NOT NULL,
name character varying(255),
size integer,
CONSTRAINT subcategory1_pkey PRIMARY KEY (category_id)
)

CREATE TABLE subcategory2
(
category_id integer NOT NULL,
name character varying(255),
volume integer,
CONSTRAINT subcategory2_pkey PRIMARY KEY (category_id)
)


Ad2.
Podejście drugie - odwzorowanie całej hierarchii klas w jednej tabeli.
Tworzę JEDEN plik mapowań: Category.hbm.xml


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<class name="Category">
<id name="id" unsaved-value="null" column="category_id">
<generator class="sequence">
<param name="sequence">category_seq</param>
</generator>
</id>
<discriminator column="type" type="string" />
<property name="name" />
<subclass name="Subcategory1" discriminator-value="1">
<property name="size" />
</subclass>
<subclass name="Subcategory2" discriminator-value="2">
<property name="volume" />
</subclass>
</class>
</hibernate-mapping>


Stworzona została jedna tabela:


CREATE TABLE category
(
category_id integer NOT NULL,
"type" character varying(255) NOT NULL,
name character varying(255),
size integer,
volume integer,
CONSTRAINT category_pkey PRIMARY KEY (category_id)
)

Ważne! Podczas tworzenia pliku z mapowaniem należy pamiętać, żeby element <discriminator> znajdował się przed wszystkimi elementami <property>.

Ad 3.
Podejście trzecie polega na reprezentacji związków dziedziczenia jako powiązań relacyjnych kluczy obcych. Każda podklasa korzysta z własnej tabeli.
Tworzę trzy pliki mapowań: Category.hbm.xml, Subcategory1.hbm.xml, Subcategory2.hbm.xml


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<class name="Category">
<id name="id" unsaved-value="null" column="category_id">
<generator class="sequence">
<param name="sequence">category_seq</param>
</generator>
</id>
<property name="name" />

</class>
</hibernate-mapping>


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<joined-subclass name="Subcategory1" extends="Category">
<key column="category_id"/>
<property name="size"/>
</joined-subclass>
</hibernate-mapping>


<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="main">
<joined-subclass name="Subcategory2" extends="Category">
<key column="category_id"/>
<property name="volume"/>
</joined-subclass>
</hibernate-mapping>


W bazie danych utworzone zostały trzy tabele:


CREATE TABLE category
(
category_id integer NOT NULL,
name character varying(255),
CONSTRAINT category_pkey PRIMARY KEY (category_id)
)

CREATE TABLE subcategory1
(
category_id integer NOT NULL,
size integer,
CONSTRAINT subcategory1_pkey PRIMARY KEY (category_id),
CONSTRAINT fk6c9080d3884719af FOREIGN KEY (category_id)
REFERENCES category (category_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)

CREATE TABLE subcategory2
(
category_id integer NOT NULL,
volume integer,
CONSTRAINT subcategory2_pkey PRIMARY KEY (category_id),
CONSTRAINT fk6c9080d4884719af FOREIGN KEY (category_id)
REFERENCES category (category_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)


Ten typ odwzorowania umożliwia wydawanie intuicyjnych zapytań polimorficznych:


cats = session.createQuery("from Category").list();
System.out.println(cats.size()); // "2"


Każdy wpis w tabeli subcategory1 i subcategory2 odpowiadać będzie wpisowi w tabeli category.

poniedziałek, 9 marca 2009

Hibernate - pierwsza aplikacja

Wiem, że to powinien być pierwszy post, ale jakoś nie miałem nigdy czasu ani chęci sklecać wszystkiego (całej konfiguracji) od początku. No ale w końcu się zmobilizowałem i zrobiłem pierwszą, prostą aplikację, która do bazy danych zapisuje jedną krotkę.

Moje POJO jest bardzo proste:


public class Category {
Integer id;
String name;

public Integer getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}



Do tego równie proste mapowanie:

<hibernate-mapping package="main">
<class name="Category">
<id name="id" value="null" column="category_id">
<generator class="sequence">
</generator></id></class></hibernate-mapping><paramname="sequence">category_seq

<property name="name">
</property>


Plik konfiguracyjny wygląda następująco:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/testy
</property>
<property name="hibernate.connection.driver_class">org.postgresql.Driver
</property>
<property name="hibernate.connection.username">test</property>
<property name="hibernate.connection.password">test</property>
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect
</property>
<property name="hibernate.show_sql">false</property>
<property name="hbm2ddl.auto">create</property>
<mapping resource="main\Category.hbm.xml" />
</session-factory>
</hibernate-configuration>


W konfiguracji zawarty jest wpis o automatycznym tworzeniu schematu bazy danych ze zdefiniowanych plików mapowań:
<property name="hbm2ddl.auto">create</property>


Główna klasa projektu wygląda następująco:

public class Main {
public static void main(String[] args) {
Session session = null;
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();
Category cat = new Category();
cat.setName("name");
session.beginTransaction();
session.save(cat);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("end");
}
}

Największą trudność może sprawić poprawne dobranie zależności. Działająca konfiguracja wygląda następująco:

- antlr-2.7.6.jar
- backport-util-concurrent-3.1.jar
- postgresql-8.1-407.jdbc3.jar
- ehcache-1.5.0.jar
- log4j-1.2.14.jar
- commons-collections-3.2.jar
- dom4j-1.6.1.jar
- commons-logging-1.1.1.jar
- hibernate-3.2.5.ga.jar
- jta-1.0.1B.jar
- xml-apis-1.3.03.jar
- cglib-nodep-2.1_3.jar

środa, 4 marca 2009

JUNG. Problem z generowaniem różnych grafów wynikowych dla takich samych danych wejściowych.

JUNG to skrót od Java Universal Network / Graph Framework. Biblioteka (albo framework, jak wolą autorzy) służy w ogólności do budowania grafów i obliczania współrzędnych ich wierzchołków. JUNG udostępnia szereg predefiniowanych typów grafów: skierowane, nieskierowane, acykliczne, z równoległymi krawędziami, drzewa, itp.
Sam framework jest bardzo dobrze udokumentowny i używa się go w sposób bardzo intuicyjny, zatem ograniczę się do krótkiego przykładu:


private void buildGraph() {
//tworzę graf
SparseGraph s = new SparseGraph();

//tworzę wierzchołki grafu
UndirectedSparseVertex node1 = new UndirectedSparseVertex();
UndirectedSparseVertex node2 = new UndirectedSparseVertex();

//dodaję wierzchołki do grafu
g.addVertex(node1);
g.addVertex(node1);


//tworzę krawędź między dwoma wierzchołkami
UndirectedSparseEdge link = new UndirectedSparseEdge(node1, node2);
//dodaję krawędź do grafu
g.addEdge(link);

//rozmieszczam elementy grafu na okręgu, płaszczyźnie o wymiarach 100 x 100.
CircleLayout layout = new MyCircleLayout(g);
layout.initialize(new Dimension(100, 100));

//informacyjnie wyświetlam obliczone przez JUNGa współrzędne
System.out.println("--------------");
System.out.println("node1.getX(): "+ layout.getLocation(node1).getX());
System.out.println("node1.getY(): "+ layout.getLocation(node1).getY());
System.out.println("node2.getX(): "+ layout.getLocation(node2).getX());
System.out.println("node2.getY(): "+ layout.getLocation(node2).getY());
}

Działa!

Ale nie do końca tak jak bym tego chciał... dosyć szybko okazało się, że JUNG nie zawsze oblicza te same współrzędne dla tego samego grafu. Gdyby powyższa metoda wywołana została dwa razy, wynik mógłby wyglądać następująco:

--------------
node1().getX(): 10.00
node1().getY(): 20.00
node2().getX(): 30.00
node2().getY(): 40.00

--------------
node1().getX(): 30.00
node1().getY(): 40.00
node2().getX(): 10.00
node2().getY(): 20.00


Problemem okazała się reprezentacja wierzchołków i krawędzi w obiekcie SparseGraph. Elementy te trzymane były w zbiorach HashSet, zatem kolejność ich pobierania ze zbioru nie zawsze musiała (i jak się okazało nie była) określona! Szybkim, prostym i skutecznym rozwiązaniem problemu okazało się napisanie klasy rozszerzającej SparseGraph i przeciążenie metody initialize(). Ostatecznie, poprawnie działający przykład, wygląda następująco:

private static class MyGraph extends SparseGraph {
protected void initialize()
{
super.initialize();
//LinkedHashSet gwarantuje, że elementy są umieszczane w zbiorze i pobierane z niego w tej samej kolejności
mVertices = new LinkedHashSet();
mEdges = new LinkedHashSet();
}
}

private void buildGraph() {
//tworzę graf
MyGraph s = new MyGraph();

//...
//dalej analogicznie jak w poprzednim przykładzie
}