niedziela, 12 kwietnia 2009

leniwe ładowanie właściwości w Hibernate

Święta, święta i... jeszcze został jeden dzień obżarstwa. Tu barszczyk, tam kiełbaska, mięsko, sałatka, mazurek. Wszystkiego trzeba spróbować. A gdyby tak jeść tylko to, co jest w danej chwili niezbędne? Po prostu nie przejadać się. Chyba byłoby zdrowiej...

To takie moje świąteczne przemyślenia. W programowaniu jednak jest trochę jak w życiu. Często mamy do czynienia z podobnymi sytuacjami. Nie zjadamy co prawda w naszych programach wszystkiego, co jest pod ręką, ale wyciągając z bazy danych obiekty przy użyciu narzędzi ORM (Object Relational Mapping), pobieramy je wyciągając wszystkie kolumny z bazy danych. Inicjujemy wszystkie, nawet te niepotrzebne w danej chwili właściwości obiektów.

O ile zazwyczaj nikomu to nie przeszkadza, o tyle w przypadku pobierania właściwości o dużych rozmiarach (np. duże obiekty binarne, tekstowe) może to się odbić czkawką i negatywnie wpłynąć na wydajność aplikacji.

Na szczęście Hibernate, będący chyba najpopularniejszym narzędziem mapowania obiektowo relacyjnego, umożliwia selektywne pobieranie właściwości obiektów. Wyboru, które kolumny pobieramy wyciągając obiekt z relacyjnej bazy danych dokonujemy za pomocą parametru fetch adnotacji @Basic. Jeżeli konfigurację przejścia obiektowo relacyjnego przechowujemy w plikach XML, za to zachowanie odpowiedzialny jest atrybut lazy znacznika property.

Domyślnie pobierane są wszystkie atrybuty obiektów, nie będące kolekcjami, co odpowiada ustawieniom:

@Basic(fetch = FetchType.EAGER)
oraz

<property lazy="false" />

Sprawdźmy więc w akcji, jak działa konfiguracja leniwego pobierania wybranych właściwości obiektów w Hibernate. Mój przykładowy program powstał na podstawie przykładów ze strony projektu Hibernate.

Pobierał będę encję Message

package pl.matt.model;

public class Message {
private Long id;
private String text;

Message() {}

public Message(String text) {
this.text = text;
}

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

public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}


konfigurację przejścia obiektowo relacyjnego zawarłem w pliku XML Message.hbm.xml


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

<hibernate-mapping>

<class
name="pl.matt.model.Message"
table="MESSAGES">

<id
name="id"
column="MESSAGE_ID"
>
<generator class="native"/>
</id>

<property
name="text"
column="MESSAGE_TEXT"
lazy="false"
/>

</class>

</hibernate-mapping>


Na prosty program pobierający obiekt z bazy danych składa klasa Main

package pl.matt.main;

import org.hibernate.Session;
import org.hibernate.Transaction;

import pl.matt.model.Message;
import pl.matt.utils.HibernateUtil;

public class Main {

public static void main(String[] args) {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();

Message message = (Message) session.get(Message.class, 1l);
System.out.println(message.getId());

tx.commit();
session.close();

HibernateUtil.shutdown();
}
}


Klasa narzędziowa HibernateUtil zawiera kilka metod ułatwiających pracę z Hibernate:

package pl.matt.utils;

import org.hibernate.*;
import org.hibernate.cfg.*;

/**
* Startup Hibernate and provide access to the singleton SessionFactory
*/
public class HibernateUtil {

private static SessionFactory sessionFactory;

static {
try {
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
throw new ExceptionInInitializerError(ex);
}
}

public static SessionFactory getSessionFactory() {
// Alternatively, we could look up in JNDI here
return sessionFactory;
}

public static void shutdown() {
// Close caches and connection pools
getSessionFactory().close();
}
}


W pliku hibernate.cfg.xml konfiguruję połączenie z bazą danych HSQL oraz ustawiam wartość true parametru show_sql który umożliwia podglądanie zapytań SQL wykonywanych przez Hibernate.


<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>

<property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="hibernate.connection.url">jdbc:hsqldb:hsql://localhost</property>
<property name="hibernate.connection.username">sa</property>

<!-- SQL to stdout logging -->
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<property name="use_sql_comments">true</property>

<property name="hibernate.hbm2ddl.auto">update</property>

<property name="dialect">org.hibernate.dialect.HSQLDialect</property>

<mapping resource="pl/matt/model/Message.hbm.xml"/>

</session-factory>
</hibernate-configuration>


Uruchamiam program i zgodnie z oczekiwaniami widzę zapytanie pobierające 2 właściwości z tabeli MESSAGES:

Hibernate:
/* load pl.matt.model.Message */ select
message0_.MESSAGE_ID as MESSAGE1_0_0_,
message0_.MESSAGE_TEXT as MESSAGE2_0_0_
from
MESSAGES message0_
where
message0_.MESSAGE_ID=?


zmieniam w pliku Message.hbm.xml wartość atrybutu lazy na true. Uruchamiam ponownie program i tym razem niezgodnie z oczekiwaniami widzę ponownie:

Hibernate:
/* load pl.matt.model.Message */ select
message0_.MESSAGE_ID as MESSAGE1_0_0_,
message0_.MESSAGE_TEXT as MESSAGE2_0_0_
from
MESSAGES message0_
where
message0_.MESSAGE_ID=?


Hmm... słabo.
Zagłębiłem się w dokumentację i już widzę, że nie będzie łatwo:

Note

To enable property level lazy fetching, your classes have to be instrumented: bytecode is added to the original one to enable such feature, please refer to the Hibernate reference documentation. If your classes are not instrumented, property level lazy loading is silently ignored.

Nawet nie wiedziałem, że jest taki czasownik instrument. W każdym razie moje ustawienia zostały po cichu zignorowane, jak to ładnie określiła dokumentacja Hibernate. Trzeba by to zmienić. Szperam więc dalej w poszukiwaniu sposobu zinstrumetowania klas.

Wykonać ten proces można antowym zadaniem:

<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.cglib.InstrumentTask">
<classpath path="${build.dir}"/>
<classpath refid="project.classpath"/>
</taskdef>

<instrument verbose="true">
<fileset dir="${build.dir}/pl/matt/model">
<include name="*.class"/>
</fileset>
</instrument>
</target>


W dokumentacji Hibernate jest mały błąd, gdyż klasa zadania instrument InstrumentTask nie znajduje się w pakiecie org.hibernate.tool.instrument tylko w org.hibernate.tool.instrument.cglib.

Jako, że powyższe zadanie zmienia kod plików *.class, muszę uruchamiać te skompilowane i zmienione przez ANTa pliki. Posłużę się do tego celu poniższym plikiem build.xml:

<project name="hibernateLazyBasic" default="compile" basedir=".">

<!-- Name of project and version -->
<property name="proj.name" value="hibernateLazyBasic"/>
<property name="proj.shortname" value="hibernateLazyBasic"/>
<property name="version" value="1.0"/>

<!-- Global properties for this build -->
<property name="database.dir" value="database"/>
<property name="src.java.dir" value="src/java"/>
<property name="lib.dir" value="lib"/>
<property name="build.dir" value="build"/>

<!-- Classpath declaration -->
<path id="project.classpath">
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
<include name="**/*.zip"/>
</fileset>
</path>

<!-- Useful shortcuts -->
<patternset id="meta.files">
<include name="**/*.xml"/>
<include name="**/*.properties"/>
</patternset>

<!-- Clean up -->
<target name="clean" description="Clean the build directory">
<delete dir="${build.dir}"/>
<mkdir dir="${build.dir}"/>
</target>

<!-- Compile Java source -->
<target name="compile">
<mkdir dir="${build.dir}"/>
<javac srcdir="${src.java.dir}"
destdir="${build.dir}"
classpathref="project.classpath"/>
</target>

<!-- Copy metadata to build classpath -->
<target name="copymetafiles">
<mkdir dir="${build.dir}"/>
<copy todir="${build.dir}">
<fileset dir="${src.java.dir}">
<patternset refid="meta.files"/>
</fileset>
</copy>
</target>

<target name="run-instrumented" depends="clean, instrument, copymetafiles">
<java fork="true"
classname="pl.matt.main.Main"
classpathref="project.classpath">
<classpath path="${build.dir}"/>
</java>
</target>

<target name="run" depends="clean, compile, copymetafiles">
<java fork="true"
classname="pl.matt.main.Main"
classpathref="project.classpath">
<classpath path="${build.dir}"/>
</java>
</target>

<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.cglib.InstrumentTask">
<classpath path="${build.dir}"/>
<classpath refid="project.classpath"/>
</taskdef>
<instrument verbose="true">
<fileset dir="${build.dir}/pl/matt/model">
<include name="*.class"/>
</fileset>
</instrument>
</target>

</project>


zadanie o nazwie run-instrumented kompiluje, przeprowadza instrumentację klas z pakiedu pl.matt.model i uruchamia program.

Po jego wykonaniu, otrzymuję na konsoli:

[java] Hibernate:
[java] /* load pl.matt.model.Message */ select
[java] message0_.MESSAGE_ID as MESSAGE1_0_0_
[java] from
[java] MESSAGES message0_
[java] where
[java] message0_.MESSAGE_ID=?


Ładowane jest tylko pole ID z tabeli MESSAGES, czyli zgodnie z oczekiwaniami. Kolumna MESSAGE_TEXT pobierana jest z bazy osobnym zapytaniem:

[java] Hibernate:
[java] /* sequential select
[java] pl.matt.model.Message */ select
[java] message_.MESSAGE_TEXT as MESSAGE2_0_
[java] from
[java] MESSAGES message_
[java] where
[java] message_.MESSAGE_ID=?

dopiero wtedy, kiedy będzie potrzebna.

Generalnie wszystko działa, tylko trzeba odrobinę zmienić proces kompilacji programu.
Kod źródłowy przedstawiający powyższe rozwiązanie znajdziesz tutaj

a że święta już w połowie, pozostaje mi życzyć Wam mokrego dyngusa.

1 komentarz:

mapi pisze...

"Domyślnie pobierane są wszystkie atrybuty obiektów" nie jest to prawdą dla kolekcji,

"Lazy collection fetching - a collection is fetched when the application invokes an operation upon that collection. (This is the default for collections.)" za Improving performance.