Skip to content
Open
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 @@ -13,6 +13,7 @@
import org.hibernate.boot.internal.ClassLoaderAccessImpl;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.spi.PreCollectionUpdateEvent;
import org.hibernate.event.spi.PreCollectionUpdateEventListener;
import org.hibernate.event.spi.PreDeleteEvent;
Expand Down Expand Up @@ -51,8 +52,9 @@ public class BeanValidationEventListener
private final Validator validator;
private final GroupsPerOperation groupsPerOperation;

public BeanValidationEventListener(
ValidatorFactory factory, Map<String, Object> settings, ClassLoaderService classLoaderService) {
private SessionFactoryImplementor sessionFactory;

public BeanValidationEventListener(ValidatorFactory factory, Map<String, Object> settings, ClassLoaderService classLoaderService) {
traversableResolver = new HibernateTraversableResolver();
validator =
factory.usingContext()
Expand All @@ -63,9 +65,9 @@ public BeanValidationEventListener(

@Override
public void sessionFactoryCreated(SessionFactory factory) {
var implementor = factory.unwrap( SessionFactoryImplementor.class );
implementor.getMappingMetamodel()
.forEachEntityDescriptor( entityPersister -> traversableResolver.addPersister( entityPersister, implementor ) );
sessionFactory = factory.unwrap( SessionFactoryImplementor.class );
sessionFactory.getMappingMetamodel()
.forEachEntityDescriptor( entityPersister -> traversableResolver.addPersister( entityPersister, sessionFactory ) );
}

public boolean onPreInsert(PreInsertEvent event) {
Expand Down Expand Up @@ -110,11 +112,21 @@ public void onPreUpdateCollection(PreCollectionUpdateEvent event) {
final Object entity = castNonNull( event.getCollection().getOwner() );
validate(
entity,
event.getSession().getEntityPersister( event.getAffectedOwnerEntityName(), entity ),
getEntityPersister( event.getSession(), event.getAffectedOwnerEntityName(), entity ),
GroupsPerOperation.Operation.UPDATE
);
}

private EntityPersister getEntityPersister(SharedSessionContractImplementor session, String entityName, Object entity) {
if ( session != null ) {
return session.getEntityPersister( entityName, entity );
}
return entityName == null
? sessionFactory.getMappingMetamodel().getEntityDescriptor( entity.getClass().getName() )
: sessionFactory.getMappingMetamodel().getEntityDescriptor( entityName )
.getSubclassEntityPersister( entity, sessionFactory );
}
Comment on lines +121 to +128
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fine workaround, but I wonder if we could rather make org.hibernate.event.spi.AbstractSessionEvent take a org.hibernate.engine.spi.SharedSessionContractImplementor instead of org.hibernate.event.spi.EventSource, deprecating the old signature (and delegating to the new one since EventSource extends SharedSessionContractImplementor.

Since stateless session fire this events too now, it would make more sense to be able to retrieve them in AbstractSessionEvent#getSession (which, as the name implies, could not only return EventSources). Perhaps let's hear what @gavinking thinks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 there is the

public abstract class AbstractEvent implements Serializable {
protected final SharedSessionContractImplementor source;

but it's a different hierarchy of events, and these collection ones don't get the same source 😖


private <T> void validate(
T object,
EntityPersister persister,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.annotations.beanvalidation;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.constraints.Size;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
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.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SessionFactory
@DomainModel(annotatedClasses = {
CollectionActionsValidationStatelessTest.Author.class,
CollectionActionsValidationStatelessTest.Book.class,
})
@ServiceRegistry(settings = @Setting(name = AvailableSettings.JAKARTA_VALIDATION_MODE, value = "auto"))
@Jira("https://hibernate.atlassian.net/browse/HHH-19843")
public class CollectionActionsValidationStatelessTest {

@Test
void smoke(SessionFactoryScope scope) {
scope.inStatelessTransaction( session -> {
final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> {
ArrayList<Book> books = new ArrayList<>();
Author author = new Author( 1L, "first", "last", books );
Book book = new Book( 10L, "", author );
books.add( book );

session.upsertMultiple( List.of( author ) );
} );
assertThat( e.getConstraintViolations() ).hasSize( 1 );
} );
}

@AfterEach
public void tearDown(SessionFactoryScope scope) {
scope.getSessionFactory().getSchemaManager().truncate();
}

@Table(name = "author")
@Entity
static class Author {

public Author() {
}

public Author(long id, String firstName, String lastName, List<Book> books) {
this.firstName = firstName;
this.lastName = lastName;
this.books = books;
this.id = id;
}

@Id
Long id;

String firstName;

String lastName;

@OneToMany
@JoinColumn(name = "bookId")
@Size(min = 10)
List<Book> books;

}

@Table(name = "book")
@Entity
static class Book {

public Book() {
}

public Book(long id, String title, Author author) {
this.id = id;
this.title = title;
this.author = author;
}

@Id
Long id;

String title;

@ManyToOne
Author author;
}
}
Loading