Skip to content

Commit

Permalink
Merge pull request #198 from croz-ltd/feature_returniInitializedEntit…
Browse files Browse the repository at this point in the history
…yOnRegistryCreateAndUpdate

Avoid lazy init exception in DefaultRegistryDataService
  • Loading branch information
jzrilic authored Mar 7, 2024
2 parents 2ffe052 + f46641e commit 1025cfa
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.croz.nrich.registry.core.model.RegistryDataConfiguration;
import net.croz.nrich.registry.core.model.RegistryDataConfigurationHolder;
import net.croz.nrich.registry.core.support.ManagedTypeWrapper;
import net.croz.nrich.registry.data.util.HibernateUtil;
import net.croz.nrich.search.api.converter.StringToEntityPropertyMapConverter;
import net.croz.nrich.search.api.model.SearchConfiguration;
import net.croz.nrich.search.api.model.property.SearchPropertyConfiguration;
Expand Down Expand Up @@ -104,7 +105,7 @@ public <T> T create(String classFullName, Object entityData) {

modelMapper.map(entityData, instance);

return entityManager.merge(instance);
return mergeAndInitializeEntity(instance);
}

@Transactional
Expand All @@ -129,7 +130,7 @@ public <T> T update(String classFullName, Object id, Object entityData) {

modelMapper.map(entityData, instance);

return entityManager.merge(instance);
return mergeAndInitializeEntity(instance);
}

@Transactional
Expand Down Expand Up @@ -217,4 +218,12 @@ private void setIdFieldToOriginalValue(ManagedTypeWrapper managedTypeWrapper, Ob

modelMapper.map(idValueMap, entityData);
}

private <T> T mergeAndInitializeEntity(T instance) {
T mergedInstance = entityManager.merge(instance);

HibernateUtil.initialize(mergedInstance);

return mergedInstance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2020-2023 CROZ d.o.o, 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
*
* https://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 net.croz.nrich.registry.data.util;

import lombok.SneakyThrows;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.BeanUtils;

import jakarta.persistence.Entity;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public final class HibernateUtil {

private static final String PROPERTY_PATH_FORMAT = "%s.%s";

private static final String COLLECTION_ELEMENT_NAME_FORMAT = "%s-%s";

private HibernateUtil() {
}

public static void initialize(Object entity) {
initializeInternal(entity, null, new ArrayList<>());
}

private static void initializeInternal(Object entity, String propertyPath, List<String> alreadyInitializedProperties) {
if (entity == null || alreadyInitializedProperties.contains(propertyPath)) {
return;
}

Class<?> entityType = resolveEntityType(entity);

if (!isManagedType(entityType)) {
return;
}

if (!Hibernate.isInitialized(entity)) {
Hibernate.initialize(entity);
}

alreadyInitializedProperties.add(propertyPath);

Arrays.stream(BeanUtils.getPropertyDescriptors(entityType)).forEach(propertyDescriptor -> {
String propertyName = propertyDescriptor.getName();
Object propertyValue = getPropertyValue(entity, propertyDescriptor);

if (propertyValue instanceof Collection<?> collection) {
int index = 0;
for (Object collectionElementValue : collection) {
String collectionElementPropertyName = String.format(COLLECTION_ELEMENT_NAME_FORMAT, propertyName, index++);
String calculatedPropertyPath = calculatePropertyPath(propertyPath, collectionElementPropertyName);

initializeInternal(collectionElementValue, calculatedPropertyPath, alreadyInitializedProperties);
}
}
else {
String calculatedPropertyPath = calculatePropertyPath(propertyPath, propertyName);

initializeInternal(propertyValue, calculatedPropertyPath, alreadyInitializedProperties);
}
});
}

@SneakyThrows
private static Object getPropertyValue(Object entity, PropertyDescriptor propertyDescriptor) {
Method method = propertyDescriptor.getReadMethod();

if (method == null) {
return null;
}

return method.invoke(entity);
}

private static Class<?> resolveEntityType(Object entity) {
Class<?> type = entity.getClass();

if (entity instanceof HibernateProxy hibernateProxy) {
type = hibernateProxy.getHibernateLazyInitializer().getPersistentClass();
}

return type;
}

private static boolean isManagedType(Class<?> entityType) {
return entityType.getAnnotation(Entity.class) != null;
}

private static String calculatePropertyPath(String existingPropertyPath, String propertyName) {
return existingPropertyPath == null ? propertyName : String.format(PROPERTY_PATH_FORMAT, existingPropertyPath, propertyName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import net.croz.nrich.registry.data.stub.RegistryTestEntity;
import net.croz.nrich.registry.test.BaseControllerTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
Expand All @@ -40,6 +39,7 @@
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createDeleteRegistryRequest;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createListRegistryRequest;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createRegistryRequest;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createRegistryRequestWithAssociation;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createRegistryTestEmbeddedUserGroup;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createRegistryTestEntity;
import static net.croz.nrich.registry.data.testutil.RegistryDataGeneratingUtil.createRegistryTestEntityList;
Expand Down Expand Up @@ -108,6 +108,21 @@ void shouldCreateRegistryEntity() throws Exception {
.andExpect(jsonPath("$.name").value(entityName));
}

@Test
void shouldCreateRegistryEntityWithInitializedAssociation() throws Exception {
// given
RegistryTestEntity entity = executeInTransaction(platformTransactionManager, () -> createRegistryTestEntity(entityManager));
String requestUrl = fullUrl("create");
CreateRegistryRequest request = createRegistryRequestWithAssociation(objectMapper, REGISTRY_TYPE_NAME, entity);

// when
ResultActions result = performPostRequest(requestUrl, request);

// then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.parent.id").value(entity.getId()));
}

@Test
void shouldReturnErrorWhenCreateInputDataIsNotValid() throws Exception {
// given
Expand Down Expand Up @@ -228,8 +243,6 @@ void shouldDeleteRegistryEntityWithEmbeddedId() throws Exception {
result.andExpect(status().isOk());
}

// TODO enable when https://hibernate.atlassian.net/browse/HHH-15875 is solved.
@Disabled
@Test
void shouldNotFailListingRegistryWithLazyAssociationsInEmbeddedId() throws Exception {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ public static CreateRegistryRequest createRegistryRequest(ObjectMapper objectMap
return createRegistryRequest(objectMapper, classFullName, name, 50);
}

@SneakyThrows
public static CreateRegistryRequest createRegistryRequestWithAssociation(ObjectMapper objectMapper, String classFullName, RegistryTestEntity parent) {
CreateRegistryRequest request = new CreateRegistryRequest();

request.setClassFullName(classFullName);
request.setJsonEntityData(objectMapper.writeValueAsString(Map.of("name", "name", "age", 40, "parent", parent)));

return request;
}

@SneakyThrows
public static CreateRegistryRequest createRegistryRequest(ObjectMapper objectMapper, String classFullName, String name, Integer age) {
CreateRegistryRequest request = new CreateRegistryRequest();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2020-2023 CROZ d.o.o, 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
*
* https://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 net.croz.nrich.registry.data.util;

import net.croz.nrich.registry.RegistryTestConfiguration;
import net.croz.nrich.registry.data.util.stub.HibernateUtilTestEntity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.transaction.PlatformTransactionManager;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

import static net.croz.nrich.registry.data.util.testutil.HibernateUtilGeneratingUtil.createAndSaveHibernateUtilTestEntity;
import static net.croz.nrich.registry.testutil.PersistenceTestUtil.executeInTransaction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchException;

@SpringJUnitWebConfig(RegistryTestConfiguration.class)
class HibernateUtilTest {

@Autowired
private PlatformTransactionManager transactionManager;

@PersistenceContext
private EntityManager entityManager;

@Test
void shouldInitializeEntity() {
// given
HibernateUtilTestEntity entity = executeInTransaction(transactionManager, () -> createAndSaveHibernateUtilTestEntity(entityManager));

// when
HibernateUtilTestEntity loadedEntity = loadAndInitializeEntity(entity.getId());

// and when
Exception exception = catchException(() -> {
loadedEntity.getParent().getId();
loadedEntity.getChildren().get(0).getId();
});

// then
assertThat(exception).isNull();
}

private HibernateUtilTestEntity loadAndInitializeEntity(Long id) {
return executeInTransaction(transactionManager, () -> {
HibernateUtilTestEntity loadedEntity = entityManager.find(HibernateUtilTestEntity.class, id);

HibernateUtil.initialize(loadedEntity);

return loadedEntity;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2020-2023 CROZ d.o.o, 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
*
* https://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 net.croz.nrich.registry.data.util.stub;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.util.List;

@Setter
@Getter
@Entity
public class HibernateUtilTestEntity {

@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Id
private Long id;

private String name;

@ManyToOne(fetch = FetchType.LAZY)
private HibernateUtilTestEntity parent;

@OneToMany(fetch = FetchType.LAZY)
private List<HibernateUtilTestEntity> children;

@Getter(AccessLevel.NONE)
private String hiddenName;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2020-2023 CROZ d.o.o, 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
*
* https://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 net.croz.nrich.registry.data.util.testutil;

import net.croz.nrich.registry.data.util.stub.HibernateUtilTestEntity;

import jakarta.persistence.EntityManager;
import java.util.List;

public final class HibernateUtilGeneratingUtil {

private HibernateUtilGeneratingUtil() {
}

public static HibernateUtilTestEntity createAndSaveHibernateUtilTestEntity(EntityManager entityManager) {
HibernateUtilTestEntity firstChild = createHibernateUtilTestEntityInternal(entityManager, "first", null, null);
HibernateUtilTestEntity secondChild = createHibernateUtilTestEntityInternal(entityManager, "second", null, null);
HibernateUtilTestEntity parent = createHibernateUtilTestEntityInternal(entityManager, "parent", null, null);

return createHibernateUtilTestEntityInternal(entityManager, "main", parent, List.of(firstChild, secondChild));
}

private static HibernateUtilTestEntity createHibernateUtilTestEntityInternal(EntityManager entityManager, String name, HibernateUtilTestEntity parent, List<HibernateUtilTestEntity> children) {
HibernateUtilTestEntity entity = new HibernateUtilTestEntity();

entity.setName(name);
entity.setParent(parent);
entity.setChildren(children);

entityManager.persist(entity);

return entity;
}
}

0 comments on commit 1025cfa

Please sign in to comment.