Hibernate интересные моменты.
Замапленый объект может находиться в четырех состояниях:
- transient-объект создаётся командой new, может быть заполнен данными но, ни когда не сохранялся в сессии т.е. не ассоциирован со строкой в таблице БД.
- persistent-объект, объект в данный момент связанный с некоторой сессией, hibernate сессия "работает" с экземпляром объекта в данный промежуток времени, т.е. объект ассоциирован со строкой в таблице БД. Получить persistent-объект можно двумя способами: 1-й способ запросить объект такими методами как get(), load(), list(), uniqueResult(), iterate(), scroll(), find(), getReference(). 2-й способ перевести наш transient-объект в persistent-объект путем вызова таких методов как save(), saveOrUpdate(), persist(), merge().
- detached-объект - это persistent-объект отсоединенный от сессии, это состояние объекта возникает после закрытия сессии close(), которая работала с объектом до этого или при вызовах методов evict(), clear() сессии. Переход из состояния detached в persistent объектом возможно при вызове методов сессии - update(), saveOrUpdate(), merge().
- removed - объект - это persistent-объект удаленный в сессии методом delete() или remove() в jpa.
Еще хотелось бы сказать пару слов о detached-объектах, их можно использовать дальше, при работе с новой сессией, если к ним применить такие команды как update(); saveOrUpdate() или
merge() (в jpa), то они переходят в состояние persistent - в Hibernate такие операции называются reattached mode или merging mode - в jpa.
Замапленный объект должен удовлетворять требованиям, тогда и только тогда он будет правильно взаимодействовать с hibernate:
- Иметь конструктор по умолчанию без параметров.
- Переопределять методы toString(), Equals(), HashCode().
- Иметь поле "первичного ключа" (id) как минимум, как максимум сложный составной ключ.
- Класс сущности не должен быть объявлен как final класс.
- Класс сущности должен наследовать интерфейс Serializable.
Немножко отступим от темы и поговорим о Serializable. С каждым сериализуемым классом связан уникальный идентификационный номер. Если вы не указываете этот идентификатор явно, декларируя поле private static final long с названием serialVersionUID, система генерирует его автоматически, используя для класса сложную схему расчетов. При этом на автоматически генерируемое значение оказывают влияние название класса, названия реализуемых им интерфейсов, а также все открытые и защищенные члены. Если вы каким-то образом поменяете что-либо в этом наборе, например, добавите простой и удобный метод, изменится
и автоматически генерируемый serial version UID. Следовательно, если вы не будете явным образом декларировать этот идентификатор, совместимость с предыдущими версиями будет потеряна. Клиенты, которые пытаются сериализовать объект с помощью старой версии класса и десериализовать его уже с помощью новой версии, получат сбой программы. И так, как создать static final long serialVersionUID? Пусть у нас есть скомпилированный класс src/org/hibernate/tutorial/annotations/Person.class перейдем в папку src и запустим команду serialver которая входит в jdk:
C:\...\src>serialver org.hibernate.tutorial.annotations.Person
org.hibernate.tutorial.annotations.Person:
static final long serialVersionUID = -7593775012501239455L;
C:\...\src>
копируем и вставляем в наш класс:
package org.hibernate.tutorial.annotations;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.hibernate.annotations.Proxy;
import javax.persistence.*;
import javax.persistence.criteria.Fetch;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@Table(name = "Persons")
@ToString
@EqualsAndHashCode
//@Proxy(lazy = true)//@Proxy(lazy = false)
public class Person implements Serializable {
static final long serialVersionUID = -7593775012501239455L;
@Id
@GeneratedValue()
@Column(name = "personId")
private Long id;
@Column(name = "fName")
private String firstName;
@Column(name = "sName")
private String sureName;
/* @OneToMany(cascade = CascadeType.ALL
, fetch = FetchType.EAGER,
mappedBy = "person1")
@Getter
@Setter
private List events = new ArrayList();
*/
public String getSureName() {
return sureName;
}
public void setSureName(String sureName) {
this.sureName = sureName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
Различие между командами load() и get(), оба метода предназначены для получения объекта из базы данных, отличие в том что если метод get() не находит объект в БД то возвращает null, метод load() может вернуть прокси объект (если разрещена реалиализация lazy объектов - по умолчанию в Hibernate 3), вместо реального объекта. Заполнение данными proxy-объекта происходит, только после вызова любого метода прокси объекта исключение составляет запрос первичного ключа (getId()). Hibernate выполняет sql запрос только тогда, когда нужны реальные данные, если существует такая запись в БД то она заполняет данными proxy-объект. Все это называется ленивой загрузкой и её можно отменить выставив параметр @Proxy(lazy = false) у объекта. Если запись не найдена в БД, то происходит Exception (org.hibernate.ObjectNotFoundException.class). Смотрите коментарии в примерах:
@Test()
public void loadProxyPersons(){
Session session = sessionFactory.openSession();
session.beginTransaction();
//в бд нет записи 10L load() возвращает proxy объект
Person person2 = (Person) session.load(Person.class, 10L);
//проверяем существование proxy объекта
assertNotNull(person2);
session.getTransaction().commit();
session.close();
}
@Test(expected = org.hibernate.ObjectNotFoundException.class)
public void loadProxyPersonsException(){
Session session = sessionFactory.openSession();
session.beginTransaction();
// в бд нет записи 10L load() возвращает proxy объект
Person person2 = (Person) session.load(Person.class, 10L);
//пытаемся заполнить проси объект данными, вызываем исключение org.hibernate.ObjectNotFoundException.class
person2.getFirstName();
session.getTransaction().commit();
session.close();
}
@Test()
public void getNullPerson(){
Session session = sessionFactory.openSession();
session.beginTransaction();
//запращиваем объект
Person person2 = (Person) session.get(Person.class, 10L);
//проверяем что get возвращает null
assertNull(person2);
session.getTransaction().commit();
session.close();
}
По умолчанию Hibernate, всегда использует кеш первого уровня в транзакции,
его не возможно отключить. Т.е когда мы производим операции над
persistent-объектом изменения не сразу попадают в БД, это нужно
для уменьшения количества выполняемых sql запросов к базе данных.
Допустим объект модифицируется в одной сессии несколько раз,
все изменения происходят в памяти (aka кеше первого уровня), в итоге генерируется 1 update
sql запрос который скидывает только последние изменения по последнему состоянию,
все предыдущие состояния объекта не учитываються и не сохраняюстя в БД.
Расмотрим пример:
@Test
public void testHibernateCacheLevelOne() {
Person person = new Person();
person.setFirstName("Vit");
person.setSureName("Lopanov");
Statistics statistics = sessionFactory.getStatistics();
Session session = sessionFactory.openSession();
session.beginTransaction();
session.saveOrUpdate(person);
session.getTransaction().commit();
session.close();
//включаем статистику для сбора информации.
statistics.setStatisticsEnabled(true);
session = sessionFactory.openSession();
session.beginTransaction();
// загружаем объект из БД - выполняется sql:
//Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
person = (Person)session.load(Person.class,1L);
// проверяем сохранен ли наш объект в кеше - true
System.out.println("contains in cache: "+session.contains(person));
person.setFirstName("Tom");
session.saveOrUpdate(person);
//.... выполняем еще какие-то операции
//запрашиваем заново наш объект, sql не выполняется, объект подсовывается из кеша
person = (Person)session.load(Person.class,1L);
// проверяем находиться ли наш объект в кеше - true
System.out.println("contains in cache: "+session.contains(person));
person.setSureName("Aristov");
session.saveOrUpdate(person);
// выгружаем объект из кеша
if (session.contains(person)) session.evict(person);
// объект не найден в кеше, выполняем sql запрос
//Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
person = (Person)session.load(Person.class,1L);
person.setFirstName("Oleg");
session.saveOrUpdate(person);
session.getTransaction().commit();
session.close();
System.out.println("InsertCount: "+statistics.getEntityInsertCount()+" " +
"UpdateCount: "+statistics.getEntityUpdateCount()+" " +
"FlushCount: "+statistics.getFlushCount()+" " +
"TransactionCount: "+statistics.getTransactionCount()+" " +
"SuccessfulTransactionCount: "+statistics.getSuccessfulTransactionCount());
}
этот пример даёт такой лог, обратите внимание что мы меняли состояние объекта 3
раза (setSureName("Aristov"), setFirstName("Oleg") ... ), а sql - update выполнился лишь один раз:
Hibernate: insert into Persons (personId, fName, sName) values (null, ?, ?)
contains in cache: true
Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
contains in cache: true
Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
Hibernate: update Persons set fName=?, sName=? where personId=?
InsertCount: 0 UpdateCount: 1 FlushCount: 1 TransactionCount: 1 SuccessfulTransactionCount: 1
Отсюда вывод что: При использовании методов save(), update(), saveOrUpdate(), load(), get(), list(), iterate(), scroll() всегда будет задействован кеш первого уровня,
для того что бы немедленно сохранить объект в БД, после вызовов методов save(), update(), saveOrUpdate(),
нужно выполнить команду session.flush(). А для непосредственной загрузки объекта
из БД sql-м, методами load(), get(), list(), iterate(), scroll(),
а не из кеша первого уровня, нужно вызвать команду session.evict(person)
- удаления объекта из кеша. Так же сушествует команда которая очищает полностью
кеш первого уровня session.clear();
Продолжая тему кеша первого уровня, нельзя не упомянуть про Batch Inserts,
загрузку множества объектов, для этого нужно контролировать кеш первого уровня
от разростания и вовремя скидывать данные в БД, очищая при этом кеш:
@Test
public void batchInsert() {
Person person;
Session session = sessionFactory.openSession();
session.beginTransaction();
for (int i = 0; i < 10000; i++) {
person = new Person();
person.setFirstName("Vit" + i);
person.setSureName("Lopanov" + i);
session.saveOrUpdate(person);
if (i % 30 == 0) {
//скидываем изменения из кеша первого уровня
session.flush();
//очищаем кеш первого уровня
session.clear();
}
}
session.getTransaction().commit();
session.close();
}
Не забудте установить параметр hibernate.jdbc.batch_size и отключить кеш второго уровня (second-level cache). И на последок вкладываю maven pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.hibernate.tutorials</groupId>
<artifactId>hibernate-tutorials</artifactId>
<version>4.1.7.Final</version>
<packaging>pom</packaging>
<name>Hibernate Getting Started Guide Tutorials</name>
<description>Aggregator for the Hibernate tutorials presented in the Getting Started Guide</description>
<properties>
<!-- Skip artifact deployment -->
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<modules>
<module>annotations</module>
<module>annotations0</module>
<module>annotations1</module>
<module>annotations2</module>
<module>annotations3</module>
<module>annotations4</module>
<module>annotations5</module>
<module>annotations6</module>
<module>annotations7</module>
<module>annotations8</module>
</modules>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.7.Final</version>
</dependency>
<!-- Hibernate uses jboss-logging for logging, for the tutorials we will use the sl4fj-simple backend -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.1</version>
</dependency>
<!-- The tutorials use JUnit test cases to illustrate usage -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<!-- The tutorials use the H2 in-memory database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.2.145</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.1.7.Final</version>
</dependency>
<!--<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.0</version>
</dependency>-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.6</version>
</dependency>
<!--<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.3</version>
</dependency> -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.11.0</version>
<scope>provided</scope>
</dependency>
<!--<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>-->
<!-- Hibernate c3p0 connection pool -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>4.1.7.Final</version>
</dependency>
<!-- Hibernate jbosscache-core -->
<dependency>
<groupId>org.jboss.cache</groupId>
<artifactId>jbosscache-core</artifactId>
<version>3.2.5.GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
<testResources>
<testResource>
<filtering>false</filtering>
<directory>src/test/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
</build>
<repositories>
<repository>
<id>sourceforge</id>
<url>http://oss.sonatype.org/content/groups/sourceforge/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<repository>
<id>terracotta-releases1</id>
<url>http://www.terracotta.org/download/reflector/releases</url>
</repository>
<repository>
<id>terracotta-releases</id>
<url>http://www.terracotta.org/download/reflector/releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Maven Repository Group</name>
<url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE</property>
<property name="connection.username">sa</property>
<property name="connection.password"></property>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Disable the second-level cache -->
<property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property>
<!-- Echo all executed SQL to stdout -->
<!--<property name="show_sql">true</property> -->
<!--<property name="format_sql">true</property>-->
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<property name="hibernate.jdbc.fetch_size">2</property>
<property name="hibernate.jdbc.batch_size">30</property>
<!-- Names the annotated entity class -->
<mapping class="org.hibernate.tutorial.annotations.Person"/>
<mapping class="org.hibernate.tutorial.annotations.Event"/>
</session-factory>
</hibernate-configuration>
AnnotationsIllustrationTest.java
package org.hibernate.tutorial.annotations;
import java.util.*;
import static junit.framework.Assert.*;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.stat.Statistics;
import org.junit.*;
import org.junit.Test;
@Slf4j
public class AnnotationsIllustrationTest {
private SessionFactory sessionFactory;
@Before
public void setUp() {
// A SessionFactory is set up once for an application
sessionFactory = new Configuration()
.configure() // configures settings from hibernate.cfg.xml
.buildSessionFactory();
}
@After
public void tearDown() {
if (sessionFactory != null) {
sessionFactory.close();
}
}
@Test
@Ignore
public void testBasicUsage() {
//transient object
Person person = new Person();
person.setFirstName("Vit");
person.setSureName("Lopanov");
/*Event event1 = new Event("Our very first event!", new Date());
Event event2 = new Event("A follow up event", new Date());
event1.setPerson1(person);
event2.setPerson1(person);
List events = new ArrayList();
events.add(event1);
events.add(event2);
person.setEvents(events); */
Session session = sessionFactory.openSession();
session.beginTransaction();
//persistent object
session.saveOrUpdate(person);
session.getTransaction().commit();
session.close();
//detached object объект person перешел в состояние detached
/*
session = sessionFactory.openSession();
session.beginTransaction();
List result = session.createQuery("from Person p").list();
for (Person person1 : (List) result) {
log.info("Event (" + person1.getFirstName() + ") : " + person1.getSureName() + " \n"
+ person1.getEvents().toString());
}
Person person2 = (Person) session.get(Person.class, 1L);
//removed - объект
session.delete(person2);
result = session.createQuery("from Event").list();
for (Event event : (List) result) {
log.info(event.toString());
event.getPerson1().toString();
}
session.getTransaction().commit();
session.close();*/
}
@Test()
@Ignore
public void loadProxyPersons() {
Session session = sessionFactory.openSession();
session.beginTransaction();
// в бд нет записи 10L load() возвращает proxy объект
Person person2 = (Person) session.load(Person.class, 10L);
//проверяем существование proxy объекта
assertNotNull(person2);
session.getTransaction().commit();
session.close();
}
@Ignore
@Test(expected = org.hibernate.ObjectNotFoundException.class)
public void loadProxyPersonsException() {
Session session = sessionFactory.openSession();
session.beginTransaction();
// в бд нет записи 10L load() возвращает proxy объект
Person person2 = (Person) session.load(Person.class, 10L);
// вызываем исключение org.hibernate.ObjectNotFoundException.class
person2.getFirstName();
session.getTransaction().commit();
session.close();
}
@Test()
@Ignore
public void getNullPerson() {
Session session = sessionFactory.openSession();
session.beginTransaction();
//запращиваем объект
Person person2 = (Person) session.get(Person.class, 10L);
//проверяем что get возвращает null
assertNull(person2);
session.getTransaction().commit();
session.close();
}
@Test
@Ignore
public void testHibernateCacheLevelOne() {
Person person = new Person();
person.setFirstName("Vit");
person.setSureName("Lopanov");
Statistics statistics = sessionFactory.getStatistics();
Session session = sessionFactory.openSession();
session.beginTransaction();
session.saveOrUpdate(person);
session.getTransaction().commit();
session.close();
//включаем статистику для сбора информации.
statistics.setStatisticsEnabled(true);
session = sessionFactory.openSession();
session.beginTransaction();
// загружаем объект из БД - выполняется sql:
//Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
person = (Person) session.load(Person.class, 1L);
// проверяем сохранен ли наш объект в кеше - true
System.out.println("contains in cache: " + session.contains(person));
person.setFirstName("Tom");
session.saveOrUpdate(person);
//.... выполняем еще какие-то операции
//запрашиваем заново наш объект, sql не выполняется, объект подсовывается из кеша
person = (Person) session.load(Person.class, 1L);
// проверяем находиться ли наш объект в кеше - true
System.out.println("contains in cache: " + session.contains(person));
person.setSureName("Aristov");
session.saveOrUpdate(person);
// выгружаем объект из кеша
if (session.contains(person)) session.evict(person);
// объект не найден в кеше, выполняем sql запрос
//Hibernate: select person0_.personId as personId16_0_, person0_.fName as fName16_0_, person0_.sName as sName16_0_ from Persons person0_ where person0_.personId=?
person = (Person) session.load(Person.class, 1L);
person.setFirstName("Oleg");
session.flush();
session.saveOrUpdate(person);
session.getTransaction().commit();
session.close();
System.out.println("InsertCount: " + statistics.getEntityInsertCount() + " " +
"UpdateCount: " + statistics.getEntityUpdateCount() + " " +
"FlushCount: " + statistics.getFlushCount() + " " +
"TransactionCount: " + statistics.getTransactionCount() + " " +
"SuccessfulTransactionCount: " + statistics.getSuccessfulTransactionCount());
}
@Test //@Ignore
public void batchInsert() {
Person person;
Session session = sessionFactory.openSession();
session.beginTransaction();
for (int i = 0; i < 10000; i++) {
person = new Person();
person.setFirstName("Vit" + i);
person.setSureName("Lopanov" + i);
session.saveOrUpdate(person);
if (i % 30 == 0) {
//скидываем изменения
session.flush();
//очищаем кеш
session.clear();
}
}
session.getTransaction().commit();
session.close();
}
}