piątek, 18 września 2009

Kaskadowość w hibernate

Postaram się przeanalizować dosyć nieintuicyjną (według mnie) kwestię kaskadowego zapisywania relacji w Hibernate. Posłużę się wykorzystywanym już wcześniej przykładem dwóch tabel: Produkt i Kategoria.

Na początek: brak kaskadowości, mapowanie jednostronne.
Mapowania i klasy POJO:


<?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="Produkt" table="produkt">
<id name="id" unsaved-value="null" column="produkt_id">
<generator class="sequence">
<param name="sequence">prod_seq</param>
</generator>
</id>
<property name="name" />
<property name="value" />
</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="Kategoria" table="kategoria">
<id name="id" unsaved-value="null" column="kategory_id">
<generator class="sequence">
<param name="sequence">kat_seq</param>
</generator>
</id>
<property name="name" />
<property name="type" />
<property name="description" />
<set name="products" lazy="true" inverse="false" cascade="none">
<key column="kategory_id" />
<one-to-many class="Produkt" />
</set>
</class>
</hibernate-mapping>


package main;

public class Produkt {
Integer id;
String name;
Integer value;


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 Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}
}

package main;

import java.util.HashSet;
import java.util.Set;

public class Kategoria {
Integer id;
String name;
String type;
String description;
Set<Produkt> products = new HashSet<Produkt>();

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 String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public Set<Produkt> getProducts() {
return products;
}

public void setProducts(Set<Produkt> products) {
this.products = products;
}
}


Zapisanie do bazy danych kategorii i produktu wygląda następująco:


package main;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class Main {
public static void main(String[] args) {
Session session = null;
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();

Kategoria kat = new Kategoria();
Produkt prod1 = new Produkt();
Produkt prod2 = new Produkt();
kat.setName("kat");
prod1.setName("prod1");
prod2.setName("prod2");
kat.getProducts().add(prod1);
kat.getProducts().add(prod2);
session.beginTransaction();
session.save(prod1);
session.save(prod2);
session.save(kat);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("zapisane!");
session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
session.clear();
session.delete(prod1);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("produkt usuniety");
session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
session.clear();
session.delete(kat);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("kategoria usunieta");
session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
System.out.println("end");
}
}


Bardzo ważne jest, aby zapisać zarówno produkt jak i kategorię. Jeśli tego nie zrobimy, pojawi się błąd: exception in thread "main" org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: main.Produkt. Kolejność zapisywania nie jest istotna.
W kodzie musiałem wyczyścić sesję (session.clear()), żeby uniknąć błędu: org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session.
Wynikiem działania programiku jest:
zapisane!
Kategorie: 1
Produkty: 2
produkt usuniety
Kategorie: 1
Produkty: 1
kategoria usunieta
Kategorie: 0
Produkty: 1
end


Niby ok, ale dlaczego istnieje produkt bez kategorii? Jeśli spojrzymy do bazy danych, to faktycznie zobaczymy, że w tabeli Produkty istnieje wpis "prod2" z pustą wartością klucza obcego kategory_id. Trochę to nieintuicyjne. Może można zrobić tak, że obiekty zależne (produkty) zapisywane i usuwane są razem z obiektem nadrzędnym (kategorią)? Oczywiście, że można. Trzeba się tylko trochę namęczyć:

Mapowania:

<?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="Kategoria" table="kategoria">
<id name="id" unsaved-value="null" column="kategory_id">
<generator class="sequence">
<param name="sequence">kat_seq</param>
</generator>
</id>
<property name="name" />
<property name="type" />
<property name="description" />
<set name="products" lazy="true" inverse="true" cascade="all">
<key column="kategory_id" />
<one-to-many class="Produkt"/>
</set>
</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="Produkt" table="produkt">
<id name="id" unsaved-value="null" column="produkt_id">
<generator class="sequence">
<param name="sequence">prod_seq</param>
</generator>
</id>
<property name="name" />
<property name="value" />
<many-to-one name="kategoria" class="Kategoria" not-null="true" column="kategory_id"/>
</class>
</hibernate-mapping>


Klasy POJO:


package main;

import java.util.HashSet;
import java.util.Set;

public class Kategoria {
Integer id;
String name;
String type;
String description;
Set<Produkt> products = new HashSet<Produkt>();

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 String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public Set<Produkt> getProducts() {
return products;
}

public void setProducts(Set<Produkt> products) {
this.products = products;
}

public void addProduct(Produkt product) {
if (product.getKategoria()==null)
product.setKategoria(this);
products.add(product);
}
}
package main;

public class Produkt {
Integer id;
String name;
Integer value;
Kategoria kategoria;

public Kategoria getKategoria() {
return kategoria;
}

public void setKategoria(Kategoria kategoria) {
this.kategoria=kategoria;
this.kategoria.getProducts().add(this);
}

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 Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}
}


W mapowaniach i POJO kluczowe są opcje "cascade", "inverse" oraz settery (setKatgoria, setProdukt, addProdukt) - bez nich nie proste operacje na javovych obiektach nie miałyby bezpośredniego przełożenia na sytuację w bazie danych.

No i main:


package main;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;


public class Main {

public static void main(String[] args) {

Session session = null;
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();

session.beginTransaction();

Kategoria kat = new Kategoria();
Produkt prod1 = new Produkt();
Produkt prod2 = new Produkt();
prod1.setName("prod1");
prod2.setName("prod2");
kat.addProduct(prod1);
prod2.setKategoria(kat);

session.save(kat);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("zapisane!");


session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
session.clear();
session.delete(prod1);
kat.getProducts().remove(prod1);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("produkt usuniety");


session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
session.clear();
System.out.println("produkty: "+kat.getProducts().size());
session.delete(kat);
session.flush();
session.getTransaction().commit();
session.close();
System.out.println("kategoria usunieta");
session = sessionFactory.openSession();
session.beginTransaction();
System.out.println("Kategorie: "+session.createQuery("from Kategoria").list().size());
System.out.println("Produkty: "+session.createQuery("from Produkt").list().size());
System.out.println("end");
}
}


Wynik działania programu:

zapisane!
Kategorie: 1
Produkty: 2
produkt usuniety
Kategorie: 1
Produkty: 1
produkty: 1
kategoria usunieta
Kategorie: 0
Produkty: 0
end

wtorek, 4 sierpnia 2009

System.setProperty() z wykorzystaniem Springa

Problem w jaki sposób wywołać System.setProperty ze Springa pojawił się, gdy po zdeployowaniu dwóch aplikacji na jednym serwerze, w logach jednej z nich pojawił się następujący wpis:

"A C3P0Registry mbean is already registered. This probably means that an application using c3p0 was undeployed, but not all PooledDataSources were closed prior to undeployment. This may lead to resource leaks over time. Please take care to close all PooledDataSources."

Na forum hibernate znalazłem informację, że trzeba w systemie ustawić zmienną:
com.mchange.v2.c3p0.management.ManagementCoordinator=com.mchange.v2.c3p0.management.NullManagementCoordinator

Niestety powyższe rozwiązanie nie wchodziło w rachubę (każdy z klientów musiałby zmodyfikować swój skrypt uruchamiający Tomcata). Jedyne sensowne rozwiązanie jakie przychodziło mi do głowy, to zmuszenie Springa do zadziałania.
Trochę się naszukałem, aż w końcu znalazłem jedno (niepoprawne) rozwiązanie mojego problemu. Po zmianach i uproszczeniach uzyskałem działającego bean'a :)

Oto on:


<bean id="sysprops" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass">
<value>java.lang.System</value>
</property>
<property name="targetMethod">
<value>setProperty</value>
</property>
<property name="arguments">
<list>
<value>com.mchange.v2.c3p0.management.ManagementCoordinator</value>
<value>com.mchange.v2.c3p0.management.NullManagementCoordinator</value>
</list>
</property>
</bean>


Gdyby kogoś interesowało, dlaczego taka a nie inna zmienna musiała być ustawiona, to zapraszam na forum hibernate.

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
}

środa, 4 lutego 2009

Zarządzanie sesją i transakcjami w HibernateDaoSupport

Pierwszy wpis na blogu i od razu wytaczam ciężkie działa - zarządzanie transakcjami bazodanowymi w środowisku Spring + Hibernate. Teoretycznie sprawa jest bardzo prosta - Spring, za pomocą HibernateDaoSupport, udostępnia cały zestaw metod służących bezbolesnej obsłudze danych (dodawanie, usuwanie) bez potrzeby zawracania sobie głowy sesjami, transakcjami, flushowaniem, commitowaniem, itd. Takie usprawnienie może wydawać się bardzo atrakcyjne, jednak w pewnych sytuacjach (czytaj: zazwyczaj) rodzi bardzo wiele problemów.
Poniżej postaram się opisać, w jaki sposób ujarzmić sesje i kontrolować transakcje podczas pracy ze springowym HibernateDaoSupport.

Pozwoję sobie pominąć szczegóły implementacyjne, skupiając się tylko na meritum.
Klasyczny przykład: mamy klasę Produkt i klasę Kategoria. Jeden produkt może znajdować się tylko w jednej kategorii, kategoria posiada zbiór produktów, a zatem relacja "jeden do wiele".

Klasy POJO i mapowania:

<class name="Kategoria" table="kategoria">
<id name="id" unsaved-value="null" column="kategory_id">
<generator class="sequence">
<param name="sequence">kat_seq</param>
</generator>
</id>
<property name="name"/>
<property name="type"/>
<property name="description"/>
<set name="products" lazy="true">
<key column="kategory_id"/>
<one-to-many class="Produkt"/>
</set>
</class>

<class name="Produkt" table="produkt">
<id name="id" unsaved-value="null" column="produkt_id">
<generator class="sequence">
<param name="sequence">prod_seq</param>
</generator>
</id>
<property name="name"/>
<property name="value"/>
<many-to-one name="kategoria" class="Kategoria"/>
</class>


Ogólne DAO dla wszystkich obiektów:

public class GenericDao extends HibernateDaoSupport {

public void store(Object object) {
getHibernateTemplate().saveOrUpdate(object);
};

public void delete(Object object) {
getHibernateTemplate().delete(object);
};

public List<Object> find(String query) {
return getHibernateTemplate().find(query);
}

public Object findUnique(String query) {
List <Object> list = getHibernateTemplate().find(query);
if (list.size() > 0)
return list.get(0);
else
return null;
}
}


Zakładam, że wszystkie DAO bądą dziedziczyć z powyższej klasy:

public class KategoriaDao extends GenericDao {
public Kategoria findKategoriaByName(String name) {
return (Kategoria)findUnique("... where name = "+name);
}
}

public class ProduktDao extends GenericDao {
public Produkt findProduktByName(String name) {
return (Produkt)findUnique("... where name = "+name);
}
}


Teraz praktyczne działanie:
public void testInsert() {
Kategoria kat = new Kategoria();
kat.setName("kat");
kat.setDescription("desc");
kategoriaDao.store(kat);
}


Po wywolaniu metody store() obiekt kat zostal zapisany w bazie danych. Bez tworzenia sesji, zatwierdzania transakcji, uspójniania bazy danych z objektami...
Po prostu - wywołanie jednej metody i kategoria jest w bazie danych. Proste, prawda?

Załóżmy, że w bazie danych istnieje kategoria o nazwie "kat". W bazie istnieją również cztery produkty należące do tej kategorii.

public void testSelect() {
Kategoria kat = kategoriaDao.findKategoriaByName("kat");
System.out.println(kat.getName()); //kat
System.out.println(kat.getDescription()); //desc
}


Hmm... jeszcze prostsza sprawa. Zapisuję do bazy jedną metodą (store), odczytuję też jedną (find...), nigdzie w kodzie nie pojawia się magiczne słowo
"session", HibernateDaoSupport jest rewelacyjne!

Ok, ale zanim stwierdzimy, że Spring do spółki z Hibernatem wszystko za nas zrobią, spójrzmy na kojeny przykład:

public void testSelectCollections() {
Kategoria kat = kategoriaDao.findKategoriaByName("kat");
for (Produkt p : kat.getProducts()) {
System.out.println(p.getName());
}
}


W trakcie działania metody rzucony zostanie wyjątek LazyInitializationException: no session or session was closed.
Hmm... pierwszy raz pojawia się więc słówko "session". Co się stało?
Kategoria ma zbiór Produktów, który jest inicjalizowany w sposób "leniwy" (), co oznacza, że podczas pobierania
kategorii z bazy danych, NIE zostaną pobrane produkty należące do danej kategorii. Produkty zostaną ściągnięte z bazy danych dopiero wtedy, gdy
będą potrzebne (nastąpi odwołanie do nich). Takie odwołanie następuje w pętli w metodzie testSelectCollections(). Dlaczego więc produkty nie będą
pobrane w tym momencie? Ponieważ sesja z bazą danych wygasła i połączenie jest nieaktywne. Tylko skąd wiadomo gdzie zaczyna i gdzie kończy się
sesja i jak nad tym zapanować? Otóż Spring samodzielnie zarządza sesjami otwierając je tylko i wyłącznie na czas wywołania metody z HibernateDaoSupport. Gwoli ścisłości:

public void testSelectCollections() {
//nie ma sesji
Kategoria kat = kategoriaDao.findKategoriaByName("kat"); //jest sesja
//nie ma sesji
for (Produkt p : kat.getProducts()) {
System.out.println(p.getName());
}
}


Na szczęście Spring pozwala zapanować nad sesją. Z pomocą przychodzi Aspect Oriented Programming (AOP), które (za pomocą np. adnotacji) wskazuje Springowi
gdzie zaczyna i gdzie kończy się transakcja, a co za tym idzie i sesja. Za pomocą adnotacji @Transactional oznaczamy metodę, która ma się wykonać w jednej transakcji:

@Transactional
public void testSelectCollections() {
Kategoria kat = kategoriaDao.findKategoriaByName("kat");
for (Produkt p : kat.getProducts()) {
System.out.println(p.getName());
}
}

Sesja trwa przez cały czas wykonywania metody. Jest tylko jeden warunek - adnotacje @Transactional działają tylko w klasach, które są beanami springowymi!!!
Problem rozwiązany!

Żeby wszystko zadziałało trzeba poinformować Springa o chęci skorzystania z adnotacji i z AOP. Poniżej przykładowy plik konfiguracyjny:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
classpath:org/springframework/beans/factory/xml/spring-beans-2.0.xsd
http://www.springframework.org/schema/tx
classpath:org/springframework/transaction/config/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop
classpath:org/springframework/aop/config/spring-aop-2.0.xsd">

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
...
</bean>

<bean id="localSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
...
</list>
</property>
<property name="hibernateProperties">
<ref bean="hibernateProperties" />
</property>
</bean>

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="localSessionFactory" />
</bean>
<tx:annotation-driven/>
</beans>



Z całego pliku najważniejsze są:
- <tx:annotation-driven> - dosłownie: "transakcje sterowane adnotacjami"
- poprawne schemaLocation

Trzeba też zwrócić uwagę na to, aby dołączane do projektu jary chciały ze sobą współpracować. Pewnego razu nie mogłem dojść do ładu z AOP,
ponieważ każdy potrzebny jar był "z innej parafii". Gruntowne porządki w pom'ie były niezbędne.

Poniżej fragment zależności z pom'a, które definiują poprawną konfigurację jar'ów (<springframework.version>2.0.6</springframework.version>):

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-dao</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-hibernate3</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jmx</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-remoting</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>