Spring boot & mongodb, domain class issue with findAll() and findById() - spring

I am implementing a spring boot service with an underlying mongodb database for data storage.
In this service I have to use some domain classes that comes from another library which I can not alter. Unfortunately these classes have instance variables marked as final. For example here is the Discount domain class:
#Value
#NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
#AllArgsConstructor(access = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#JsonDeserialize(builder = Discount.DiscountBuilderImpl.class)
#EqualsAndHashCode(callSuper = true)
#ToString(callSuper = true)
public class Discount extends AbstractEntityBase {
#NonNull
UUID id;
#NonNull
String no;
#JsonProperty("iNo")
Integer iNo;
#NonNull
LocalizedTexts designation;
LocalizedTexts printText;
boolean isAutomatic;
LocalDate validFromDate;
LocalDate validToDate;
LocalTime validFromTime;
LocalTime validToTime;
String checkScriptParameters;
ReferenceScript checkScript;
#NonNull
ReferenceScript calculationScript;
public static DiscountBuilder<?, ?> builder(UUID id,
String no,
LocalizedTexts designation,
ReferenceScript calculationScript,
boolean isAutomatic) {
return new DiscountBuilderImpl()
.id(id)
.no(no)
.designation(designation)
.calculationScript(calculationScript)
.isAutomatic(isAutomatic);
}
/**
* Overwritten getters for optional properties
*/
...
#JsonPOJOBuilder(withPrefix = "")
#JsonIgnoreProperties(ignoreUnknown = true)
public static final class DiscountBuilderImpl
extends DiscountBuilder<Discount, DiscountBuilderImpl> {
}
}
In my test class I am testing save and find (findAll, findById) operations. Save operations work fine but I have issues with findAll and findById methods. Here is such a test class:
#RunWith(SpringRunner.class)
#DataMongoTest
public class TestDiscountRepository {
#Autowired
DiscountRepository discountRepository;
#Before
public void init(){
}
#After
public void resetMongoDb() {
//discountRepository.deleteAll();
}
#Test
public void save_success() {
UUID id = UUID.randomUUID();
...
Discount discount = Discount.builder(id, no, designation, calculationScript, true)
.iNo(iNo)
.printText(printText)
.isAutomatic(isAutomatic)
.validFromDate(validFromDate)
.validToDate(validToDate)
.validFromTime(validFromTime)
.validToTime(validToTime)
.checkScriptParameters(checkScriptParameters)
.checkScript(checkScript)
.build();
Discount savedDiscount = discountRepository.save(discount);
assertThat(savedDiscount).isNotNull();
assertThat(savedDiscount.getId()).isNotNull();
}
#Test
public void save_bulk_success() {
UUID id1 = UUID.randomUUID();
...
UUID id2 = UUID.randomUUID();
...
UUID id3 = UUID.randomUUID();
...
List<Discount> discounts = Arrays.asList(
Discount.builder(id1, no1, designation1, calculationScript1, true)
...
.build(),
Discount.builder(id2, no2, designation2, calculationScript2, true)
...
.build(),
Discount.builder(id3, no3, designation3, calculationScript3, true)
...
.build()
);
List<Discount> allDiscounts = discountRepository.saveAll(discounts);
AtomicInteger validIdFound = new AtomicInteger();
allDiscounts.forEach(discount -> {
if(discount.getId() != null) {
validIdFound.getAndIncrement();
}
});
assertThat(validIdFound.intValue()).isEqualTo(3);
}
#Test
public void find_all_success() {
List<Discount> discounts = discountRepository.findAll();
Assert.assertNotNull(discounts);
}
#Test
public void find_by_id_success() {
UUID id = UUID.randomUUID();
String no = "900";
...
Discount discount = Discount.builder(id, no, designation, calculationScript, true)
...
.build();
Discount savedDiscount = discountRepository.save(discount);
Optional<Discount> result = discountRepository.findById(savedDiscount.getId());
Assert.assertNotNull(result.get());
}
}
And here is the exception I am getting when I try to run/test findAll() and findById():
java.lang.IllegalStateException: Cannot set property id because no setter, no wither and it's not part of the persistence constructor private net.mspos.possible.svc_pos_controller_data.entity.Discount()!
at org.springframework.data.mapping.model.InstantiationAwarePropertyAccessor.setProperty(InstantiationAwarePropertyAccessor.java:118)
at org.springframework.data.mapping.model.ConvertingPropertyAccessor.setProperty(ConvertingPropertyAccessor.java:64)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readAndPopulateIdentifier(MappingMongoConverter.java:450)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:418)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:394)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:356)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:292)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:288)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:107)
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3207)
at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2822)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2529)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2499)
at org.springframework.data.mongodb.core.MongoTemplate.findById(MongoTemplate.java:888)
at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.findById(SimpleMongoRepository.java:132)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at jdk.proxy2/jdk.proxy2.$Proxy93.findById(Unknown Source)
at net.mspos.possible.svc_pos_controller_data.repository.TestDiscountRepository.find_by_id_success(TestDiscountRepository.java:209)
Here is also how a document into the database looks like:
Any ideas on how I could overcome this issue? Unfortunately altering the domain classes is not an option.

Finally the easiest and more convenient solution in my case is to avoid using repositories to map the domain classes and provide basic CRUD operations.
Instead of that I am using the MongoCollection interface alongside with ObjectMapper to serialize/deserialize objects into mongodb documents for all CRUD operations. For instance a relevant service for a domain class could look like:
#Service
#EnableConfigurationProperties(MongoProperties.class)
public class DiscountMongoService implements BasicMongoService {
private MongoProperties mongoProperties;
private CustomMongoConfig customMongoConfig;
private BusinessEntityMapper mapper;
private MongoCollection<Document> discountCollection;
#Autowired
public DiscountMongoService(MongoProperties mongoProperties, CustomMongoConfig customMongoConfig, BusinessEntityMapper mapper) {
this.mongoProperties = mongoProperties;
this.customMongoConfig = customMongoConfig;
this.mapper = mapper;
discountCollection =
this.customMongoConfig.mongoClient().getDatabase(this.mongoProperties.getDatabase()).getCollection(Discount.class.getSimpleName());
}
// Insert
public boolean insertOne(Discount discount) throws JsonProcessingException {
Document discountDoc = transformEntityIntoDocumentWithMongodbId(discount);
InsertOneResult result = discountCollection.insertOne(discountDoc);
System.out.println(result);
return result.wasAcknowledged();
}
....
// Find
public List<Discount> findAll() {
List<Discount> discounts = new ArrayList<>();
discountCollection.find().forEach(dd -> {
Discount discount;
try {
discount = (Discount) mapper.getBusinessEntityFromJSON(dd.toJson(),Discount.class);
discounts.add(discount);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
return discounts;
}

Related

How to only allow specific fields to sort by in a Spring Data JPA Repository Pageable?

Using a Pageable parameter in a Spring Data JPA Repository allows for specifying fields to sort by like: PageRequest.of(0, 50, Sort.by("field1", "field2")), which would sort by field1 and field2 ascending.
It works by appending an ORDER BY clause directly by doing SQL injection which would result in a JPA query like SELECT a FROM SomeEntity a ORDER BY field1, field2. However, if a non-existing field name is passed in it would result in a org.springframework.dao.InvalidDataAccessApiUsageException as seen below.
How do you whitelist, only allow specific fields, or validate the sorting without adding boilerplate code in a service that wraps the repository? Same goes for in a #RestController ensuring that a 400 level HttpStatus.BAD_REQUEST is returned to the API?
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.QueryException: could not resolve property: field1 of: com.example.SomeEntity [SELECT a FROM com.example.SomeEntity a order by a.field1 asc, a.field2 asc]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:531)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:154)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:149)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy340.searchPaged(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.zeroturnaround.jrebel.integration.springdata.RepositoryReloadingProxyFactoryBuilder$ReloadingMethodHandler.invoke(RepositoryReloadingProxyFactoryBuilder.java:80)
...
Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: could not resolve property: field1 of: com.example.SomeEntity [SELECT a FROM com.example.SomeEntity a order by a.field1 asc, a.field2 asc]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:725)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:113)
at jdk.internal.reflect.GeneratedMethodAccessor749.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:366)
at com.sun.proxy.$Proxy265.createQuery(Unknown Source)
at jdk.internal.reflect.GeneratedMethodAccessor749.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314)
at com.sun.proxy.$Proxy265.createQuery(Unknown Source)
at org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery.createJpaQuery(AbstractStringBasedJpaQuery.java:150)
at org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery.doCreateQuery(AbstractStringBasedJpaQuery.java:86)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.createQuery(AbstractJpaQuery.java:226)
at org.springframework.data.jpa.repository.query.JpaQueryExecution$PagedExecution.doExecute(JpaQueryExecution.java:175)
at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:618)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 127 more
Caused by: org.hibernate.QueryException: could not resolve property: field1 of: com.example.SomeEntity [SELECT a FROM com.example.SomeEntity a order by a.field1 asc, a.field2 asc]
at org.hibernate.QueryException.generateQueryException(QueryException.java:120)
at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:103)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:220)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:144)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:113)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:73)
at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:162)
at org.hibernate.internal.AbstractSharedSessionContract.getQueryPlan(AbstractSharedSessionContract.java:604)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:716)
... 154 more
Caused by: org.hibernate.QueryException: could not resolve property: field1 of: com.example.SomeEntity
at org.hibernate.persister.entity.AbstractPropertyMapping.propertyException(AbstractPropertyMapping.java:77)
at org.hibernate.persister.entity.AbstractPropertyMapping.toType(AbstractPropertyMapping.java:71)
at org.hibernate.persister.entity.AbstractEntityPersister.toType(AbstractEntityPersister.java:2043)
at org.hibernate.hql.internal.ast.tree.FromElementType.getPropertyType(FromElementType.java:412)
at org.hibernate.hql.internal.ast.tree.FromElement.getPropertyType(FromElement.java:520)
at org.hibernate.hql.internal.ast.tree.DotNode.getDataType(DotNode.java:694)
at org.hibernate.hql.internal.ast.tree.DotNode.prepareLhs(DotNode.java:269)
at org.hibernate.hql.internal.ast.tree.DotNode.resolve(DotNode.java:209)
at org.hibernate.hql.internal.ast.HqlSqlWalker.resolve(HqlSqlWalker.java:1053)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.expr(HqlSqlBaseWalker.java:1303)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.orderExpr(HqlSqlBaseWalker.java:1887)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.orderExprs(HqlSqlBaseWalker.java:1681)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.orderClause(HqlSqlBaseWalker.java:1654)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:666)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:325)
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:273)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:276)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:192)
... 160 more
I ended up using JSR-303 validations on the repository methods to whitelist the sort fields.
Enable method validation post processor to run JSR-303 validation annotations at the method level.
ValidationConfig.java
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
Create a validation that takes in a list of sort fields to validate against.
AllowSortFields.java
#Documented
#Constraint(validatedBy = {AllowSortFieldsValidator.class})
#Target({ANNOTATION_TYPE, TYPE, FIELD, PARAMETER})
#Retention(RUNTIME)
public #interface AllowSortFields {
String message() default "Sort field values provided are not within the allowed fields that are sortable.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* Specify an array of fields that are allowed.
*
* #return the allowed sort fields
*/
String[] value() default {};
}
AllowSortFieldsValidator.java
/**
* Validates a list of sort fields within a Pageable against an allowed list.
*/
public class AllowSortFieldsValidator implements ConstraintValidator<AllowSortFields, Pageable> {
private List<String> allowedSortFields;
static final String PROPERTY_NOT_FOUND_MESSAGE = "The following sort fields [%s] are not within the allowed fields. "
+ "Allowed sort fields are: [%s]";
#Override
public void initialize(AllowSortFields constraintAnnotation) {
allowedSortFields = Arrays.asList(constraintAnnotation.value());
}
#Override
public boolean isValid(Pageable value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
if (CollectionUtils.isEmpty(allowedSortFields)) {
return true;
}
// ignore unsorted
Sort sort = value.getSort();
if (sort.isUnsorted()) {
return true;
}
String fieldsNotFound = fieldsNotFoundAsCommaDelimited(sort);
// all found fields are allowed
if (StringUtils.isEmpty(fieldsNotFound)) {
return true;
}
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(String.format(PROPERTY_NOT_FOUND_MESSAGE, fieldsNotFound, String.join(",", allowedSortFields)))
.addConstraintViolation();
return false;
}
private String fieldsNotFoundAsCommaDelimited(Sort sort) {
String fieldsNotFound = sort.stream()
.map(order -> order.getProperty())
.filter(property -> !allowedSortFields.contains(property))
.collect(joining(","));
return fieldsNotFound;
}
}
AllowSortFieldsValidatorSmallTest.java
public class AllowSortFieldsValidatorSmallTest {
private static final String[] ALLOWED_SORT_FIELDS = new String[]{"allowed1", "allowed2"};
private static final String ALLOWED_SORT_FIELDS_DELIMITED = String.join(",", Arrays.asList(ALLOWED_SORT_FIELDS));
private static Validator validator;
#BeforeClass
public static void setupValidator() throws Exception {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
#Test
public void isValid_TwoOfFourFieldsAllowed_FalseWithExpectedMessageExplainingDisallowedFields() {
List<String> sortFields = List.of("allowed1", "allowed2|desc", "notfound1", "not.found2");
AllowedSortFields toValidate = newAllowedSortFields(sortFields);
Set<ConstraintViolation<AllowedSortFields>> constraintViolations = validator.validate(toValidate, Default.class);
String expected = String.format(AllowSortFieldsValidator.PROPERTY_NOT_FOUND_MESSAGE, "notfound1,not.found2", ALLOWED_SORT_FIELDS_DELIMITED);
String actual = getConstraintMessages(constraintViolations);
assertEquals(expected, actual);
}
#Test
public void isValid_NoSortFields_True() {
List<String> sortFields = null;
AllowedSortFields toValidate = newAllowedSortFields(sortFields);
Set<ConstraintViolation<AllowedSortFields>> constraintViolations = validator.validate(toValidate, Default.class);
assertTrue(constraintViolations.isEmpty());
}
#Test
public void isValid_EmptyAllowedSortFields_True() {
List<String> sortFields = List.of("allowed1", "allowed2|desc", "notfound1", "not.found2");
EmptyAllowedSortFields toValidate = newEmptyAllowedSortFields(sortFields);
Set<ConstraintViolation<EmptyAllowedSortFields>> constraintViolations = validator.validate(toValidate, Default.class);
assertTrue(constraintViolations.isEmpty());
}
#Test
public void isValid_AllSortFieldsFoundAsAllowed_True() {
List<String> sortFields = Arrays.asList(ALLOWED_SORT_FIELDS);
AllowedSortFields toValidate = newAllowedSortFields(sortFields);
Set<ConstraintViolation<AllowedSortFields>> constraintViolations = validator.validate(toValidate, Default.class);
assertTrue(constraintViolations.isEmpty());
}
#Test
public void isValid_NullValue_True() {
AllowedSortFields toValidate = new AllowedSortFields();
toValidate.pageable = null;
Set<ConstraintViolation<AllowedSortFields>> constraintViolations = validator.validate(toValidate, Default.class);
assertTrue(constraintViolations.isEmpty());
}
private String getConstraintMessages(Set<ConstraintViolation<AllowedSortFields>> constraintViolations) {
String actual = constraintViolations.stream()
.map(c -> c.getMessage())
.collect(joining(","));
return actual;
}
private AllowedSortFields newAllowedSortFields(List<String> sortFields) {
AllowedSortFields toValidate = new AllowedSortFields();
toValidate.pageable = new CustomPageable().sort(sortFields);
return toValidate;
}
private EmptyAllowedSortFields newEmptyAllowedSortFields(List<String> sortFields) {
EmptyAllowedSortFields toValidate = new EmptyAllowedSortFields();
toValidate.pageable = new CustomPageable().sort(sortFields);
return toValidate;
}
public class AllowedSortFields {
#AllowSortFields({"allowed1", "allowed2"})
public Pageable pageable;
}
public class EmptyAllowedSortFields {
#AllowSortFields
public Pageable pageable;
}
}
Finally the usage within the repository. Be sure to put #Validated at the top of the class.
ExampleSearchRepository.java
public interface ExampleSearchRepository extends JpaRepository<ExampleSearch, Integer>,
JpaSpecificationExecutor<ExampleSearch>, PagingAndSortingRepository<ExampleSearch, Integer> {
public Page<ExampleSearch> search(
#Param("searchCriteria") ExampleSearchCriteria searchCriteria,
#AllowSortFields({"field1","subfield.name"}) Pageable pageable);

sprng boot jpa + redis LazyInitializationException

I use spring boot 2.1.2 and redis as cache provider.
But now, I have a question.
In sysUser entity
#Data
#Entity
#Table(name = "sys_user")
#ToString(exclude = "roles")
#EqualsAndHashCode(callSuper = true)
#Proxy(lazy = false)
public class SysUser extends BaseEntity implements UserDetails {
// ...
/**
* 当前用户的权限
*/
#ManyToMany(fetch = FetchType.EAGER)
#JsonIgnoreProperties(value = "users")
#JoinTable(name = "sys_user_role",
joinColumns = {#JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {#JoinColumn(name = "role_id", nullable = false)})
private List<SysRole> roles;
// ...
}
In sysRole entity
#Data
#Entity
#Table(name = "sys_role")
#EqualsAndHashCode(callSuper = true)
#ToString(exclude = {"users", "permissions"})
#Proxy(lazy = false)
public class SysRole extends BaseEntity {
// ...
/**
* 当前角色的菜单
*/
#JsonIgnoreProperties(value = "roles")
#ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
#JoinTable(name = "sys_permission_role", joinColumns = #JoinColumn(name = "role_id"),
inverseJoinColumns = #JoinColumn(name = "permission_id"))
private List<SysPermission> permissions = new ArrayList<>();
/**
* 当前角色对应的用户
* 双向映射造成数据重复查询死循环问题
*/
#ManyToMany(mappedBy = "roles")
private List<SysUser> users = new ArrayList<>();
}
In SysPermission entitty
#Data
#Entity
#Table(name = "sys_permission")
#EqualsAndHashCode(callSuper = true)
#Proxy(lazy = false)
public class SysPermission extends BaseEntity {
// ...
/**
* 菜单角色
* 双向映射造成数据重复查询死循环问题
*/
#ManyToMany(mappedBy = "permissions")
private List<SysRole> roles = new ArrayList<>();
}
In sysUser service impl
#Override
#Cacheable
public SysUser loadUserByUsername(String username) {
return sysUserRepository.findFirstByUsernameAndEnabledTrue(username).orElseThrow(() ->
new UsernameNotFoundException("用户不存在")
);
}
redis config
#Bean
#Override
public CacheManager cacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(12))
.prefixKeysWith(applicationProperties.getName())
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.disableCachingNullValues();
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration)
.transactionAware()
.build();
}
#Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
Question
When I first called loadUserByUsername,it is ok.And in redis
in json.cn
But when I secound called loadUserByUsername,it is wrong,And get exception
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"])
at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:132)
at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:110)
at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:48)
......
Caused by: com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
......
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:597)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:216)
at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:160)
at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:287)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:302)
......
Other
I try these methods
#JsonIgnore , but it will set roles is null, I want to use this field.
Config jackson registerModule Hibernate5Module, it will set roles is null.
Use #Proxy(lazy = false), no changes.
Use #ManyToMany(fetch = FetchType.EAGER), no changes
config
spring:
jpa:
open-in-view: true
properties
hibernate:
enable_lazy_load_no_trans: true
no changes...
Use another json tools, such as gson and FastJson, but infinite loop for jpa when save cache.
Please help me, I had spent three days...But I do not resolve this question...
Thanks!
github address: XIAOMING
If do not have resolve method, maybe I must use Mybatis. But there is a lot of work.Please help me resolve this question...
1st. create 2 classes below
The HibernateCollectionIdResolver.class will translate HibernateCollection class into JDK collection class, so the Jackson will write json from
{
"paramA": [
"org.hibernate.collection.internal.PersistentSet",
[]
]
}
to
{
"paramA": [
"java.util.HashSet",
[]
]
}
then the method typeFromId will get JDK JavaType from the class full name above, to deserialize your json to POJO.
class HibernateCollectionIdResolver extends TypeIdResolverBase {
public HibernateCollectionIdResolver() {
}
#Override
public String idFromValue(Object value) {
//translate from HibernanteCollection class to JDK collection class
if (value instanceof PersistentArrayHolder) {
return Array.class.getName();
} else if (value instanceof PersistentBag || value instanceof PersistentIdentifierBag || value instanceof PersistentList) {
return List.class.getName();
} else if (value instanceof PersistentSortedMap) {
return TreeMap.class.getName();
} else if (value instanceof PersistentSortedSet) {
return TreeSet.class.getName();
} else if (value instanceof PersistentMap) {
return HashMap.class.getName();
} else if (value instanceof PersistentSet) {
return HashSet.class.getName();
} else {
//default is JDK collection
return value.getClass().getName();
}
}
#Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return idFromValue(value);
}
//deserialize the json annotated JDK collection class name to JavaType
#Override
public JavaType typeFromId(DatabindContext ctx, String id) throws IOException {
try {
return ctx.getConfig().constructType(Class.forName(id));
} catch (ClassNotFoundException e) {
throw new UnsupportedOperationException(e);
}
}
#Override
public JsonTypeInfo.Id getMechanism() {
return JsonTypeInfo.Id.CLASS;
}
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS
)
#JsonTypeIdResolver(value = HibernateCollectionIdResolver.class)
public class HibernateCollectionMixIn {
}
2nd. register this MixIn class to you ObjectMapper
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new JavaTimeModule());
mapper.registerModule(new JodaModule());
mapper.addMixIn(Collection.class, HibernateCollectionMixIn.class);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
last, register your jackson2JsonRedisSerializer to your RedisCacheConfiguration.
This would be helpful, I spent 2 days researching how to solve this problem.
And I found the json type id could be rewrite...
So just override jackson typeIdResolver~
EDIT: solve deserialization issue and add some comments
In your code you return valueSerializer like this
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
But you will have to return the GenericJackson2JsonRedisSerializer with Jackson Object mapper that has Hibernate5Module or Hibernate4Module registered as a module
public ObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
mapper.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// Registering Hibernate5Module to support lazy objects for hibernate 5
// Use Hibernate4Module if using hibernate 4
mapper.registerModule(new Hibernate5Module());
return mapper;
}
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer(getMapper());
}

How does Spring's JPARepository and #Transactional behave together?

I have two methods (in a Spring boot application) that handle an entity. The entity has two fields, both boolean isDefault and isPdfGenerated. The first method (which is called from a controller) changes the isDefault flag when a new entity is created while the second one (called from a #Scheduled annotated method) changes the isPdfGenrated after it generates a pdf file for that entity.
My problem is that sometimes the second method finds entities with the isPdfGenerated flag set to false even though the file has been generated and saved in the database.
Both the methods have the #Transactional annotation and the repository interface for the entity extends JpARepository.
My guess is that the first method loads the entity from the database before the second method does but saves the entity after the second method does its job, thus overriding the isPdfGenerated flag.
Is this possible ? If the answer is yes, how should one handle such cases ? Shouldn't JPARepository handle the case when an entity gets updated from an external source ?
Bellow is some code to better illustrate the situation.
MyController:
#Controller
#RequestMapping("/customers")
public class MyController {
#Autowired
private EntityService entityService;
#RequestMapping(value = "/{id}/changeDefault", method = RequestMethod.POST)
public String changeDefault(#PathVariable("id") Long customerId, #ModelAttribute EntityForm entityForm, Model model) {
Entity newDefaultEntity = entityService.updateDefaultEntity(customerId, entityForm);
if (newDefaultEntity == null)
return "redirect:/customers/" + customerId;
return "redirect:/customers/" + customerId + "/entity/default;
}
}
EntityService:
import org.springframework.transaction.annotation.Transactional;
#Service
public class EntityService {
#Autowired
private EntityRepository entityRepository;
#Autowired
private CustomerRepository customerRepository;
#Transactional
public Entity updateDefaultEntity(Long customerId, submittedData) {
Customer customer = customerRepository.findById(customerId);
if(customer == null)
return customer; // I know there are better ways to do this
Entity currentDefaultEntity = entityRepository.findUniqueByCustomerAndDefaultFlag(customer, true);
if(currentDefaultEntity == null)
return null; // I know there are better ways to do this also
Entity newDefaultEntity = new Entity();
newDefaultEntity.setField1(submittedData.getField1());
newDefaultEntity.setField2(submittedData.getField2());
newDefaultEntity.setCustomer(customer);
oldDefaultEntity.setDefaultFlag(false);
newDefaultEntity.setDefaultFlag(true);
entityRepository.save(newDefaultEntity);
}
#Transactional
public void generatePdfDocument(Entity entity) {
Document pdfDocument = generateDocument(entity);
if(pdfDocument == null)
return;
documentRepository.save(pdfDocument);
entity.setPdfGeneratedFlag(true);
entityRepository.save(entity);
}
}
ScheduledTasks:
#Component
public class ScheduledTasks {
private static final int SECOND_IN_MILLISECONDS = 1000;
private static final int MINUTE_IN_SECONDS = 60;
#Autowired
private EntityRepository entityRepository;
#Autowired
private DocumentService documentService;
#Scheduled(fixedDelay = 20 * SECOND_IN_MILLISECONDS)
#Transactional
public void generateDocuments() {
List<Quotation> quotationList = entityRepository.findByPdfGeneratedFlag(false);
for(Entity entity : entitiesList) {
documentService.generatePdfDocument(entity);
}
}
}
DocumentService:
#Service
public class DocumentService {
#Autowired
private EntityRepository entityRepository;
#Autowired
private DocumentRepository documentRepository;
#Transactional
public void generatePdfDocument(Entity entity) {
Document pdfDocument = generateDocument(entity);
if(pdfDocument == null)
return;
documentRepository.save(pdfDocument);
entity.setPdfGeneratedFlag(true);
entityRepository.save(entity);
}
}
EntityRepository:
#Repository
public interface EntityRepository extends JpaRepository<Entity, Long> {
Entity findById(#Param("id") Long id);
List<Entity> findByPdfGeneratedFlag(#Param("is_pdf_generated") Boolean pdfGeneratedFlag);
Entity findUniqueByCustomerAndDefaultFlag(
#Param("customer") Customer customer,
#Param("defaultFlag") Boolean defaultFlag
);
}
DocumentRepository:
#Repository
public interface DocumentRepository extends JpaRepository<Document, Long> {
Document findById(#Param("id") Long id);
}
Entity:
#Entity
#Table(name = "entities")
#JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
public class Entity {
private Long id;
private boolean defaultFlag;
private boolean pdfGeneratedFlag;
private String field1;
private String field2;
private Customer customer;
public Entity() { }
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "is_default")
public boolean isDefaultFlag() {
return defaultFlag;
}
public void setDefaultFlag(boolean defaultFlag) {
this.defaultFlag = defaultFlag;
}
#Column(name = "is_pdf_generated")
public boolean isPdfGeneratedFlag() {
return pdfGeneratedFlag;
}
public void setPdfGeneratedFlag(boolean pdfGeneratedFlag) {
this.pdfGeneratedFlag = pdfGeneratedFlag;
}
#Column(name = "field_1")
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
#Column(name = "field_2")
public String getField2() {
return field2;
}
public void setField2(String field2) {
this.field2 = field2;
}
#ManyToOne
#JoinColumn(name = "customer_id", referencedColumnName = "id", nullable = false)
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entity quotation = (Entity) o;
return id != null ? id.equals(entity.id) : entity.id == null;
}
#Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
#Override
public String toString() {
return "Entity{" +
"id=" + id +
", pdfGeneratedFlag=" + pdfGeneratedFlag +
", defaultFlag=" + defaultFlag +
", field1=" + field1 +
", field2=" + field2 +
", customer=" + (customer == null ? null : customer.getId()) +
"}";
}
}
I have omitted the other classes because they are either POJOs ( EntityForm ) or the same as other domain model classes ( Document ).
If you're talking about a row on the database that is getting updated by another process after the first process has read it but before it has been updated, then you need to put in some sort of optimistic locking strategy.
This will be handled by the underlying ORM api (e.g. Hibernate or Eclipselink) rather than Spring Data (which will just handle an optimistic locking errors thrown by the ORM).
Have a look at this article. Bear in mind that if you want optimistic locking you need some way of determining a row's version. In JPA this is normally done using a column annotated with the #Version tag.
https://vladmihalcea.com/hibernate-locking-patterns-how-does-optimistic-lock-mode-work/

NotInTransactionException when adding to mapped set

I have two #NodeEntities mapped via SDN using simple mapping, PersonNode and FamilyNode. FamilyNode has a #RelatedTo collection, children. I also have a FamilyService (using Spring's #Service annotation) with #Transactional annotation on an updateFamily method. This method loads the FamilyNode given an id, and uses a callback interface to modify the node. In one implementation of the callback, I am adding a PersonNode to the children collection, and this is generating the NotInTransactionException, specifically at the point that Neo4J is attempting to create the relationship between the FamilyNode and the PersonNode.
Source code can be found at github and in particular a failing test. Here are relevant bits of the code:
FamilyNode.java:
#NodeEntity
public class FamilyNode implements Family {
#Indexed(indexName = "families", unique = true)
private String id;
#GraphId
private Long identifier;
#RelatedTo(elementClass = PersonNode.class, type = "CHILD")
private Set<Person> children;
void addChild(Person child) {
if (this.children == null) {
this.children = new HashSet<>();
}
this.children.add(child);
}
}
PersonNode.java:
#NodeEntity
public class PersonNode implements Person {
#RelatedTo(elementClass = FamilyNode.class, type = "CHILD", direction = INCOMING)
private Family childOf;
#Indexed(indexName = "people", unique = true)
private String id;
#GraphId
private Long identifier;
}
FamilyRepository.java:
public interface FamilyRepository extends GraphRepository<Family> {
public FamilyNode findById(String id);
}
FamilyServiceImpl.java:
#Service
public class FamilyServiceImpl implements FamilyService {
#Autowired
private FamilyRepository families;
#Autowired
private Neo4jTemplate template;
#Override
public List<Family> getFamilies(String[] ids) {
List<Family> families = new ArrayList<>();
for (String id : ids) {
families.add(getFamily(id));
}
return families;
}
#Override
public Family getFamily(String id) {
return familyNode(id);
}
#Override
#Transactional
public Family createFamily(Family family) {
return lazyLoadRelationships((FamilyNode) this.families.save(family));
}
#Override
#Transactional
public Family updateFamily(String id, FamilyNodeUpdater updater) {
return this.families.save(updater.update(familyNode(id)));
}
private FamilyNode familyNode(String id) {
return lazyLoadRelationships(this.families.findById(id));
}
private FamilyNode lazyLoadRelationships(FamilyNode family) {
this.template.fetch(family.getFather());
this.template.fetch(family.getMother());
this.template.fetch(family.getChildren());
return family;
}
}
and the failing test, FamilyServiceTest.java:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = { TestConfig.class })
public class FamilyServiceTest {
#Configuration
#ComponentScan(basePackageClasses = { com.bonevich.ancestral.family.FamilyServiceImpl.class }, resourcePattern = "FamilyServiceImpl.class")
#EnableNeo4jRepositories(basePackageClasses = { com.bonevich.ancestral.family.FamilyNode.class })
static class TestConfig extends Neo4jConfiguration {
#Bean
public GraphDatabaseService graphDatabaseService() {
return new GraphDatabaseFactory().newEmbeddedDatabaseBuilder("/data/neo4j/ancestral-familyservicetest/")
.newGraphDatabase();
}
}
#Autowired
private FamilyService families;
#Autowired
private GraphDatabaseService graphDatabaseService;
#Autowired
private Neo4jTemplate neo4jTemplate;
#Test
public void testUpdateFamily() {
this.families.createFamily(FamilyNode.instance("testFamily"));
Transaction tx = this.graphDatabaseService.beginTx();
PersonNode person = PersonNode.instance("John", "Johanson", "M", "a_person");
PersonNode expectedChild = this.neo4jTemplate.save(person);
final long childId = expectedChild.getIdentifier();
tx.success();
tx.finish();
Family actualFamily = this.families.updateFamily("testFamily", new FamilyNodeUpdater() {
#Override
public FamilyNode update(FamilyNode family) {
family.addChild(FamilyServiceTest.this.neo4jTemplate.findOne(childId, PersonNode.class));
return family;
}
});
assertThat(actualFamily.getId(), is("testFamily"));
assertThat(actualFamily.getChildren().get(0), is((Person) expectedChild));
}
}
Running this test yields the following exception stack:
org.neo4j.graphdb.NotInTransactionException
at org.neo4j.kernel.impl.core.NoTransactionState.acquireWriteLock(NoTransactionState.java:43)
at org.neo4j.kernel.impl.transaction.LockType$2.acquire(LockType.java:51)
at org.neo4j.kernel.impl.core.NodeManager.getNodeForProxy(NodeManager.java:473)
at org.neo4j.kernel.InternalAbstractGraphDatabase$6.lookup(InternalAbstractGraphDatabase.java:733)
at org.neo4j.kernel.impl.core.NodeProxy.createRelationshipTo(NodeProxy.java:207)
at org.springframework.data.neo4j.fieldaccess.RelationshipHelper.obtainSingleRelationship(RelationshipHelper.java:62)
at org.springframework.data.neo4j.fieldaccess.RelationshipHelper.createSingleRelationship(RelationshipHelper.java:142)
at org.springframework.data.neo4j.fieldaccess.RelationshipHelper.createAddedRelationships(RelationshipHelper.java:96)
at org.springframework.data.neo4j.fieldaccess.RelatedToFieldAccessor.createAddedRelationships(RelatedToFieldAccessor.java:78)
at org.springframework.data.neo4j.fieldaccess.RelatedToCollectionFieldAccessorFactory$RelatedToCollectionFieldAccessor.setValue(RelatedToCollectionFieldAccessorFactory.java:68)
at org.springframework.data.neo4j.fieldaccess.ManagedFieldAccessorSet.updateValue(ManagedFieldAccessorSet.java:112)
at org.springframework.data.neo4j.fieldaccess.ManagedFieldAccessorSet.update(ManagedFieldAccessorSet.java:100)
at org.springframework.data.neo4j.fieldaccess.ManagedFieldAccessorSet.add(ManagedFieldAccessorSet.java:126)
at com.bonevich.ancestral.family.FamilyNode.addChild(FamilyNode.java:124)
at com.bonevich.ancestral.family.FamilyServiceTest$1.update(FamilyServiceTest.java:64)
at com.bonevich.ancestral.family.FamilyServiceImpl.updateFamily(FamilyServiceImpl.java:42)
at com.bonevich.ancestral.family.FamilyServiceTest.testUpdateFamily(FamilyServiceTest.java:61)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
I have got to believe I am missing something in configuring SDN or Transactions, but have been unable to track it down.
I recently answered this question here:
Spring Data Neo4J - NotInTransactionException while modifying set of nodeEntity
In short, it has to do with the fact that these List are special list which are backed by SDN and any modification is immediately persisted to the DB. If you want to prevent this behaviour, you should use Iterable in your model classes. If you have some more question after reading the link, just post them as a comment.

Lazy loading of child throwing session error

I'm the following error when calling purchaseService.updatePurchase(purchase) inside my TagController:
SEVERE: Servlet.service() for servlet [PurchaseAPIServer] in context with path [/PurchaseAPIServer] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed] with root cause
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:383)
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:375)
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:368)
at org.hibernate.collection.PersistentSet.add(PersistentSet.java:212)
at com.app.model.Purchase.addTags(Purchase.java:207)
at com.app.controller.TagController.createAll(TagController.java:79)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:225)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:999)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:565)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:309)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
TagController:
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchase.addTags(tags);
purchaseService.updatePurchase(purchase);
}
Purchase:
#Entity
#XmlRootElement
public class Purchase implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 6603477834338392140L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(mappedBy = "purchase", fetch = FetchType.LAZY, cascade={CascadeType.ALL})
private Set<Tag> tags;
#JsonIgnore
public Set<Tag> getTags()
{
if (tags == null)
{
tags = new LinkedHashSet<Tag>();
}
return tags;
}
public void setTags(Set<Tag> tags)
{
this.tags = tags;
}
public void addTag(Tag tag)
{
tag.setPurchase(this);
this.tags.add(tag);
}
public void addTags(Set<Tag> tags)
{
Iterator<Tag> it = tags.iterator();
while (it.hasNext())
{
Tag tag = it.next();
tag.setPurchase(this);
this.tags.add(tag);
}
}
...
}
Tag:
#Entity
#XmlRootElement
public class Tag implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 5165922776051697002L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns({#JoinColumn(name = "PURCHASEID", referencedColumnName = "ID")})
private Purchase purchase;
#JsonIgnore
public Purchase getPurchase()
{
return purchase;
}
public void setPurchase(Purchase purchase)
{
this.purchase = purchase;
}
}
PurchaseService:
#Service
public class PurchaseService implements IPurchaseService
{
#Autowired
private IPurchaseDAO purchaseDAO;
public PurchaseService()
{
}
#Transactional
public List<Purchase> getAll()
{
return purchaseDAO.findAll();
}
#Transactional
public Purchase getById(Long id)
{
return purchaseDAO.findOne(id);
}
#Transactional
public void addPurchase(Purchase purchase)
{
purchaseDAO.save(purchase);
}
#Transactional
public void updatePurchase(Purchase purchase)
{
purchaseDAO.update(purchase);
}
}
TagService:
#Service
public class TagService implements ITagService
{
#Autowired
private ITagDAO tagDAO;
public TagService()
{
}
#Transactional
public List<Tag> getAll()
{
return tagDAO.findAll();
}
#Transactional
public Tag getById(Long id)
{
return tagDAO.findOne(id);
}
#Transactional
public void addTag(Tag tag)
{
tagDAO.save(tag);
}
#Transactional
public void updateTag(Tag tag)
{
tagDAO.update(tag);
}
}
Any ideas on how I can fix this? (I want to avoid using EAGER loading).
Do I need to setup some form of session management for transactions?
Thanks
Updates based on JB suggestions
TagController
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
// Validation
RestPreconditions.checkRequestElementNotNull(purchase);
RestPreconditions.checkRequestElementIsNumeric(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchaseService.addTagsToPurchase(purchaseId, tags);
}
PurchaseService
#Transactional
public void addTagsToPurchase(Long purchaseId, Set<Tag> tags)
{
Purchase p = purchaseDAO.findOne(purchaseId);
p.addTags(tags);
}
You load the Purchase without initializing its tags collection and then, in your controller, once the transaction is committed and the session closed, you're adding a tag to this non-initialized collection. So obviously, this exception is thrown.
You have two possibilities:
Load the tags with the Purchase, either by calling Hibernate.initialize(purchase.getTags()), or by executing a JPQL query that loads the tag along with its tags, in a single query (select distinct p from Purchase p left join fetch p.tags).
Instead of having a service class that offers noting more than the DAO, add a real service method addTagsToPurchase(Long purchaseId, Set<Tag> tags) which reloads the purchase and adds the tags to this purchase. This is in fact the service method needed by your controller.
I of course prefer the second solution by far:
it shows why a service layer is useful,
it removes business code from the presentation layer to put it in the service layer,
it's more efficient
it's safer because it uses attached entities, and thus avoids the kind of exception you're having.
In general, if your controller, to execute a single atomic action, needs several calls to the service layer, it means that the service layer doesn't offer the needed functionality, and that the service is implemented in the presentation layer instead.

Resources