Pokazywanie postów oznaczonych etykietą hibernate. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą hibernate. Pokaż wszystkie posty

czwartek, 22 października 2009

Oracle + daty + JDBC

Ostatnio musiałem się zmierzyć z problemem związanym z obsługą dat w ORACLE 10. Wprowadzenie do tego problemu można znaleźć tutaj. W dużym skrócie: w przypadku insert do kolumny DATE wartości typu java.sql.Timestamp, czyli z poziomu Hibernate zapisujemy property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ), do bazy trafia zapisana przez nas data z czasem, ale bez milisekund - co jest zrozumiałe, bo DATE nie trzyma milisekund. Jeśli jednak spróbujemy odczytać zapisaną przez nas uprzednio krotkę, podając w warunku where zapisaną przed chwilą wartość - wynik jest pusty!
Pewnym popularnym rozwiązaniem, jest ustawienie specjalnego property dla ORACLE 9/10 JDBC driver: oracle.jdbc.V8Compatible=true. Konsekwencje użycia tego ustawienia można znaleźć tu.
Wyżej wymieniony problem można samemu zdiagnozować za pomocą testu.
Środowisko testowe korzysta z Oracle 10 g (10.2.0), JDBC Driver 10.2.0.4 a same testy są uruchamiane w JUNIT 4 za pomocą Spring TestContext Framework.
Encja, która będzie służyć do testów:

@Entity
@Table(name = "TestTable")
@SequenceGenerator(name = "seq_id", sequenceName = "SQ_TT")
public class TestEntity {
@Id
@GeneratedValue(generator = "seq_id")
Long i;

@Column
String state;

@Column(columnDefinition = "date")
@Temporal(TemporalType.TIMESTAMP)
Date d;

public TestEntity() {
}

public TestEntity( String state, Date d ) {
this.state = state;
this.d = d;
}

Test ukazujący w/w problem

@Test
public void test1(){
TestEntity t = new TestEntity("foo",new Date());
sessionFactory.getCurrentSession().save( t );
sessionFactory.getCurrentSession().flush();
sessionFactory.getCurrentSession().clear();
t = (TestEntity) sessionFactory.getCurrentSession().createQuery( "from TestEntity where d=?").setTimestamp( 0, t.getD()).uniqueResult();
assertThat( t, notNullValue() );
}

Test ten kończy się failure.
Można to łatwo poprawić, gdy "wyzerujemy" milisekundy

TestEntity t = new TestEntity("foo",new Date());
sessionFactory.getCurrentSession().save( t );
sessionFactory.getCurrentSession().flush();
sessionFactory.getCurrentSession().clear();
Calendar cal = Calendar.getInstance();
cal.setTime( t.getD() );
cal.set( Calendar.MILLISECOND, 0 );
t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=?").setTimestamp( 0, cal.getTime()).uniqueResult();
assertThat( t, notNullValue() );

Wykonanie tego testu kończy się sukcesem.
Na tym niestety problem się nie kończy. Pomimo tego, że udało nam się uzyskać poprawny wynik, to potrzeba "zerowania" milisekund jest strasznie upierdliwe, ale co gorsze zapytanie takie nie wykorzystują indeksu, który byłyby założone na kolumnie typu DATE. Można się o tym przekonać wykonując następujące polecenia (użytkownik system) :

select sql_id, child_number,sql_text from V$SQL where sql_text like '%from TestEntity%' order by last_load_time desc;

W wyniku dostaniemy sql_id oraz child_number, obie wartości służą jako input dla polecenia:

SELECT * FROM table(DBMS_XPLAN.display_cursor('<sql_id >',<child_number>));

W przypadku gdy mielibyśmy stworzony indeks np. na 2 kolumnach (jedna z nich będzie typu DATE), to zapytania z warunkiem where, które teoretycznie mogłyby wykorzystywać indeks (, np status=? and d>=? and d mogą się bardzo długo wykonywać.
Jeśli nawet ORACLE użyje indeksu to może zrobić to bardzo nieefektywnie.
Jest to spowodowane tym, że ORACLE będzie "promował" daty w indeksie do typu TIMESTAMP. Taka "promocja" powoduję, że przeglądnięcie indeksu jest nieefektywne. Widać to w planie zapytania:
|*  6 |      INDEX RANGE SCAN         | MOJ_INDEX |   741 |       | |                                                                                                                                                                            
--------------------------------------------------------------------------------------------------------

6 - access("state"=:3)
filter((INTERNAL_FUNCTION("D")>=:1 AND INTERNAL_FUNCTION("D")<:2))

"Promocja" typu jest prezentowana jako INTERNAL_FUNCTION
Rozwiązaniem problemu (brak użycia indeksu oraz potrzeba "zerowania" milisekund) byłoby "wypchnięcie" do bazy z poziomu JDBC wartości typu DATE a nie TIMESTAMP.
Można to zrobić poprzez:
  • używanie funkcji to_date - wymaga to ręcznego wprowadzania daty w postaci String

    @Test
    public void testToDate(){
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    String dateInStringFormat = createDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( t.getD() );
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=to_date(?,'YYYY-MM-DD HH24:MI:SS')").setString( 0, dateInStringFormat ).uniqueResult();
    assertThat( t, notNullValue() );
    }
  • użycie funkcji cast as
    - cast z TIMESTAMP na DATE nie tylko "rzutuje" jeden typ na drugi, ale także dokonuję zaokrąglenia:

    @Test
    public void testCastMilis500andAbove(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 500 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= cast (? as date)").setTimestamp( 0, calendar.getTime()).uniqueResult();
    assertThat( t, nullValue() );
    }

    @Test
    public void testCastMillisBelow500(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 499 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= cast (? as date)").setTimestamp( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );
    }
    Nie objedzie się w takim przypadku od "zerowania" milisekund. W przypadku Criteria API nie wiem w jaki sposób out-of -box używać wywołań funkcji. Chyba trzeba by stworzyć własnego Criterion
  • bezpośrednie użycie typu oracle.sql.DATE:

    @Test
    public void test3ImplicitOracleDateJDBC() throws SQLException{
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
    Timestamp ts = new Timestamp(t.getD().getTime());
    oracle.sql.DATE d = new oracle.sql.DATE(ts);
    long queryForLong = jdbcTemplate.queryForLong( "select count(* ) from TestTable where d=?", d );
    assertThat( queryForLong, equalTo( 1l ) );
    }

    Skuteczne, ale niewygodne. W przypadku HQL trzeba by pewnie stworzyć custom type, który byłby użyty przy bindowaniu parametrów przy pomocy wywołania

    Query setParameter(int position, Object val, Type type);

    W przypadku użycia Criteria API chyba najwygodniejsze byłoby stworzenie własnego Criterion
  • przy użyciu driveraJDBC w wersji 11.1.0.7.0 zadziała taka konstrukcja:

    @Test
    public void testImplicitType() throws SQLException{
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    Connection connection = sessionFactory.getCurrentSession().connection();
    PreparedStatement preparedStatement = connection.prepareStatement( "select count(*) from TestTable where d=?");
    preparedStatement.setObject( 1,new Timestamp(t.getD().getTime()) ,java.sql.Types.DATE );
    assertThat( preparedStatement.executeUpdate(), equalTo( 1 ));
    }
  • Oracle JDBC 9/10 (dla 9 nie sprawdzałem) driver undocumented "feature":

    @Test
    public void testSetTimeMilis500andAbove(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 500 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= ? ").setTime( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );
    }

    @Test
    public void testSetTimeMilisBelow500(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 499 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=?").setTime( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );;
    }

    Zgodnie z dokumentacją setTime() powinno zostać skonwertowane na TIME, ale konwersja jest robiona na DATE! Dla Criteria API trzeba by stworzyć własne Criterion, aby móc wymusić na Hibernate użycie setTime() - Hibernate out-of-box decyduje jaką metodę wywołac na PreparedStatement na podstawie typu/annotacji property na której ustawiamy warunek.

poniedziałek, 15 czerwca 2009

Oracle i Hibernate

W 2 ostatnich projektach dość intensywnie wykorzystujemy Hibernate jako ORM dla Oracle 10.2.0.1.0. (sterownik JDBC w wersji 10.2.0.4.0.) Takie rozwiązanie nie zawsze okazało się bezproblemowe.
  • "Wrong column type in {nazwa schematu.nazwa kolumny} for column {nazwa kolumny}. Found: date, expected: timestamp". Walidacja schematu przez Hibernate(hibernate.hbm2ddl.auto=validate) wyrzuca wyjątek przy mapowaniu
    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date published;

    gdy kolumna w bazie jest typu DATE.
    Oracle w wersji 9.2 wprowadził nowy typ danych TIMESTAMP, ale jednocześnie wprowadził mapowanie DATE =>java.sql.Date (
    javax.persistence.TemporalType.Date)
    . Typ java.sql.Date posiada jedynie dane o dacie bez info o czasie, przy czym Oraclowy DATE przetrzymuje dane zarówno o dacie jak i o czasie. Sprawdzenie przez drvier JDBC czy DATE mapuje się na java.sql.TimeStamp(
    javax.persistence.TemporalType.TIMESTAMP)
    jest false i rzucany jest wyjątek. Rozwiązań tego problemu jest kilka, przy czym większość z nich polega na modyfikacji definicji kolumn i/lub zmian w kodzie. Na nasze potrzeby najsensowniejsze okazało się wprowadzenie property (jako property połączenia lub property systemowe) oracle.jdbc.V8Compatible=true. Oznacza to powrót do mapowania między DATE a java.sql.TimeStamp zgodnie z tym jak to było dla Oracle 8i (który nie miał typu TIMESTAMP). W przypadku konfiguracji Spring dla c3p0 definicja DataSource wygląda mniej więcej następująco:

    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSourcePool" method="close">
    <property name="properties">
    <props>
    <prop key="oracle.jdbc.V8Compatible">true</prop>
    </props>
    </property>
    <property name="driverClass" value="${db.driver}"/>
    <property name="jdbcUrl" value="${db.url}"/>
    <property name="user" value="${db.username}"/>
    <property name="password" value="${db.pwd}"/>
    ...
    </bean>

    Ważne jest zachowanie kolejności: najpierw property properties wraz z entry oracle.jdbc.V8Compatible ustawione na true, a dopiero później pozostałe propertiesy.
  • Zgodnie z FAQ oracle.jdbc.V8Compatible zostało wprowadzone aby zapewnić kompatybilność z Oracle 8.1, który nie posiadał typu Timestamp. Oznacza to, że przy przekazywaniu do bazy danych obiektu typu java.sql.Timestamp (z poziomu Hibernate w postaci property typu java.util.Timestamp lub property typu java.util.Calendar/ java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ) zostanie on przekonwertowany na typ bazodanowe DATE , który nie przechowuje milisekund. Czyli, jeśli mamy w bazie kolumne ts typu TIMESTAMP to pomimo mapowania:

    @Column(name="ts)
    java.sql.Timestamp ts;
    lub

    @Column(name="ts)
    @Temporal(TemporalType.TIMESTAMP)
    java.util.Date/java.util.Calendar ts;
    insert/update property ts zapisze do bazy danych date z czasem ale bez milisekund.
    Co ciekawsze przy odczycie, property ts posiada milisekundy !!! (zakres milisekund zależy od tego czy zamapowaliśmy na javowe Calendar, Date czy Timestamp) . Jeśli chcemy z poziomu JDBC przechowywać w Oracle daty wraz z czasem z milisekundami nie można użyć oracle.jdbc.V8Compatible=true. Aby móc jednocześnie walidować schemat wystarczy wskazać typ kolumny jako wartość columnDefinition w annotacji @Column:

    @Column(columnDefinition="date")
    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date d;
    Dodatkowo warto pamiętac, że w przypadku zapisywania do kolumny DATE wartości typu java.sql.Timestamp (czyli z poziomu Hibernate property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ) do bazy zawsze trafi data z czasem bez milisekund. Jest to logiczne gdyż DATE nie trzyma milisekund. Jednak gdy najpierw zapiszemy do bazy wiersz, którego składową jest kolumna DATE wypełniana z pozimou javy wartością typu java.sql.Timestamp ( z poziomu Hibernate property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ), a następnie będziemy chcieli tą samą wartość wykorzystać w warunku where, może się okazać, że nie znajdziemy już takiej krotki. ORACLE najpierw dokona "promocji" wartości w kolumnie DATE do typu parametru wejściowego. W naszym przypadku wartości w kolumnie typu DATE zostaną "wypromowane" do typu TIMESTAMP, ponieważ nasz wejściowy parametr typu java.sql.Timestamp zostanie zamapowany po stronie bazy na typ TIMESTAMP, przy czym proces "promowanie" dodatkowo uzupełni milisekund zerami. Następnie dopiero zostanie wykonane porównywanie. Nawet jeśli ręcznie "wyzerujemy" milisekundy naszego wejściowego java.sql.Timestamp i w ten sposób znajdziemy żądany wiersz, to trzeba pamiętac, że w zapytaniu ORACLE nie użyje indeksu założonego na kolumnie typu DATE- indeks jest założony na typie DATE a nie TIMESTAMP. W przypadku gdy mielibyśmy włączone oracle.jdbc.V8Compatible takich problemów udałoby się uniknąć, ponieważ sterownik JDBC zamapuje nam typ java.sql.Timestamp na DATE. Więcej na ten temat możemy znaleźć tu i tu
  • "Wrong column type in {nazwa schematu.nazwa kolumny} for column {nazwa kolumny}. Found: char, expected: varchar2(255 char)". Walidacja schematu przez Hibernate(hibernate.hbm2ddl.auto=validate) wyrzuca wyjątek przy mapowaniu:

    @Column(name="name")
    private String name;
    gdy kolumna w bazie jest typu char(x) gdzie x>1.
    Problem wynika z błędu w Hibernate, który mapuje bazodanowy typ char na javowy Character.
    Gdy zmienimy mapowanie na

    @Column(name="name")
    private char name;

    wszystko jest oki, ale będziemy ograniczeni do przechowywania/odczytywania pojedyńczego znaku...
    Aby typ char(x) mapował się na String trzeba albo załadować patch opisany powyżej w zgłoszeniu, albo też wskazać konkretny typ bazdanowy na jaki będzie zamapowane nasze property.
    W tym drugim przypadku wystarczy dopisać columnDefinition

    @Column(name="name", columnDefinition="char")
    private String name;

  • Criterion uwzględniające escapowanie znaków specjalnych do wyszukiwania za pomocą like: http://levanhuy.wordpress.com/2009/02/19/providing-an-escape-sequence-for-criteria-queries/
    Trzeba jednak kod trochę zmodyfikować aby escapował znak służący jako escape character, czyli w tym przypadku trzeba escapowac znak '\'.
  • Wyszukiwanie Clob po zawartości. Posiadając mapowanie
    @Lob
    @Column(name = "CONTENT", nullable = false)
    private char[] content;
    do typu bazodanowego CLOB można wyszukiwać Cloby po zawartości. Nie chcę tutaj pisać na temat wydajności takiego rozwiązania, ale ogólnie jest to możliwe i działa. Wyszukiwanie przy pomocy like wygląda standardowo:
    List list = session.createQuery(" from Obj where content like ?").setParameter(0, "%Ala%".toCharArray()).list();

    W przypadku używanie Criteria najlepiej posłużyc się zmodyfikowaną klasą CriteriaEscape predstawioną powyżej. Dla Clob, które są zamapowane do char[] trzeba dokonać małej poprawki:
    public TypedValue[] getTypedValues(Criteria criteria,
    CriteriaQuery criteriaQuery) throws HibernateException {
    return new TypedValue[]{criteriaQuery.getTypedValue(criteria, propertyName, ("%" + value + "%").toCharArray())};
    }

    Idealnie wydaję się być usunięcie manualnej konkatenacji Stringów i użycie org.hibernate.criterion.MatchMode.toMatchString(). Nie tylko będzie to bardziej eleganckie, ale też bardziej funkcjonalne - można by przekazać implementacje org.hibernate.criterion.MatchMode w konstruktorze klasy i dzięki temu możemy łatwo sterować czy znak '%' ma być na początku/końcu czy i na początku i na końcu szukanej frazy.
    Trochę inaczej to wygląda gdy spróbujemy wyszukiwać Cloby za pomocą operatora równości. Próba wykonania kodu:
    Object object = currentSession.createQuery(" from Obj where content=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

    kończy się wyjątkiem: java.sql.SQLException: ORA-00932: inconsistent datatypes: expected - got CLOB. Nie da się ukryć, że typy faktycznie nie pasują. Można się posiłkować Oraclową funkcją TO_CHAR. W Oracle 10.1.0.4.2 takie zapytanie przejdzie:

    Object object = currentSession.createQuery(" from Obj where to_char(content)=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

    Oracle "po cichu" obetnie CLOB do pierwszych 4000 znaków i wtedy dokona porównania.
    Od wersji 10.2.0.1.0 jednak w takim przypadku dostaniemy błąd:
    SQL Error: ORA-22835: Buffer too small for CLOB to CHAR or BLOB to RAW conversion (actual: {wartość powyżej 4000}, maximum: 4000). Oracle tym razem już nie obetnie "po cichu" CLOB do 4000 znaków, ale można takie zachowanie na wymusić:
    Object object = currentSession.createQuery(" from Obj where to_char(substr(CONTENT,1,4000))=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

  • Przy okazji udało się natrafić na parę "niedoskonałości" Hibernate, które zostały już jakiś czas temu znaleziono, ale nie poprawione

    • Zdefiniowanie Criteria na klasie, która nie jest zmapowana, nie powoduje wystąpienia żadnego wyjątku. Jest to dziwne biorąc pod uwagę, że w HQL coś takiego nie przejdzie. Problem został już dawno zgłoszony
    • Hibernate generuje nie poprawne zapytanie w przypadku projekcji definiującej alias, który jest taki sam jak nazwa property a jednocześnie zdefinujemy restrykcji na tym property. Objawia się to wyjątkiem java.sql.SQLException: ORA-00904: "Y0_": invalid identifier. Dotyczy to jedynie aliasów dla root entity. Problem juz został dawno zgłoszony, przygotowano dla niego patch, ale bezpieczniej jest prefixowanie wszystkich restrykcji na root entity za pomocą aliasu"this"
      ProjectionList projectionList = Projections.projectionList().add( Projections.id().as( "id" ) ).add( Projections
      .property( "name" ).as( "name" ) ).add( Projections.property( "surname" ).as( "surname" ) );
      List result = currentSession.createCriteria(Author.class)
      .setProjection( projectionList )
      .add( Restrictions.eq( "this.surname", "Borchardt") )
      .setResultTransformer( new AliasToBeanResultTransformer(Author.class) )
      .list();
      albo korzystanie z podlasy AliasedProjection napisanej przez Chris Federowicza i Kevin Schmidta

      ProjectionList projectionList = Projections.projectionList().add( new CustomPropertyAliasProjection("id","id"))
      .add( new CustomPropertyAliasProjection("name", "name")).add( new CustomPropertyAliasProjection("surname","surname" ));
      List result = currentSession.createCriteria(Author.class)
      .setProjection( projectionList )
      .add( Restrictions.like( "surname", "Borchardt") )
      .setResultTransformer( new AliasToBeanResultTransformer(Author.class) )
      .list();

      Problem ten nie dotyczy wszystkich baz, np HSQLDB radzi sobie bez problemu gdy warunek w where jest definiowany na aliasie a nie na kolumnie, czyli bład ten tam nie występuję. Ogólnie jest to kolejny przykład "rozjazdu" kiedy system pracuję z bazą A a na testy podpinamy bazę B.


wtorek, 12 maja 2009

Hibernate i Spring DM Server

Spring Dm Server w wersji 1.0 odpaliłem już prawie rok temu. Udało mi się odpalić aplikację web, która pod spodem korzysta z Hibernate. Nie ukrywam, że nie poszło to gładko, ale w końcu się udało. Niestety rok po tym, chciałem pobawić się jeszcze raz, tym razem pod STS 2.0, cała wiedza wyparowała i musiałem jeszcze raz się przebijac i oglądać wyjątki w stylu java.lang.NoClassDefFoundError. W końcu zdecydowałem się to spisać, aby w przyszłości zaoszczędzic sobie ponownej straty czasu.
Aplikacja składa się z następujących projektów:
  • web - Spring DM web bundle
  • domain - obiekty domeny wykorzystywane przez moduł web oraz moduł api, korzysta z hsqlbd oraz commons dbcp
  • api - definiuje interfejs- kontrakt między warstwą web a warstwą dao
  • dao - implementuję interfejs zdefiniowany przez moduł api, dostarcza implementacji usługi OSGI

  1. No PAR deployment- aplikacja składa się z luźno wrzucanych bundli.
    • konfiguracja ORM określona za pomocą annotacji JPA (AnnotationSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
        Import-Library: org.springframework.spring.instrumented
        jak się można było spodziewać, importujemy bundle Hibernate

      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        Import-Bundle: com.springsource.javax.persistence;version="[1.0.0,1.0.0]",
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"

        pomimo, że nie używamy annotacji Hibernate (a jedynie JPA) bundle Hibernate musi zostac zaimportowany - w przeciwnym przypadku przy starcie bundle dao dostaniemy wyjątek: java.lang.NoClassDefFoundError: org/hibernate/proxy/HibernateProxy

    • konfiguracja ORM określona w plikach konfiguracyjnych xml (LocalSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
        Import-Library: org.springframework.spring.instrumented
        bez zmian
      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        tym razem bundle domain nie potrzebuje żadnego importu: ani JPA ani Hibernate

    Aby moc korzystać z HQL to dla No-PAR deployment wymagane jest zaimportowanie Hibernate dla bundle web.
    Manifest-Version: 1.0
    Web-DispatcherServletUrlPatterns: *.html
    Module-Type: Web
    Bundle-Version: 1.0.0
    Bundle-Name: HibernateTest_web Bundle
    Bundle-ManifestVersion: 2
    Web-ContextPath: HibernateTest
    Bundle-SymbolicName: foo.bar.HibernateTest_web
    Import-Package: foo.bar.dao.api,
    foo.bar.domain;version="[1.0.0,1.0.0]"
    Import-Bundle: com.springsource.org.apache.taglibs.standard;version="[1.1.2,1.1.2]",
    com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
    Import-Library: org.springframework.spring.instrumented

    W innym przypadku, przy próbie wykonania zapytania HQL zobaczymy: org.hibernate.QueryException: ClassNotFoundException: org.hibernate.hql.ast.HqlToken. Hibernate ładuję cześć klas za pomocą Thread context classloadera, czyli w naszym przypadku za pomocą ClassLoadera, który załadował moduł web. Najciekawsze jest to, że zapytania korzystające z Criteria API, operacje get/load działają bez tego importu. Jest on jedna wymagany do odpalania zapytań w HQL.

  2. PAR deployment - nasze bundle zostają zapakowane w archiwum PAR
    • konfiguracja ORM określona za pomocą annotacji JPA (AnnotationSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]";import-scope:=application
        Import-Library: org.springframework.spring.instrumented
        tym razem importujemy bundle Hibernate z dodatkowym atrybutem import-scope:=application, który jest pewnym rozszerzeniem Spring DM Server

      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        Import-Bundle: com.springsource.javax.persistence;version="[1.0.0,1.0.0]"

        używamy jedynie annotacji JPA, nie musimy importować bundle Hibernate

    • konfiguracja ORM określona w plikach konfiguracyjnych xml (LocalSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]";import-scope:=application
        Import-Library: org.springframework.spring.instrumented
        bez zmian
      • domain

        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        skoro tym razem nie używamy JPA to nie potrzebujemy importu bundle JPA

    Tym razem do bundle web nie musimy importować bundle Hibernate aby móc korzystać z HQL

    Manifest-Version: 1.0
    Web-DispatcherServletUrlPatterns: *.html
    Module-Type: Web
    Bundle-Version: 1.0.0
    Bundle-Name: HibernateTest_web Bundle
    Bundle-ManifestVersion: 2
    Web-ContextPath: HibernateTest
    Bundle-SymbolicName: foo.bar.HibernateTest_web
    Import-Package: foo.bar.dao.api,
    foo.bar.domain;version="[1.0.0,1.0.0]"
    Import-Bundle: com.springsource.org.apache.taglibs.standard;version="[1.1.2,1.1.2]"
    Import-Library: org.springframework.spring.instrumented

    Dzięki importowaniu bundle Hibernate z atrybutem import-scope:=application nie ma potrzeby importowania Hibernate ani w bundle web ani w bundle domain. Po podłączeniu się za pomocą telnet do konsoli Equinox możemy wyświetlić nagłówki bundli wchodzące w skład naszego PAR, w ten sposób zobaczymy że, każdy z nich "automagicznie" importuje pakiety Hibernate.