diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java index 9051ed439..df763ef78 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2011 the original author or authors. + * Copyright 2011-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,21 @@ package org.springframework.data.jpa.repository.support; import java.io.Serializable; import java.lang.reflect.Field; -import java.lang.reflect.Member; -import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import javax.persistence.IdClass; import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.SingularAttribute; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.NotWritablePropertyException; import org.springframework.data.repository.core.EntityInformation; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -37,13 +44,13 @@ import org.springframework.util.ReflectionUtils; public class JpaMetamodelEntityInformation extends JpaEntityInformationSupport implements JpaEntityInformation { - private final SingularAttribute attribute; + private final IdMetadata idMetadata; /** * Creates a new {@link JpaMetamodelEntityInformation} for the given domain class and {@link Metamodel}. * - * @param domainClass - * @param metamodel + * @param domainClass must not be {@literal null}. + * @param metamodel must not be {@@iteral null}. */ public JpaMetamodelEntityInformation(Class domainClass, Metamodel metamodel) { @@ -60,21 +67,36 @@ public class JpaMetamodelEntityInformation extends J throw new IllegalArgumentException("The given domain class does not contain an id attribute!"); } - IdentifiableType identifiableType = (IdentifiableType) type; - this.attribute = identifiableType.getId(identifiableType.getIdType().getJavaType()); + this.idMetadata = new IdMetadata((IdentifiableType) type); } /* * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.IdAware#getId(java.lang.Object - * ) + * @see org.springframework.data.repository.core.EntityInformation#getId(java.lang.Object) */ @SuppressWarnings("unchecked") public ID getId(T entity) { - return (ID) getMemberValue(attribute.getJavaMember(), entity); + BeanWrapper entityWrapper = new DirectFieldAccessFallbackBeanWrapper(entity); + + if (idMetadata.hasSimpleId()) { + return (ID) entityWrapper.getPropertyValue(idMetadata.getSimpleIdAttribute().getName()); + } + + BeanWrapper idWrapper = new DirectFieldAccessFallbackBeanWrapper(idMetadata.getType()); + boolean partialIdValueFound = false; + + for (SingularAttribute attribute : idMetadata) { + Object propertyValue = entityWrapper.getPropertyValue(attribute.getName()); + + if (propertyValue != null) { + partialIdValueFound = true; + } + + idWrapper.setPropertyValue(attribute.getName(), propertyValue); + } + + return (ID) (partialIdValueFound ? idWrapper.getWrappedInstance() : null); } /* @@ -85,30 +107,7 @@ public class JpaMetamodelEntityInformation extends J */ @SuppressWarnings("unchecked") public Class getIdType() { - - return (Class) attribute.getJavaType(); - } - - /** - * Returns the value of the given {@link Member} of the given {@link Object} . - * - * @param member - * @param source - * @return - */ - private static Object getMemberValue(Member member, Object source) { - - if (member instanceof Field) { - Field field = (Field) member; - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, source); - } else if (member instanceof Method) { - Method method = (Method) member; - ReflectionUtils.makeAccessible(method); - return ReflectionUtils.invokeMethod(method, source); - } - - throw new IllegalArgumentException("Given member is neither Field nor Method!"); + return (Class) idMetadata.getType(); } /* @@ -118,7 +117,99 @@ public class JpaMetamodelEntityInformation extends J * getIdAttribute() */ public SingularAttribute getIdAttribute() { + return idMetadata.getSimpleIdAttribute(); + } - return attribute; + /** + * Simple value object to encapsulate id specific metadata. + * + * @author Oliver Gierke + */ + private static class IdMetadata implements Iterable> { + + private final IdentifiableType type; + private final Set> attributes; + + @SuppressWarnings("unchecked") + public IdMetadata(IdentifiableType source) { + + this.type = source; + this.attributes = (Set>) (source.hasSingleIdAttribute() ? Collections + .singleton(source.getId(source.getIdType().getJavaType())) : source.getIdClassAttributes()); + } + + public boolean hasSimpleId() { + return attributes.size() == 1; + } + + public Class getType() { + + try { + return type.getIdType().getJavaType(); + } catch (IllegalStateException e) { + // see https://hibernate.onjira.com/browse/HHH-6951 + IdClass annotation = type.getJavaType().getAnnotation(IdClass.class); + return annotation == null ? null : annotation.value(); + } + } + + public SingularAttribute getSimpleIdAttribute() { + return attributes.iterator().next(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + public Iterator> iterator() { + return attributes.iterator(); + } + } + + /** + * Custom extension of {@link BeanWrapperImpl} that falls back to direct field access in case the object or type being + * wrapped does not use accessor methods. + * + * @author Oliver Gierke + */ + private static class DirectFieldAccessFallbackBeanWrapper extends BeanWrapperImpl { + + public DirectFieldAccessFallbackBeanWrapper(Object entity) { + super(entity); + } + + public DirectFieldAccessFallbackBeanWrapper(Class type) { + super(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.BeanWrapperImpl#getPropertyValue(java.lang.String) + */ + @Override + public Object getPropertyValue(String propertyName) throws BeansException { + try { + return super.getPropertyValue(propertyName); + } catch (NotReadablePropertyException e) { + Field field = ReflectionUtils.findField(getWrappedClass(), propertyName); + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, getWrappedInstance()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.BeanWrapperImpl#setPropertyValue(java.lang.String, java.lang.Object) + */ + @Override + public void setPropertyValue(String propertyName, Object value) throws BeansException { + try { + super.setPropertyValue(propertyName, value); + } catch (NotWritablePropertyException e) { + Field field = ReflectionUtils.findField(getWrappedClass(), propertyName); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, getWrappedInstance(), value); + } + } } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 16f9af5e8..823513b67 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2011 the original author or authors. + * Copyright 2008-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -195,16 +195,20 @@ public class SimpleJpaRepository implements JpaRepos Assert.notNull(id, "The given id must not be null!"); - String placeholder = provider.getCountQueryPlaceholder(); - String entityName = entityInformation.getEntityName(); - String idAttributeName = entityInformation.getIdAttribute().getName(); + if (entityInformation.getIdAttribute() != null) { - String existsQuery = String.format(EXISTS_QUERY_STRING, placeholder, entityName, idAttributeName); + String placeholder = provider.getCountQueryPlaceholder(); + String entityName = entityInformation.getEntityName(); + String idAttributeName = entityInformation.getIdAttribute().getName(); + String existsQuery = String.format(EXISTS_QUERY_STRING, placeholder, entityName, idAttributeName); - TypedQuery query = em.createQuery(existsQuery, Long.class); - query.setParameter("id", id); + TypedQuery query = em.createQuery(existsQuery, Long.class); + query.setParameter("id", id); - return query.getSingleResult() == 1; + return query.getSingleResult() == 1; + } else { + return findOne(id) != null; + } } /* diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClass.java b/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClass.java new file mode 100644 index 000000000..e50c2af9d --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClass.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.domain.sample; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; + +import org.springframework.data.domain.Persistable; + +/** + * Sample entity using {@link IdClass} annotation to demarcate ids. + * + * @author Oliver Gierke + */ +@Entity +@IdClass(SampleWithIdClassPK.class) +public class SampleWithIdClass implements Persistable { + + private static final long serialVersionUID = 1L; + + @Id + Long first; + + @Id + Long second; + + private boolean isNew; + + protected SampleWithIdClass() { + this.isNew = true; + } + + public SampleWithIdClass(Long first, Long second) { + this.first = first; + this.second = second; + this.isNew = true; + } + + /** + * @return the first + */ + public Long getFirst() { + return first; + } + + /** + * @return the second + */ + public Long getSecond() { + return second; + } + + /* (non-Javadoc) + * @see org.springframework.data.domain.Persistable#getId() + */ + public SampleWithIdClassPK getId() { + return new SampleWithIdClassPK(first, second); + } + + /* (non-Javadoc) + * @see org.springframework.data.domain.Persistable#isNew() + */ + public boolean isNew() { + return this.isNew; + } + + public void setNotNew() { + this.isNew = false; + } +} diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClassPK.java b/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClassPK.java new file mode 100644 index 000000000..5d4502844 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/domain/sample/SampleWithIdClassPK.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.domain.sample; + +import static org.springframework.util.ObjectUtils.*; + +import java.io.Serializable; + +/** + * + * @author Oliver Gierke + */ +public class SampleWithIdClassPK implements Serializable { + + private static final long serialVersionUID = 23126782341L; + + private Long first; + private Long second; + + public SampleWithIdClassPK() { + + } + + public SampleWithIdClassPK(Long first, Long second) { + this.first = first; + this.second = second; + } + + public void setFirst(Long first) { + this.first = first; + } + + public void setSecond(Long second) { + this.second = second; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !(obj.getClass().equals(getClass()))) { + return false; + } + + SampleWithIdClassPK that = (SampleWithIdClassPK) obj; + + return nullSafeEquals(this.first, that.first) && nullSafeEquals(this.second, that.second); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += nullSafeHashCode(this.first); + result += nullSafeHashCode(this.second); + + return result; + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationIntegrationTests.java index 27d125b67..0740e8614 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011 the original author or authors. + * Copyright 2011-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,10 @@ import javax.persistence.metamodel.Metamodel; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.jpa.domain.AbstractPersistable; +import org.springframework.data.jpa.domain.sample.SampleWithIdClass; +import org.springframework.data.jpa.domain.sample.SampleWithIdClassPK; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.repository.core.EntityInformation; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -62,4 +65,31 @@ public class JpaMetamodelEntityInformationIntegrationTests { JpaEntityInformation information = JpaEntityInformationSupport.getMetadata(AbstractPersistable.class, em); assertEquals(Serializable.class, information.getIdType()); } + + /** + * @see DATAJPA-50 + */ + @Test + public void detectsIdClass() { + + EntityInformation information = JpaEntityInformationSupport.getMetadata( + SampleWithIdClass.class, em); + assertThat(information.getIdType(), is(typeCompatibleWith(SampleWithIdClassPK.class))); + } + + /** + * @see DATAJPA-50 + */ + @Test + public void returnsIdInstanceCorrectly() { + + SampleWithIdClass entity = new SampleWithIdClass(2L, 4L); + + JpaEntityInformation information = JpaEntityInformationSupport.getMetadata( + SampleWithIdClass.class, em); + Object id = information.getId(entity); + + assertThat(id, is(SampleWithIdClassPK.class)); + assertThat(id, is((Object) new SampleWithIdClassPK(2L, 4L))); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationUnitTest.java b/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationUnitTest.java new file mode 100644 index 000000000..04f6908b1 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationUnitTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.support; + +import static java.util.Arrays.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.metamodel.IdentifiableType; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.Type; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.jpa.domain.sample.SampleWithIdClass; +import org.springframework.data.jpa.domain.sample.SampleWithIdClassPK; + +/** + * Unit tests for {@link JpaMetamodelEntityInformation}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class JpaMetamodelEntityInformationUnitTest { + + @Mock + Metamodel metamodel; + + @Mock + IdentifiableType type; + @Mock + SingularAttribute first, second; + + @Mock + @SuppressWarnings("rawtypes") + Type idType; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + + when(first.getName()).thenReturn("first"); + when(second.getName()).thenReturn("second"); + Set> attributes = new HashSet>( + asList(first, second)); + + when(type.getIdClassAttributes()).thenReturn(attributes); + + when(metamodel.managedType(SampleWithIdClass.class)).thenReturn(type); + + when(type.getIdType()).thenReturn(idType); + when(idType.getJavaType()).thenReturn(SampleWithIdClassPK.class); + } + + /** + * @see DATAJPA-50 + */ + @Test + public void doesNotCreateIdIfAllPartialAttributesAreNull() { + + JpaMetamodelEntityInformation information = new JpaMetamodelEntityInformation( + SampleWithIdClass.class, metamodel); + + SampleWithIdClass entity = new SampleWithIdClass(null, null); + assertThat(information.getId(entity), is(nullValue())); + + entity = new SampleWithIdClass(2L, null); + assertThat(information.getId(entity), is(notNullValue())); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java index dcf311360..fdc5dd0e2 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2011 the original author or authors. + * Copyright 2008-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.jpa.domain.sample.SampleEntity; import org.springframework.data.jpa.domain.sample.SampleEntityPK; +import org.springframework.data.jpa.domain.sample.SampleWithIdClass; +import org.springframework.data.jpa.domain.sample.SampleWithIdClassPK; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @@ -47,11 +50,13 @@ public class JpaRepositoryTests { EntityManager em; JpaRepository repository; + CrudRepository idClassRepository; @Before public void setUp() { repository = new JpaRepositoryFactory(em).getRepository(SampleEntityRepository.class); + idClassRepository = new JpaRepositoryFactory(em).getRepository(SampleWithIdClassRepository.class); } @Test @@ -68,7 +73,28 @@ public class JpaRepositoryTests { assertThat(repository.count(), is(0L)); } + /** + * DATAJPA-50 + */ + @Test + public void executesCrudOperationsForEntityWithIdClass() { + + SampleWithIdClass entity = new SampleWithIdClass(1L, 1L); + idClassRepository.save(entity); + + assertThat(entity.getFirst(), is(notNullValue())); + assertThat(entity.getSecond(), is(notNullValue())); + + SampleWithIdClassPK id = new SampleWithIdClassPK(entity.getFirst(), entity.getSecond()); + + assertThat(idClassRepository.findOne(id), is(entity)); + } + private static interface SampleEntityRepository extends JpaRepository { } + + private static interface SampleWithIdClassRepository extends CrudRepository { + + } } diff --git a/src/test/resources/META-INF/persistence.xml b/src/test/resources/META-INF/persistence.xml index ebda8a710..9f45d06a5 100644 --- a/src/test/resources/META-INF/persistence.xml +++ b/src/test/resources/META-INF/persistence.xml @@ -11,6 +11,7 @@ org.springframework.data.jpa.domain.sample.AuditableRole org.springframework.data.jpa.domain.sample.SampleEntity org.springframework.data.jpa.domain.sample.SampleEntityPK + org.springframework.data.jpa.domain.sample.SampleWithIdClass true