From 7c2a58861341f1404b4974f1ed9809f40c1da8f6 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Sat, 12 Aug 2017 10:17:39 +0100 Subject: [PATCH] HHH-6382: Allow to use @OnDelete annotation on unidirectional @OneToMany associations --- .../cfg/annotations/CollectionBinder.java | 10 ++ .../org/hibernate/mapping/Collection.java | 6 -- .../annotations/onetomany/OneToManyTest.java | 100 +++++++++++++++++- 3 files changed, 107 insertions(+), 9 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java index 47fef3449d..fd8c19c55e 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java @@ -46,6 +46,7 @@ import org.hibernate.annotations.LazyCollectionOption; import org.hibernate.annotations.LazyGroup; import org.hibernate.annotations.Loader; import org.hibernate.annotations.ManyToAny; +import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OptimisticLock; import org.hibernate.annotations.OrderBy; import org.hibernate.annotations.Parameter; @@ -480,6 +481,15 @@ public abstract class CollectionBinder { throw new AnnotationException( message ); } + if (!isMappedBy + && oneToMany + && property.isAnnotationPresent( OnDelete.class ) + && !property.isAnnotationPresent( JoinColumn.class )) { + String message = "Unidirectional one-to-many associations annotated with @OnDelete must define @JoinColumn: "; + message += StringHelper.qualify( propertyHolder.getPath(), propertyName ); + throw new AnnotationException( message ); + } + collection.setInverse( isMappedBy ); //many to many may need some second pass informations diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java index 69d5ce3b7a..f57d8647fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java @@ -293,12 +293,6 @@ public abstract class Collection implements Fetchable, Value, Filterable { assert getKey() != null : "Collection key not bound : " + getRole(); assert getElement() != null : "Collection element not bound : " + getRole(); - if ( getKey().isCascadeDeleteEnabled() && ( !isInverse() || !isOneToMany() ) ) { - throw new MappingException( - "only inverse one-to-many associations may use on-delete=\"cascade\": " - + getRole() - ); - } if ( !getKey().isValid( mapping ) ) { throw new MappingException( "collection foreign key mapping has wrong number of columns: " diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OneToManyTest.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OneToManyTest.java index 093ab3b039..803397aee9 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OneToManyTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OneToManyTest.java @@ -6,20 +6,32 @@ */ package org.hibernate.test.annotations.onetomany; -import javax.persistence.PersistenceException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.PersistenceException; +import org.hibernate.AnnotationException; import org.hibernate.Hibernate; -import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.Transaction; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.mapping.Column; import org.hibernate.mapping.PersistentClass; @@ -36,6 +48,7 @@ import org.junit.Assert; import org.junit.Test; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -345,6 +358,46 @@ public class OneToManyTest extends BaseNonConfigCoreFunctionalTestCase { s.close(); } + @Test + public void testCascadeDeleteWithUnidirectionalAssociation() throws Exception { + OnDeleteUnidirectionalOneToManyChild child = new OnDeleteUnidirectionalOneToManyChild(); + + doInHibernate( this::sessionFactory, session -> { + OnDeleteUnidirectionalOneToManyParent parent = new OnDeleteUnidirectionalOneToManyParent(); + parent.children = Collections.singletonList( child); + session.persist( parent ); + } ); + + doInHibernate( this::sessionFactory, session -> { + session.createQuery("delete from OnDeleteUnidirectionalOneToManyParent").executeUpdate(); + } ); + + doInHibernate( this::sessionFactory, session -> { + OnDeleteUnidirectionalOneToManyChild e1 = session.get( OnDeleteUnidirectionalOneToManyChild.class, child.id ); + assertNull( "delete cascade should work", e1 ); + } ); + } + + @Test + public void testOnDeleteWithoutJoinColumn() throws Exception { + StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() + .build(); + + try { + new MetadataSources( serviceRegistry ) + .addAnnotatedClass( OnDeleteUnidirectionalOneToMany.class ) + .addAnnotatedClass( ParentUnawareChild.class ) + .getMetadataBuilder() + .build(); + } + catch ( AnnotationException e ) { + assertTrue(e.getMessage().contains( "Unidirectional one-to-many associations annotated with @OnDelete must define @JoinColumn" )); + } + finally { + StandardServiceRegistryBuilder.destroy( serviceRegistry ); + } + } + @Test public void testSimpleOneToManySet() throws Exception { Session s; @@ -503,7 +556,9 @@ public class OneToManyTest extends BaseNonConfigCoreFunctionalTestCase { Person.class, Organisation.class, OrganisationUser.class, - Model.class + Model.class, + OnDeleteUnidirectionalOneToManyParent.class, + OnDeleteUnidirectionalOneToManyChild.class }; } @@ -511,4 +566,43 @@ public class OneToManyTest extends BaseNonConfigCoreFunctionalTestCase { protected String[] getXmlFiles() { return new String[] { "org/hibernate/test/annotations/onetomany/orm.xml" }; } + + @Entity(name = "OnDeleteUnidirectionalOneToManyParent") + public static class OnDeleteUnidirectionalOneToManyParent { + + @Id + @GeneratedValue + Long id; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "a_id") + @OnDelete(action = OnDeleteAction.CASCADE) + List children; + } + + @Entity(name = "OnDeleteUnidirectionalOneToManyChild") + public static class OnDeleteUnidirectionalOneToManyChild { + + @Id + @GeneratedValue + Long id; + } + + @Entity(name = "OnDeleteUnidirectionalOneToMany") + public static class OnDeleteUnidirectionalOneToMany { + + @Id + Long id; + + @OneToMany(cascade = CascadeType.ALL) + @OnDelete(action = OnDeleteAction.CASCADE) + List children; + } + + @Entity(name = "ParentUnawareChild") + public static class ParentUnawareChild { + + @Id + Long id; + } }