From c2150ffc3b77ca41a238a79fbb3f58cd335d7be2 Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Wed, 17 May 2017 17:44:10 +0300 Subject: [PATCH] HHH-11186 - Add examples for all Hibernate annotations Document @Subselect and @Synchronize annotations --- .../userguide/appendices/Annotations.adoc | 9 +- .../userguide/chapters/domain/entity.adoc | 57 +++- .../mapping/basic/SubselectTest.java | 285 ++++++++++++++++++ 3 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 documentation/src/test/java/org/hibernate/userguide/mapping/basic/SubselectTest.java diff --git a/documentation/src/main/asciidoc/userguide/appendices/Annotations.adoc b/documentation/src/main/asciidoc/userguide/appendices/Annotations.adoc index 48abd55c8b..e58364e8a4 100644 --- a/documentation/src/main/asciidoc/userguide/appendices/Annotations.adoc +++ b/documentation/src/main/asciidoc/userguide/appendices/Annotations.adoc @@ -1222,7 +1222,7 @@ See the <> section for more info. [[annotations-hibernate-synchronize]] ==== `@Synchronize` @@ -1233,14 +1233,14 @@ With this information in place, Hibernate will properly trigger an entity flush Therefore, the `@Synchronize` annotation prevents the derived entity from returning stale data when executing entity queries against the `@Subselect` entity. -//TODO: Add example +See the <> section for more info. [[annotations-hibernate-table]] ==== `@Table` The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Table.html[`@Table`] annotation is used to specify additional information to a JPA <> annotation, like custom `INSERT`, `UPDATE` or `DELETE` statements or a specific https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/FetchMode.html[`FetchMode`]. -//TODO: Add example +See the <> section for more info about Hibernate-specific `@Table` mapping. [[annotations-hibernate-tables]] ==== `@Tables` @@ -1248,8 +1248,7 @@ The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibern The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Tables.html[`@Tables`] annotation is used to group multiple <> annotations. [[annotations-hibernate-target]] -==== `@Target` - +==== `@Target`Se The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Target.html[`@Target`] annotation is used to specify an explicit target implementation when the current annotated association is using an interface type. See the <> section for more info. diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc index 8b4b50b798..aa0dab72d8 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc @@ -1,6 +1,7 @@ [[entity]] === Entity types -:sourcedir: ../../../../../test/java/org/hibernate/userguide/locking +:sourcedir-locking: ../../../../../test/java/org/hibernate/userguide/locking +:sourcedir-mapping: ../../../../../test/java/org/hibernate/userguide/mapping/basic :extrasdir: extras .Usage of the word _entity_ @@ -335,7 +336,7 @@ There are 4 available OptimisticLockTypes: ==== [source,java] ---- -include::{sourcedir}/OptimisticLockTypeAllTest.java[tag=locking-optimistic-lock-type-all-example,indent=0] +include::{sourcedir-locking}/OptimisticLockTypeAllTest.java[tag=locking-optimistic-lock-type-all-example,indent=0] ---- ==== @@ -346,7 +347,7 @@ When you need to modify the `Person` entity above: ==== [source,java] ---- -include::{sourcedir}/OptimisticLockTypeAllTest.java[tag=locking-optimistic-lock-type-all-update-example,indent=0] +include::{sourcedir-locking}/OptimisticLockTypeAllTest.java[tag=locking-optimistic-lock-type-all-update-example,indent=0] ---- [source,SQL] @@ -376,7 +377,7 @@ since the entity was loaded in the currently running Persistence Context. ==== [source,java] ---- -include::{sourcedir}/OptimisticLockTypeDirtyTest.java[tag=locking-optimistic-lock-type-dirty-example,indent=0] +include::{sourcedir-locking}/OptimisticLockTypeDirtyTest.java[tag=locking-optimistic-lock-type-dirty-example,indent=0] ---- ==== @@ -387,7 +388,7 @@ When you need to modify the `Person` entity above: ==== [source,java] ---- -include::{sourcedir}/OptimisticLockTypeDirtyTest.java[tag=locking-optimistic-lock-type-dirty-update-example,indent=0] +include::{sourcedir-locking}/OptimisticLockTypeDirtyTest.java[tag=locking-optimistic-lock-type-dirty-update-example,indent=0] ---- [source,SQL] @@ -408,3 +409,49 @@ When using `OptimisticLockType.DIRTY`, you should also use `@DynamicUpdate` beca and also the `@SelectBeforeUpdate` annotation so that detached entities are properly handled by the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#update-java.lang.Object-[`Session#update(entity)`] operation. ==== + +[[entity-sql-query-mapping]] +==== Mapping the entity to a SQL query + +You can map an entity to a SQL query using the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Subselect.html[`@Subselect`] annotation. + +[[mapping-Subselect-example]] +.`@Subselect` entity mapping +==== +[source,java] +---- +include::{sourcedir-mapping}/SubselectTest.java[tag=mapping-Subselect-example,indent=0] +---- +==== + +In the example above, the `Account` entity does not retain any balance since every account operation is registered as an `AccountTransaction`. +To find the `Account` balance, we need to query the `AccountSummary` which shares the same identifier with the `Account` entity. + +However, the `AccountSummary` is not mapped to a physical table, but to an SQL query. + +So, if we have the following `AccountTransaction` record, the `AccountSummary` balance will mach the proper amount of money in this `Account`. + +[[mapping-Subselect-entity-find-example]] +.Finding a `@Subselect` entity +==== +[source,java] +---- +include::{sourcedir-mapping}/SubselectTest.java[tag=mapping-Subselect-entity-find-example,indent=0] +---- +==== + +If we add a new `AccountTransaction` entity and refresh the `AccountSummary` entity, the balance is updated accordingly: + +[[mapping-Subselect-refresh-find-example]] +.Refreshing a `@Subselect` entity +==== +[source,java] +---- +include::{sourcedir-mapping}/SubselectTest.java[tag=mapping-Subselect-entity-refresh-example,indent=0] +---- +==== + +The goala of the `@Synchronize` annotation in the `AccountSummary` entity mapping is to instruct Hibernate which database tables are needed by the +underlying `@Subselect` SQL query. This way, when executing a HQL or JPQL which selects from the `AccountSummary` entity, +Hibernate will trigger a Persistence Context flush if there are pending `Account`, `Client` or `AccountTransaction` entity state transitions. + diff --git a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/SubselectTest.java b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/SubselectTest.java new file mode 100644 index 0000000000..b17c17f117 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/SubselectTest.java @@ -0,0 +1,285 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.userguide.mapping.basic; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.hibernate.annotations.Subselect; +import org.hibernate.annotations.Synchronize; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; + +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SubselectTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Client.class, + Account.class, + AccountTransaction.class, + AccountSummary.class + }; + } + + @Test + public void testLifecycle() { + //tag::mapping-Subselect-entity-find-example[] + doInJPA( this::entityManagerFactory, entityManager -> { + Client client = new Client(); + client.setId( 1L ); + client.setFirstName( "John" ); + client.setLastName( "Doe" ); + entityManager.persist( client ); + + Account account = new Account(); + account.setId( 1L ); + account.setClient( client ); + account.setDescription( "Checking account" ); + entityManager.persist( account ); + + AccountTransaction transaction = new AccountTransaction(); + transaction.setAccount( account ); + transaction.setDescription( "Salary" ); + transaction.setCents( 100 * 7000 ); + entityManager.persist( transaction ); + + AccountSummary summary = entityManager.createQuery( + "select s " + + "from AccountSummary s " + + "where s.id = :id", AccountSummary.class) + .setParameter( "id", account.getId() ) + .getSingleResult(); + + assertEquals( "John Doe", summary.getClientName() ); + assertEquals( 100 * 7000, summary.getBalance() ); + } ); + //end::mapping-Subselect-entity-find-example[] + + //tag::mapping-Subselect-entity-refresh-example[] + doInJPA( this::entityManagerFactory, entityManager -> { + AccountSummary summary = entityManager.find( AccountSummary.class, 1L ); + assertEquals( "John Doe", summary.getClientName() ); + assertEquals( 100 * 7000, summary.getBalance() ); + + AccountTransaction transaction = new AccountTransaction(); + transaction.setAccount( entityManager.getReference( Account.class, 1L ) ); + transaction.setDescription( "Shopping" ); + transaction.setCents( -100 * 2200 ); + entityManager.persist( transaction ); + entityManager.flush(); + + entityManager.refresh( summary ); + assertEquals( 100 * 4800, summary.getBalance() ); + } ); + + //end::mapping-Subselect-entity-refresh-example[] + } + + //tag::mapping-Subselect-example[] + @Entity(name = "Client") + @Table(name = "client") + public static class Client { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + //Getters and setters omitted for brevity + + //end::mapping-Subselect-example[] + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + //tag::mapping-Subselect-example[] + } + + @Entity(name = "Account") + @Table(name = "account") + public static class Account { + + @Id + private Long id; + + @ManyToOne + private Client client; + + private String description; + + //Getters and setters omitted for brevity + + //end::mapping-Subselect-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + //tag::mapping-Subselect-example[] + } + + @Entity(name = "AccountTransaction") + @Table(name = "account_transaction") + public static class AccountTransaction { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private Account account; + + private Integer cents; + + private String description; + + //Getters and setters omitted for brevity + + //end::mapping-Subselect-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public Integer getCents() { + return cents; + } + + public void setCents(Integer cents) { + this.cents = cents; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + //tag::mapping-Subselect-example[] + } + + @Entity(name = "AccountSummary") + @Subselect( + "select " + + " a.id as id, " + + " c.first_name||' '||c.last_name as clientName, " + + " sum(at.cents) as balance " + + "from account a " + + "join client c on c.id = a.client_id " + + "join account_transaction at on a.id = at.account_id " + + "group by a.id, c.first_name||' '||c.last_name" + ) + @Synchronize( {"client", "account", "account_transaction"} ) + public static class AccountSummary { + + @Id + private Long id; + + private String clientName; + + private int balance; + + //Getters and setters omitted for brevity + + //end::mapping-Subselect-example[] + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public int getBalance() { + return balance; + } + + public void setBalance(int balance) { + this.balance = balance; + } + //tag::mapping-Subselect-example[] + } + //end::mapping-Subselect-example[] +}