From 3f2271f51b8a9852c240fafbddaf028c0e76fc08 Mon Sep 17 00:00:00 2001 From: bungrudi <30967151+bungrudi@users.noreply.github.com> Date: Wed, 15 Aug 2018 20:23:19 +0700 Subject: [PATCH] BAEL-2043: An Intro to Hibernate Entity Lifecycle (#4960) * "An Intro to Hibernate Entity Lifecycle" "An Intro to Hibernate Entity Lifecycle" by Rudi * Revision from Predrag's feedback * Another revision from Predrag's feedback * Another feedback. --- .../lifecycle/DirtyDataInspector.java | 26 +++ .../hibernate/lifecycle/FootballPlayer.java | 35 ++++ .../lifecycle/HibernateLifecycleUtil.java | 96 ++++++++++ .../HibernateInterceptorUnitTest.java | 1 - .../lifecycle/HibernateLifecycleUnitTest.java | 164 ++++++++++++++++++ .../resources/hibernate-lifecycle.properties | 9 + .../src/test/resources/lifecycle-init.sql | 25 +++ 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/DirtyDataInspector.java create mode 100644 hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/FootballPlayer.java create mode 100644 hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUtil.java create mode 100644 hibernate5/src/test/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUnitTest.java create mode 100644 hibernate5/src/test/resources/hibernate-lifecycle.properties create mode 100644 hibernate5/src/test/resources/lifecycle-init.sql diff --git a/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/DirtyDataInspector.java b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/DirtyDataInspector.java new file mode 100644 index 0000000000..4e00be2b5c --- /dev/null +++ b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/DirtyDataInspector.java @@ -0,0 +1,26 @@ +package com.baeldung.hibernate.lifecycle; + +import org.hibernate.EmptyInterceptor; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class DirtyDataInspector extends EmptyInterceptor { + private static final ArrayList dirtyEntities = new ArrayList<>(); + + @Override + public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { + dirtyEntities.add((FootballPlayer) entity); + return true; + } + + public static List getDirtyEntities() { + return dirtyEntities; + } + + public static void clearDirtyEntitites() { + dirtyEntities.clear(); + } +} \ No newline at end of file diff --git a/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/FootballPlayer.java b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/FootballPlayer.java new file mode 100644 index 0000000000..49799a5292 --- /dev/null +++ b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/FootballPlayer.java @@ -0,0 +1,35 @@ +package com.baeldung.hibernate.lifecycle; + +import javax.persistence.*; + +@Entity +@Table(name = "Football_Player") +public class FootballPlayer { + @Id + @GeneratedValue + private long id; + + @Column + private String name; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "FootballPlayer{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} \ No newline at end of file diff --git a/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUtil.java b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUtil.java new file mode 100644 index 0000000000..a06685fb9c --- /dev/null +++ b/hibernate5/src/main/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUtil.java @@ -0,0 +1,96 @@ +package com.baeldung.hibernate.lifecycle; + +import org.h2.tools.RunScript; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.service.ServiceRegistry; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +public class HibernateLifecycleUtil { + private static SessionFactory sessionFactory; + private static Connection connection; + + public static void init() throws Exception { + Properties hbConfigProp = getHibernateProperties(); + Class.forName(hbConfigProp.getProperty("hibernate.connection.driver_class")); + connection = DriverManager.getConnection(hbConfigProp.getProperty("hibernate.connection.url"), hbConfigProp.getProperty("hibernate.connection.username"), hbConfigProp.getProperty("hibernate.connection.password")); + + try (InputStream h2InitStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lifecycle-init.sql"); + InputStreamReader h2InitReader = new InputStreamReader(h2InitStream)) { + RunScript.execute(connection, h2InitReader); + } + + ServiceRegistry serviceRegistry = configureServiceRegistry(); + sessionFactory = getSessionFactoryBuilder(serviceRegistry).applyInterceptor(new DirtyDataInspector()).build(); + } + + public static void tearDown() throws Exception { + sessionFactory.close(); + connection.close(); + } + + public static SessionFactory getSessionFactory() { + return sessionFactory; + } + + private static SessionFactoryBuilder getSessionFactoryBuilder(ServiceRegistry serviceRegistry) { + MetadataSources metadataSources = new MetadataSources(serviceRegistry); + metadataSources.addAnnotatedClass(FootballPlayer.class); + + Metadata metadata = metadataSources.buildMetadata(); + return metadata.getSessionFactoryBuilder(); + + } + + private static ServiceRegistry configureServiceRegistry() throws IOException { + Properties properties = getHibernateProperties(); + return new StandardServiceRegistryBuilder().applySettings(properties).build(); + } + + private static Properties getHibernateProperties() throws IOException { + Properties properties = new Properties(); + URL propertiesURL = Thread.currentThread().getContextClassLoader().getResource("hibernate-lifecycle.properties"); + try (FileInputStream inputStream = new FileInputStream(propertiesURL.getFile())) { + properties.load(inputStream); + } + return properties; + } + + public static List getManagedEntities(Session session) { + Map.Entry[] entries = ((SessionImplementor) session).getPersistenceContext().reentrantSafeEntityEntries(); + return Arrays.stream(entries).map(e -> e.getValue()).collect(Collectors.toList()); + } + + public static Transaction startTransaction(Session s) { + Transaction tx = s.getTransaction(); + tx.begin(); + return tx; + } + + public static int queryCount(String query) throws Exception { + try (ResultSet rs = connection.createStatement().executeQuery(query)) { + rs.next(); + return rs.getInt(1); + } + } +} diff --git a/hibernate5/src/test/java/com/baeldung/hibernate/interceptors/HibernateInterceptorUnitTest.java b/hibernate5/src/test/java/com/baeldung/hibernate/interceptors/HibernateInterceptorUnitTest.java index 0049f3a6bd..e18e989905 100644 --- a/hibernate5/src/test/java/com/baeldung/hibernate/interceptors/HibernateInterceptorUnitTest.java +++ b/hibernate5/src/test/java/com/baeldung/hibernate/interceptors/HibernateInterceptorUnitTest.java @@ -58,5 +58,4 @@ public class HibernateInterceptorUnitTest { transaction.commit(); session.close(); } - } diff --git a/hibernate5/src/test/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUnitTest.java b/hibernate5/src/test/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUnitTest.java new file mode 100644 index 0000000000..e682b46481 --- /dev/null +++ b/hibernate5/src/test/java/com/baeldung/hibernate/lifecycle/HibernateLifecycleUnitTest.java @@ -0,0 +1,164 @@ +package com.baeldung.hibernate.lifecycle; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.engine.spi.Status; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static com.baeldung.hibernate.lifecycle.DirtyDataInspector.getDirtyEntities; +import static com.baeldung.hibernate.lifecycle.HibernateLifecycleUtil.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class HibernateLifecycleUnitTest { + + @BeforeClass + public static void setup() throws Exception { + HibernateLifecycleUtil.init(); + + } + + @AfterClass + public static void tearDown() throws Exception { + HibernateLifecycleUtil.tearDown(); + } + + @Before + public void beforeMethod() { + DirtyDataInspector.clearDirtyEntitites(); + } + + @Test + public void whenEntityLoaded_thenEntityManaged() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + assertThat(getManagedEntities(session)).isEmpty(); + + List players = session.createQuery("from FootballPlayer").getResultList(); + assertThat(getManagedEntities(session)).size().isEqualTo(3); + + assertThat(getDirtyEntities()).isEmpty(); + + FootballPlayer gigiBuffon = players.stream().filter(p -> p.getId() == 3).findFirst().get(); + + gigiBuffon.setName("Gianluigi Buffon"); + transaction.commit(); + + assertThat(getDirtyEntities()).size().isEqualTo(1); + assertThat(getDirtyEntities().get(0).getId()).isEqualTo(3); + assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Gianluigi Buffon"); + } + } + + @Test + public void whenDetached_thenNotTracked() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + FootballPlayer cr7 = session.get(FootballPlayer.class, 1L); + assertThat(getManagedEntities(session)).size().isEqualTo(1); + assertThat(getManagedEntities(session).get(0).getId()).isEqualTo(cr7.getId()); + + session.evict(cr7); + assertThat(getManagedEntities(session)).size().isEqualTo(0); + + cr7.setName("CR7"); + transaction.commit(); + + assertThat(getDirtyEntities()).isEmpty(); + } + } + + @Test + public void whenReattached_thenTrackedAgain() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + FootballPlayer messi = session.get(FootballPlayer.class, 2L); + + session.evict(messi); + messi.setName("Leo Messi"); + transaction.commit(); + assertThat(getDirtyEntities()).isEmpty(); + + transaction = startTransaction(session); + session.update(messi); + transaction.commit(); + assertThat(getDirtyEntities()).size().isEqualTo(1); + assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Leo Messi"); + } + } + + @Test + public void givenNewEntityWithID_whenReattached_thenManaged() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + FootballPlayer gigi = new FootballPlayer(); + gigi.setId(3); + gigi.setName("Gigi the Legend"); + + session.update(gigi); + assertThat(getManagedEntities(session)).size().isEqualTo(1); + + transaction.commit(); + assertThat(getDirtyEntities()).size().isEqualTo(1); + } + } + + @Test + public void givenTransientEntity_whenSave_thenManaged() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + FootballPlayer neymar = new FootballPlayer(); + neymar.setName("Neymar"); + + session.save(neymar); + assertThat(getManagedEntities(session)).size().isEqualTo(1); + assertThat(neymar.getId()).isNotNull(); + + int count = queryCount("select count(*) from Football_Player where name='Neymar'"); + assertThat(count).isEqualTo(0); + + transaction.commit(); + + count = queryCount("select count(*) from Football_Player where name='Neymar'"); + assertThat(count).isEqualTo(1); + + transaction = startTransaction(session); + session.delete(neymar); + transaction.commit(); + } + } + + @Test() + public void whenDelete_thenMarkDeleted() throws Exception { + SessionFactory sessionFactory = HibernateLifecycleUtil.getSessionFactory(); + try (Session session = sessionFactory.openSession()) { + Transaction transaction = startTransaction(session); + + FootballPlayer neymar = new FootballPlayer(); + neymar.setName("Neymar"); + + session.save(neymar); + transaction.commit(); + + transaction = startTransaction(session); + session.delete(neymar); + assertThat(getManagedEntities(session).get(0).getStatus()).isEqualTo(Status.DELETED); + transaction.commit(); + } + } +} diff --git a/hibernate5/src/test/resources/hibernate-lifecycle.properties b/hibernate5/src/test/resources/hibernate-lifecycle.properties new file mode 100644 index 0000000000..d043b624f2 --- /dev/null +++ b/hibernate5/src/test/resources/hibernate-lifecycle.properties @@ -0,0 +1,9 @@ +hibernate.connection.driver_class=org.h2.Driver +hibernate.connection.url=jdbc:h2:mem:lifecycledb;DB_CLOSE_DELAY=-1; +hibernate.connection.username=sa +hibernate.connection.password= +hibernate.connection.autocommit=true + +hibernate.dialect=org.hibernate.dialect.H2Dialect +hibernate.show_sql=true +hibernate.hbm2ddl.auto=validate \ No newline at end of file diff --git a/hibernate5/src/test/resources/lifecycle-init.sql b/hibernate5/src/test/resources/lifecycle-init.sql new file mode 100644 index 0000000000..c0c9a3f34d --- /dev/null +++ b/hibernate5/src/test/resources/lifecycle-init.sql @@ -0,0 +1,25 @@ +create sequence hibernate_sequence start with 1 increment by 1; + +create table Football_Player ( + id bigint not null, + name varchar(255), + primary key (id) +); + +insert into + Football_Player + (name, id) + values + ('Cristiano Ronaldo', next value for hibernate_sequence); + +insert into + Football_Player + (name, id) + values + ('Lionel Messi', next value for hibernate_sequence); + +insert into + Football_Player + (name, id) + values + ('Gigi Buffon', next value for hibernate_sequence); \ No newline at end of file