Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HHH-7180 Test and fix wrong collection key equality comparisons #9603

Merged
merged 2 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
Object id = getIdentifier( session, ref );

// only evict if the related entity has changed
if ( ( id != null && !id.equals( oldId ) ) || ( oldId != null && !oldId.equals( id ) ) ) {
if ( ( id != null || oldId != null ) && !collectionPersister.getKeyType().isEqual( oldId, id ) ) {

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
CollectionPersister.getKeyType
should be avoided because it has been deprecated.
if ( id != null ) {
evict( id, collectionPersister, session );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
CollectionPersister persister, Object id, PersistentCollection<?> snapshot) {
return isCollectionSnapshotValid( snapshot )
&& persister.getRole().equals( snapshot.getRole() )
&& id.equals( snapshot.getKey() );
&& persister.getKeyType().isEqual( id, snapshot.getKey() );

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
CollectionPersister.getKeyType
should be avoided because it has been deprecated.
}

private static boolean isCollectionSnapshotValid(PersistentCollection<?> snapshot) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.cache;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.community.dialect.InformixDialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.stat.CollectionStatistics;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;


@SkipForDialect(dialectClass = MySQLDialect.class, majorVersion = 5, reason = "BLOB/TEXT column 'id' used in key specification without a key length")
@SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "ORA-02329: column of datatype LOB cannot be unique or a primary key")
@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns")
@DomainModel(
annotatedClasses = {
CollectionCacheEvictionComplexIdTest.Parent.class,
CollectionCacheEvictionComplexIdTest.Child.class
}
)
@SessionFactory
@ServiceRegistry(
settings = {
@Setting(name = AvailableSettings.AUTO_EVICT_COLLECTION_CACHE, value = "true"),
@Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"),
@Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true")
}
)
public class CollectionCacheEvictionComplexIdTest {

@BeforeEach
public void prepare(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Parent entity = new Parent();
entity.id = new byte[] {
(byte) ( 1 ),
(byte) ( 2 ),
(byte) ( 3 ),
(byte) ( 4 )
};
entity.name = "Simple name";

for ( int j = 1; j <= 2; j++ ) {
Child child = new Child();
child.id = j;
entity.name = "Child name " + j;
child.parent = entity;
entity.children.add(child);
}
session.persist( entity );
}
);
}

@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "delete from Child" ).executeUpdate();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
session.createQuery( "delete from Parent" ).executeUpdate();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note test

Invoking
QueryProducerImplementor.createQuery
should be avoided because it has been deprecated.
}
);
}

@Test
@JiraKey(value = "HHH-7180")
public void testEvict(SessionFactoryScope scope) {
final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
statistics.setStatisticsEnabled( true );

Parent parent = scope.fromTransaction(
session -> session.createQuery( "from Parent p", Parent.class ).getSingleResult()
);
scope.inTransaction(
session -> {
Parent p = session.createQuery( "from Parent p", Parent.class ).getSingleResult();
Child child1 = p.children.iterator().next();
child1.name = "Updated child";
child1.parent = parent;
}
);

statistics.clear();
scope.inTransaction(
session -> {
Parent p = session.createQuery( "from Parent p", Parent.class ).getSingleResult();
final CollectionStatistics collectionStatistics = statistics.getCollectionStatistics(
Parent.class.getName() + ".children" );
assertEquals( 1, collectionStatistics.getCacheHitCount() );
assertEquals( 0, collectionStatistics.getCacheMissCount() );
}
);

}

@Entity(name = "Parent")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public static class Parent {
@Id
public byte[] id;
public String name;
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public Set<Child> children = new HashSet<>();
}

@Entity(name = "Child")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public static class Child {
@Id
public Integer id;
public String name;
@ManyToOne(fetch = FetchType.LAZY)
public Parent parent;

public Child() {
}

public Child(Integer id, Parent parent) {
this.id = id;
this.parent = parent;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.id.array;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.community.dialect.InformixDialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;


@SkipForDialect(dialectClass = MySQLDialect.class, majorVersion = 5, reason = "BLOB/TEXT column 'id' used in key specification without a key length")
@SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "ORA-02329: column of datatype LOB cannot be unique or a primary key")
@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns")
@DomainModel(
annotatedClasses = {
PrimitiveByteArrayIdCollectionTest.Parent.class,
PrimitiveByteArrayIdCollectionTest.Child.class
}
)
@SessionFactory
public class PrimitiveByteArrayIdCollectionTest {

@BeforeEach
public void prepare(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Parent entity = new Parent();
entity.id = new byte[] {
(byte) ( 1 ),
(byte) ( 2 ),
(byte) ( 3 ),
(byte) ( 4 )
};
entity.name = "Simple name";

for ( int j = 1; j <= 2; j++ ) {
Child child = new Child();
child.id = j;
entity.name = "Child name " + j;
child.parent = entity;
entity.children.add(child);
}
session.persist( entity );
}
);
}

@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "delete from Child" ).executeUpdate();
session.createQuery( "delete from Parent" ).executeUpdate();
}
);
}

@Test
@JiraKey(value = "HHH-7180")
public void testReattach(SessionFactoryScope scope) {
// Since reattachment was removed in ORM 7,
// but the code path to trigger the bug is still reachable through removing a detached entity,
// construct a scenario that shows a problem

final Parent parent = scope.fromTransaction(
session -> session.createQuery( "from Parent p", Parent.class ).getSingleResult()
);
// Copy the byte-array id which will make it a different instance, yet equal to the collection key
parent.id = parent.id.clone();

final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
statistics.setStatisticsEnabled( true );
statistics.clear();

scope.inTransaction(
session -> {
session.remove( parent );
session.flush();

// The collection will be removed twice if the collection key can't be matched to the entity id
assertEquals( 1L, statistics.getCollectionRemoveCount() );
}
);

}

@Entity(name = "Parent")
public static class Parent {
@Id
public byte[] id;
public String name;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public Set<Child> children = new HashSet<>();
}

@Entity(name = "Child")
public static class Child {
@Id
public Integer id;
public String name;
@ManyToOne(fetch = FetchType.LAZY)
public Parent parent;

public Child() {
}

public Child(Integer id) {
this.id = id;
}
}
}
Loading