diff --git a/hibernate-core/src/main/java/org/hibernate/Hibernate.java b/hibernate-core/src/main/java/org/hibernate/Hibernate.java index f2d59ef964..c40f17e8d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/Hibernate.java +++ b/hibernate-core/src/main/java/org/hibernate/Hibernate.java @@ -119,6 +119,68 @@ public final class Hibernate { } } + /** + * Obtain the {@linkplain Collection#size() size} of a persistent collection, + * without fetching its state from the database. + * + * @param collection a persistent collection associated with an open session + * @return the size of the collection + * + * @since 6.1.1 + */ + public static int size(Collection collection) { + return collection instanceof PersistentCollection + ? ((PersistentCollection) collection).getSize() + : collection.size(); + } + + /** + * Determine if the given persistent collection {@linkplain Collection#contains(Object) contains} + * the given element, without fetching its state from the database. + * + * @param collection a persistent collection associated with an open session + * @return true if the collection does contain the given element + * + * @since 6.1.1 + */ + public static boolean contains(Collection collection, T element) { + return collection instanceof PersistentCollection + ? ((PersistentCollection) collection).elementExists(element) + : collection.contains(element); + } + + /** + * Obtain the value associated with the given key by the given persistent + * map, without fetching the state of the map from the database. + * + * @param map a persistent map associated with an open session + * @param key a key belonging to the map + * @return the value associated by the map with the given key + * + * @since 6.1.1 + */ + public static V get(Map map, K key) { + return map instanceof PersistentCollection + ? (V) ((PersistentCollection) map).elementByIndex(key) + : map.get(key); + } + + /** + * Obtain the element of the given persistent list with the given index, + * without fetching the state of the list from the database. + * + * @param list a persistent list associated with an open session + * @param key an index belonging to the list + * @return the element of the list with the given index + * + * @since 6.1.1 + */ + public static T get(List list, int key) { + return list instanceof PersistentCollection + ? (T) ((PersistentCollection) list).elementByIndex(key) + : list.get(key); + } + /** * Get the true, underlying class of a proxied persistent class. This operation * will initialize a proxy by side effect. @@ -225,6 +287,8 @@ public final class Hibernate { * @param id the id of the persistent entity instance * * @return a detached uninitialized proxy + * + * @since 6.0 */ @SuppressWarnings("unchecked") public static E createDetachedProxy(SessionFactory sessionFactory, Class entityClass, Object id) { @@ -242,6 +306,8 @@ public final class Hibernate { * Operations for obtaining references to persistent collections of a certain type. * * @param the type of collection, for example, {@code List<User>} + * + * @since 6.0 */ public static final class CollectionInterface { private final Supplier detached; @@ -281,6 +347,8 @@ public final class Hibernate { * of a given element type. * * @param the element type + * + * @since 6.0 */ public static CollectionInterface> bag() { return new CollectionInterface<>(PersistentBag::new, ArrayList::new); @@ -291,6 +359,8 @@ public final class Hibernate { * of a given element type. * * @param the element type + * + * @since 6.0 */ public static CollectionInterface> set() { return new CollectionInterface<>(PersistentSet::new, HashSet::new); @@ -301,6 +371,8 @@ public final class Hibernate { * of a given element type. * * @param the element type + * + * @since 6.0 */ public static CollectionInterface> list() { return new CollectionInterface<>(PersistentList::new, ArrayList::new); @@ -312,6 +384,8 @@ public final class Hibernate { * * @param the key type * @param the value type + * + * @since 6.0 */ public static CollectionInterface> map() { return new CollectionInterface<>(PersistentMap::new, HashMap::new); @@ -322,6 +396,8 @@ public final class Hibernate { * of a given element type. * * @param the element type + * + * @since 6.0 */ public static CollectionInterface> sortedSet() { return new CollectionInterface<>(PersistentSortedSet::new, TreeSet::new); @@ -334,6 +410,7 @@ public final class Hibernate { * @param the key type * @param the value type * + * @since 6.0 */ public static CollectionInterface> sortedMap() { return new CollectionInterface<>(PersistentSortedMap::new, TreeMap::new); @@ -344,6 +421,8 @@ public final class Hibernate { * of the given type. * * @param collectionClass the Java class object representing the collection type + * + * @since 6.0 */ @SuppressWarnings("unchecked") public static CollectionInterface collection(Class collectionClass) { diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java index dcfda12a09..a25460fe32 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java @@ -145,9 +145,10 @@ public abstract class AbstractPersistentCollection implements Serializable, P return true; } else { - final boolean isExtraLazy = withTemporarySessionIfNeeded( + return withTemporarySessionIfNeeded( () -> { - final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( AbstractPersistentCollection.this ); + final CollectionEntry entry = session.getPersistenceContextInternal() + .getCollectionEntry(this); if ( entry != null ) { final CollectionPersister persister = entry.getLoadedPersister(); @@ -162,18 +163,36 @@ public abstract class AbstractPersistentCollection implements Serializable, P read(); } } - else{ + else { throwLazyInitializationExceptionIfNotConnected(); } return false; } ); - return isExtraLazy; } } return false; } + public int getSize() { + if ( cachedSize>=0 ) { + return cachedSize; + } + CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry(this); + if ( entry == null ) { + throwLazyInitializationExceptionIfNotConnected(); + throwLazyInitializationException("collection not associated with session"); + throw new AssertionFailure("impossible"); + } + else { + if ( hasQueuedOperations() ) { + session.flush(); + } + cachedSize = entry.getLoadedPersister().getSize( entry.getLoadedKey(), session ); + return cachedSize; + } + } + /** * TBH not sure why this is public * @@ -332,6 +351,22 @@ public abstract class AbstractPersistentCollection implements Serializable, P return null; } + @Override + public boolean elementExists(Object element) { + final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( AbstractPersistentCollection.this ); + if ( entry == null ) { + throwLazyInitializationExceptionIfNotConnected(); + throwLazyInitializationException("collection not associated with session"); + throw new AssertionFailure("impossible"); + } + else { + if ( hasQueuedOperations() ) { + session.flush(); + } + return entry.getLoadedPersister().elementExists( entry.getLoadedKey(), element, session ); + } + } + protected static final Object UNKNOWN = new MarkerObject( "UNKNOWN" ); protected Object readElementByIndex(final Object index) { @@ -368,6 +403,22 @@ public abstract class AbstractPersistentCollection implements Serializable, P } + @Override + public Object elementByIndex(Object index) { + final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( AbstractPersistentCollection.this ); + if ( entry == null ) { + throwLazyInitializationExceptionIfNotConnected(); + throwLazyInitializationException("collection not associated with session"); + throw new AssertionFailure("impossible"); + } + else { + if ( hasQueuedOperations() ) { + session.flush(); + } + return entry.getLoadedPersister().getElementByIndex( entry.getLoadedKey(), index, session, owner ); + } + } + protected int getCachedSize() { return cachedSize; } diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java index d3ea45e6b0..9fcace451c 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentCollection.java @@ -452,6 +452,21 @@ public interface PersistentCollection { */ Collection getOrphans(Serializable snapshot, String entityName); + /** + * Obtain the size of this collection without initializing it + */ + int getSize(); + + /** + * Determine if the given element belongs to this collection without initializing it + */ + boolean elementExists(Object element); + + /** + * Obtain the element os this collection associated with the given index without initializing it + */ + Object elementByIndex(Object index); + void initializeEmptyCollection(CollectionPersister persister); /** diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/CollectionSizeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/CollectionSizeTest.java new file mode 100644 index 0000000000..35397939e2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/CollectionSizeTest.java @@ -0,0 +1,69 @@ +/* + * 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.orm.test.collection.basic; + +import org.hibernate.Hibernate; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + Contact.class, EmailAddress.class, User.class + } +) +@SessionFactory +public class CollectionSizeTest { + + @Test + public void prepareData(SessionFactoryScope scope) { + Set emailAddresses = new HashSet<>(); + emailAddresses.add( new EmailAddress( "test1@test.com" ) ); + emailAddresses.add( new EmailAddress( "test2@test.com" ) ); + emailAddresses.add( new EmailAddress( "test3@test.com" ) ); + User user = new User(); + user.setName( "john" ); + Contact contact = new Contact(); + contact.setName( "John Doe" ); + contact.setEmailAddresses( emailAddresses ); + emailAddresses.forEach( address -> contact.getContactsByEmail().put(address, contact) ); + + scope.inTransaction( + session -> { + user.setContact( contact ); + session.persist( user ); + session.persist( contact ); + session.flush(); + } + ); + scope.inTransaction( + session -> { + Contact cont = session.get(Contact.class, contact.getId()); + Set addresses = cont.getEmailAddresses(); + assertEquals( Hibernate.size(addresses), 3 ); + assertTrue( Hibernate.contains( addresses, new EmailAddress( "test1@test.com" ) ) ); + assertFalse( Hibernate.contains( addresses, new EmailAddress( "test9@test.com" ) ) ); + assertFalse( Hibernate.isInitialized(addresses) ); + Map contactsByEmail = cont.getContactsByEmail(); + assertEquals( cont, Hibernate.get(contactsByEmail, new EmailAddress( "test1@test.com" ) ) ); + assertNull( Hibernate.get(contactsByEmail, new EmailAddress( "test9@test.com" ) ) ); + assertFalse( Hibernate.isInitialized(contactsByEmail) ); + } + ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/Contact.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/Contact.java index f533b0b4c2..79d1b35738 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/Contact.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/Contact.java @@ -7,7 +7,9 @@ package org.hibernate.orm.test.collection.basic; import java.io.Serializable; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import jakarta.persistence.Basic; import jakarta.persistence.CollectionTable; @@ -17,6 +19,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @Entity @@ -26,8 +29,9 @@ public class Contact implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String name; - private Set emailAddresses = new HashSet(); - private Set emailAddresses2 = new HashSet(); + private Set emailAddresses = new HashSet<>(); + private Set emailAddresses2 = new HashSet<>(); + private Map contactsByEmail = new HashMap<>(); @Id @GeneratedValue(strategy = GenerationType.AUTO) @@ -68,6 +72,16 @@ public class Contact implements Serializable { this.emailAddresses = emailAddresses; } + @ManyToMany + @CollectionTable(name="contact_email_addresses") + public Map getContactsByEmail() { + return contactsByEmail; + } + + public void setContactsByEmail(Map contactsByEmail) { + this.contactsByEmail = contactsByEmail; + } + @Override public int hashCode() { int hash = 0;