Ś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
tutaja że święta już w połowie, pozostaje mi życzyć Wam mokrego dyngusa.