From 783c6a87eca482a86c7f29b3a9531d96234ac886 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Jan 2024 14:00:45 +0100 Subject: [PATCH] Introduce caching and equality checks to derived persistent entities and properties. We now provide equals/hashCode and selective caching for derived properties and entities derived from embedded properties and constructor-annotated properties to avoid memory leaks using properties and entities as cache keys. Closes #1471 --- .../convert/MappingCassandraConverter.java | 18 +---- ...AnnotatedCassandraConstructorProperty.java | 34 ++++------ .../BasicCassandraPersistentEntity.java | 27 ++++++++ .../CassandraConstructorProperty.java | 35 +++++++--- .../mapping/CassandraPersistentEntity.java | 15 +++++ .../mapping/EmbeddedEntityOperations.java | 65 +++++++++++++++---- 6 files changed, 134 insertions(+), 60 deletions(-) rename spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/{convert => mapping}/AnnotatedCassandraConstructorProperty.java (87%) rename spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/{convert => mapping}/CassandraConstructorProperty.java (87%) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java index 9b14b4e54..38aa4e075 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java @@ -27,12 +27,12 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; -import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -1406,8 +1406,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter throw new MappingException(String.format("Parameter %s does not have a name", parameter)); } - CassandraPersistentProperty property = getPersistentProperty(name, parameter.getType(), - parameter.getAnnotations()); + CassandraPersistentProperty property = entity.getProperty(parameter); if (property == null) { @@ -1418,19 +1417,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return (T) getReadValue(context.forProperty(property.getName()), provider, property); } - @Nullable - private CassandraPersistentProperty getPersistentProperty(String name, TypeInformation typeInformation, - MergedAnnotations annotations) { - - CassandraPersistentProperty property = entity.getPersistentProperty(name); - - if (annotations.isPresent(Column.class) || annotations.isPresent(Element.class)) { - return new AnnotatedCassandraConstructorProperty( - property == null ? new CassandraConstructorProperty(name, entity, typeInformation) : property, annotations); - } - - return property; - } } private record PropertyTranslatingPropertyAccessor (PersistentPropertyAccessor delegate, diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/AnnotatedCassandraConstructorProperty.java similarity index 87% rename from spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java rename to spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/AnnotatedCassandraConstructorProperty.java index 2d34fcaca..90be2a9e9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/AnnotatedCassandraConstructorProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/AnnotatedCassandraConstructorProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.core.convert; +package org.springframework.data.cassandra.core.mapping; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; @@ -22,11 +22,9 @@ import java.lang.reflect.Method; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.cassandra.core.cql.Ordering; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; -import org.springframework.data.cassandra.core.mapping.Column; -import org.springframework.data.cassandra.core.mapping.Element; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.util.TypeInformation; @@ -46,41 +44,31 @@ class AnnotatedCassandraConstructorProperty implements CassandraPersistentProper private final CassandraPersistentProperty delegate; - private final MergedAnnotations annotations; + private final MergedAnnotation column; + + private final MergedAnnotation element; public AnnotatedCassandraConstructorProperty(CassandraPersistentProperty delegate, MergedAnnotations annotations) { this.delegate = delegate; - this.annotations = annotations; + this.column = annotations.get(Column.class); + this.element = annotations.get(Element.class); } @Override @Nullable public CqlIdentifier getColumnName() { - - if (annotations.isPresent(Column.class)) { - return CqlIdentifier.fromCql(annotations.get(Column.class).getString("value")); - } - - return delegate.getColumnName(); + return column.isPresent() ? CqlIdentifier.fromCql(column.getString("value")) : delegate.getColumnName(); } @Override public boolean hasExplicitColumnName() { - if (annotations.isPresent(Column.class)) { - return !ObjectUtils.isEmpty(annotations.get(Column.class).getString("value")); - } - return false; + return column.isPresent() && !ObjectUtils.isEmpty(column.getString("value")); } @Override @Nullable public Integer getOrdinal() { - - if (annotations.isPresent(Element.class)) { - return annotations.get(Element.class).getInt("value"); - } - - return delegate.getOrdinal(); + return element.isPresent() ? Integer.valueOf(element.getInt("value")) : delegate.getOrdinal(); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentEntity.java index f3d3da72f..deb55daa5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentEntity.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentEntity.java @@ -17,7 +17,9 @@ package org.springframework.data.cassandra.core.mapping; import java.lang.annotation.Annotation; import java.util.Comparator; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import org.springframework.beans.BeansException; @@ -26,9 +28,11 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -60,6 +64,8 @@ public class BasicCassandraPersistentEntity extends BasicPersistentEntity, CassandraPersistentProperty> constructorProperties = new ConcurrentHashMap<>(); + /** * Create a new {@link BasicCassandraPersistentEntity} given {@link TypeInformation}. * @@ -213,4 +219,25 @@ public class BasicCassandraPersistentEntity extends BasicPersistentEntity parameter) { + + if (parameter.getName() == null) { + return null; + } + + MergedAnnotations annotations = parameter.getAnnotations(); + if (annotations.isPresent(Column.class) || annotations.isPresent(Element.class)) { + + return constructorProperties.computeIfAbsent(parameter, it -> { + + CassandraPersistentProperty property = getPersistentProperty(it.getName()); + return new AnnotatedCassandraConstructorProperty( + property == null ? new CassandraConstructorProperty(it, this) : property, it.getAnnotations()); + }); + } + + return getPersistentProperty(parameter.getName()); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraConstructorProperty.java similarity index 87% rename from spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java rename to spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraConstructorProperty.java index 08c192077..96c3fd79b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraConstructorProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraConstructorProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.core.convert; +package org.springframework.data.cassandra.core.mapping; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; @@ -25,9 +25,8 @@ import java.util.Optional; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.data.cassandra.core.cql.Ordering; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -42,17 +41,20 @@ import com.datastax.oss.driver.api.core.CqlIdentifier; */ class CassandraConstructorProperty implements CassandraPersistentProperty { + private final Parameter constructorParameter; + private final String name; private final CassandraPersistentEntity owner; private final TypeInformation typeInformation; - public CassandraConstructorProperty(String name, CassandraPersistentEntity owner, - TypeInformation typeInformation) { - this.name = name; + public CassandraConstructorProperty(Parameter constructorParameter, + CassandraPersistentEntity owner) { + this.constructorParameter = constructorParameter; + this.name = constructorParameter.getName(); this.owner = owner; - this.typeInformation = typeInformation; + this.typeInformation = constructorParameter.getType(); } @Nullable @@ -296,4 +298,21 @@ class CassandraConstructorProperty implements CassandraPersistentProperty { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { throw new UnsupportedOperationException(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CassandraConstructorProperty that)) { + return false; + } + + return constructorParameter.equals(that.constructorParameter); + } + + @Override + public int hashCode() { + return constructorParameter.hashCode(); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraPersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraPersistentEntity.java index 20284dd85..0139bec32 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraPersistentEntity.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraPersistentEntity.java @@ -15,7 +15,9 @@ */ package org.springframework.data.cassandra.core.mapping; +import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.datastax.oss.driver.api.core.CqlIdentifier; @@ -29,6 +31,19 @@ import com.datastax.oss.driver.api.core.CqlIdentifier; */ public interface CassandraPersistentEntity extends PersistentEntity { + /** + * Retrieve a {@link CassandraPersistentProperty} from a {@link Parameter persistence creator (constructor/factory + * method) parameter}. Parameters are either derived by name or synthesized if their name does not map to a existing + * property. + * + * @param parameter the parameter to create a property from. Parameters without a name return no ({@literal null}) + * parameter. + * @return the property, synthetic property or {@literal null}, if the parameter is unnamed. + * @since 4.1.9 + */ + @Nullable + CassandraPersistentProperty getProperty(Parameter parameter); + /** * Returns whether this entity represents a composite primary key. */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/EmbeddedEntityOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/EmbeddedEntityOperations.java index 4c622b71e..9507f71da 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/EmbeddedEntityOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/EmbeddedEntityOperations.java @@ -29,19 +29,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.data.cassandra.core.cql.Ordering; -import org.springframework.data.mapping.Alias; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.IdentifierAccessor; -import org.springframework.data.mapping.InstanceCreatorMetadata; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PersistentPropertyPathAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.mapping.SimpleAssociationHandler; -import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.*; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -323,6 +311,13 @@ public class EmbeddedEntityOperations { return delegate.requiresPropertyPopulation(); } + @Nullable + @Override + public CassandraPersistentProperty getProperty(Parameter parameter) { + CassandraPersistentProperty property = delegate.getProperty(parameter); + return property == null ? null : wrap(property); + } + @NotNull @Override public Iterator iterator() { @@ -342,6 +337,28 @@ public class EmbeddedEntityOperations { return delegate.spliterator(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PrefixedCassandraPersistentEntity that)) { + return false; + } + + if (!prefix.equals(that.prefix)) { + return false; + } + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + int result = prefix.hashCode(); + result = 31 * result + delegate.hashCode(); + return result; + } + private PrefixedCassandraPersistentProperty wrap(CassandraPersistentProperty source) { return new PrefixedCassandraPersistentProperty(prefix, source); } @@ -653,5 +670,27 @@ public class EmbeddedEntityOperations { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { delegate.setApplicationContext(applicationContext); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PrefixedCassandraPersistentProperty that)) { + return false; + } + + if (!prefix.equals(that.prefix)) { + return false; + } + return delegate.equals(that.delegate); + } + + @Override + public int hashCode() { + int result = prefix.hashCode(); + result = 31 * result + delegate.hashCode(); + return result; + } } }