czwartek, 16 października 2008

Moc dialektów Hibernate

Od czasu do czasu mam przyjemność przeprowadzania rozmów rekrutacyjnych z kandydatami do pracy. Osobom, które deklarują znajomość Oracle PL/SQLa i przebrną przez pierwsze pytanie dotyczące sekwencji (a 2/3 znawców PL/SQLa nie wie, co to jest sekwencja) zadaję następujące zadanko:

Przedstawiam encję Osoby:


i proszę o napisanie zapytania pokazującego 3 najmniej zarabiające osoby. Jeżeli kandydat zna również MySQL, proszę o napisanie tego zapytania w 2 wersjach. Bajer polega na tym, że w MySQLu do ograniczenia listy wyników do trzech wystarczy wykorzystać klauzulę limit:

SELECT * FROM OSOBY ORDER BY OSOBY.PENSJA LIMIT 3;


podczas gdy w Oracle takiej magicznej klauzuli nie ma. Jest tam za to możliwość wyświetlenia wierszy o określonym numerze (indeksie):

SELECT * FROM OSOBY WHERE NUMROW <= 3 ORDER BY OSOBY.PENSJA;

ale numer wiersza jest sprzed sortowania, zatem powyższe zapytanie wyświetli trzech pierwszych użytkowników posortowanych po wielkości pensji. Aby osiągnąć zamierzony cel, trzeba skorzystać z podzapytania:

SELECT * FROM (SELECT * FROM OSOBY OSOBY ORDER BY OSOBY.PENSJA ) WHERE NUMROW <= 3;

Zapytanie o to samo, a jednak inaczej skonstruowane (a niby SQL to język deklaratywny).
Zaczęło mnie zastanawiać, co na to Hibernate. Czy jest na tyle mądry, że potrafi to samo zapytanie HQLowe zapisać na 2 zupełnie różne sposoby? Czy jego dialekty, ograniczają się jedynie do podmiany słów kluczowych i nazw typów, czy są mechanizmem dużo potężniejszym? Przekonajmy się.

Na bazie aplikacji Hibernate Getting Started - Hello World stworzyłem prosty projekt w środowisku Eclipsie. Skasowałem pliki Message.java oraz Message.hbm.xml.

Stworzyłem za to klasę Person:

public class Person {
private Long id;
private String name;
private int salary;

public Person() {

}

public Person(String text, int salary) {
this.name = text;
this.salary = salary;
}

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

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

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

}

oraz plik Person.hbm.xml odzwierciedlający zawartość klasy Person w bazie danych:

<?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="hello.Person" table="PEOPLE">

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

<property name="name" column="NAME" type="string" />

<property name="salary" column="SALARY" type="integer" />

</class>

</hibernate-mapping>

Skonfigurowałem w pliku hibernate.cfg.xml połączenie z bazą danych MySQL:

<?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">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>

<property name="hibernate.connection.username">test</property>
<property name="hibernate.connection.password">test</property>

<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>

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

<property name="hibernate.hbm2ddl.auto">update</property>
<mapping resource="hello/Person.hbm.xml" />
</session-factory>
</hibernate-configuration>

Dzięki parametrowi
<property name="hibernate.hbm2ddl.auto">update</property>
schemat bazy danych zostanie dostosowany do aplikacji (zostaną utworzone lub zmodyfikowane odpowiednie tabele) przy jej starcie.
Natomiast parametr
<property name="show_sql">true</property>
pozwala wyświetla w konsoli zapytań SQL, które wykonuje Hibernate.

Parametr
<property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
określa tzw. dialekt Hibernate, który odpowiada za tłumaczenie zapytań Hibernate na zapytania SQLowe zrozumiałe dla konkretnego systemu zarządzania bazami danych.

Do tego zmieniłem zawartość metody main() klasy HelloWorld:

public static void main(String[] args) {
Session session = HibernateUtil.getSessionFactory().openSession();
Collection people = session.createCriteria(Person.class).list();
if (people.size() < 5) {
Transaction tx = session.beginTransaction();

Person p1 = new Person("Adam", 100);
Person p2 = new Person("Marek", 200);
Person p3 = new Person("Adrian", 150);
Person p4 = new Person("Konrad", 180);
Person p5 = new Person("Romek", 120);

session.persist(p1);
session.persist(p2);
session.persist(p3);
session.persist(p4);
session.persist(p5);

tx.commit();
}

Query query = session.createQuery("FROM Person p ORDER BY p.salary").setMaxResults(3);
List list = query.list();
for (Person person : list) {
System.out.println(person.getName() + " " + person.getSalary());
}

session.close();
HibernateUtil.shutdown();
}

Na początek prostsze zadanie - zapytanie do bazy MySQL. Odpalenie aplikacji kończy się spodziewanym wynikiem:

Hibernate: select person0_.PERSON_ID as PERSON_ID0_, person0_.NAME as NAME0_,
person0_.SALARY as SALARY0_ from PEOPLE person0_ order by person0_.SALARY limit ?
Adam 100
Romek 120
Adrian 150

Czas więc na uruchomienie aplikacji z bazą Oracle. Na początku, trzeba zmienić odrobinę plik Person.hbm.xml tak, aby unikalny identyfikator osoby generowany był z sekwencji:

<id name="id" column="PERSON_ID">
<generator class="sequence">
<param name="sequence">people_seq</param>
</generator>
</id>

oraz plik hibernate.cfg.xml, konfigurując połączenie z bazą Oracle:

<?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">oracle.jdbc.driver.OracleDriver</property>
<property name="hibernate.connection.url">jdbc:oracle:thin:@127.0.0.1:1521/xe</property>
<property name="dialect">org.hibernate.dialect.OracleDialect</property>

<property name="hibernate.connection.username">test</property>
<property name="hibernate.connection.password">test</property>


<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>

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

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

<mapping resource="hello/Person.hbm.xml" />

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

Uruchamiam aplikację i ...

Hibernate: select * from
( select person0_.PERSON_ID as PERSON_ID0_, person0_.NAME as NAME0_,
person0_.SALARY as SALARY0_ from PEOPLE person0_ order by person0_.SALARY )
where rownum <= ?
Adam 100
Romek 120
Adrian 150

Czyli bez niespodzianki. Dialekty Hibernate są więc dość potężnym mechanizmem. Pozwalają nie tylko na zmianę pojedynczych słów kluczowych, ale także konstrukcji całych zapytań. Trudno się zresztą dziwić. Hibernate to już bardzo dopracowany produkt.

Niech no mi się teraz nawinie ktoś na rozmowie rekrutacyjnej, ze znajomością Oracle PL/SQLa i Hibernate...