refactor: Migrate to JSpecify annotations for nullability constraints. (#3016)

Replaces all deprecated Spring annotations with JSpecify annotations, and includes a full refactor so that the parent build profile `-Pnullaway` compiles and works proper.

Closes #2987

Signed-off-by: Michael Simons <michael@simons.ac>
This commit is contained in:
Michael Simons
2025-06-03 06:57:22 +02:00
committed by GitHub
parent a4efefa61f
commit 7fcacb7991
153 changed files with 1461 additions and 1279 deletions

104
pom.xml
View File

@@ -84,7 +84,11 @@
<java-module-name>spring.data.neo4j</java-module-name>
<java.version>17</java.version>
<jaxb.version>2.3.1</jaxb.version>
<jna.version>5.8.0</jna.version>
<jspecify.version>1.0.0</jspecify.version>
<jsr305.version>3.0.2</jsr305.version>
<junit-cc-testcontainer>2021.0.1</junit-cc-testcontainer>
<junit-pioneer.version>2.2.0</junit-pioneer.version>
<maven-install-plugin.version>3.1.4</maven-install-plugin.version>
<maven-site-plugin.version>3.7.1</maven-site-plugin.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
@@ -92,6 +96,7 @@
<neo4j-migrations.version>2.17.3</neo4j-migrations.version>
<neo4j.version>4.4.41</neo4j.version>
<objenesis.version>3.0.1</objenesis.version>
<org.osgi.core.version>6.0.0</org.osgi.core.version>
<project.build.docs>${project.build.directory}/docs</project.build.docs>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<r2dbc.releasetrain>Arabba-SR11</r2dbc.releasetrain>
@@ -102,22 +107,14 @@
<skipUnitTests>${skipTests}</skipUnitTests>
<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
<junit-pioneer.version>2.2.0</junit-pioneer.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl-bom</artifactId>
<version>${cypher-dsl.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<version>${jsr305.version}</version>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
@@ -131,9 +128,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>${junit-pioneer.version}</version>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<version>${blockhound.version}</version>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
@@ -161,18 +158,35 @@
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.8.0</version>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>org.apiguardian</groupId>
<artifactId>apiguardian-api</artifactId>
<version>${apiguardian.version}</version>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>${jspecify.version}</version>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>${junit-pioneer.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>${neo4j.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl-bom</artifactId>
<version>${cypher-dsl.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
@@ -191,7 +205,7 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
<version>6.0.0</version>
<version>${org.osgi.core.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@@ -210,11 +224,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<version>${blockhound.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -279,6 +288,11 @@
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
@@ -341,6 +355,11 @@
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl</artifactId>
@@ -415,28 +434,18 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
</repository>
<repository>
<id>spring-milestone</id>
@@ -643,31 +652,6 @@
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>java-compile</id>
<configuration combine.self="append">
<compilerArgs>
<arg>-Xlint:all,-options,-path</arg>
</compilerArgs>
</configuration>
</execution>
<execution>
<!-- You would think that applying above configuration to java-test-compile would have the same result.
Alas, it doesn't. The build just fails, -e -X doesn't show anything sane.
So, showDeprecation is just warnings, if set to errors, than it fails hard (which is nice for
deprecation, but it also fails during missing When.MAYBE whateever Jakarta annotation
and there's just no motiviation to deal with Spring Data Commons build setup in this project -->
<id>java-test-compile</id>
<configuration combine.self="append">
<showDeprecation>true</showDeprecation>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
@@ -689,8 +673,8 @@
<build>
<resources>
<resource>
<directory>src/main/antora/resources/antora-resources</directory>
<filtering>true</filtering>
<directory>src/main/antora/resources/antora-resources</directory>
</resource>
</resources>
</build>

View File

@@ -15,17 +15,17 @@
*/
package org.springframework.data.neo4j.aot;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.core.ResolvableType;
import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* @author Gerrit Meier
* @since 7.0.0
*/
public class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor {
public final class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor {
public Neo4jManagedTypesBeanRegistrationAotProcessor() {
setModuleIdentifier("neo4j");
@@ -36,7 +36,7 @@ public class Neo4jManagedTypesBeanRegistrationAotProcessor extends ManagedTypesB
return isNeo4jManagedTypes(beanType) || super.isMatch(beanType, beanName);
}
protected boolean isNeo4jManagedTypes(@Nullable Class<?> beanType) {
boolean isNeo4jManagedTypes(@Nullable Class<?> beanType) {
return beanType != null && ClassUtils.isAssignable(Neo4jManagedTypes.class, beanType);
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.neo4j.aot;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -34,7 +35,6 @@ import org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository;
import org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository;
import org.springframework.data.querydsl.QuerydslUtils;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.lang.Nullable;
import java.util.Arrays;

View File

@@ -0,0 +1,4 @@
@NullMarked
package org.springframework.data.neo4j.aot;
import org.jspecify.annotations.NullMarked;

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.config;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,7 +31,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
/**
@@ -65,11 +65,11 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
* @return A imperative Neo4j client.
*/
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME)
public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) {
public Neo4jClient neo4jClient(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(getUserSelectionProvider())
.withUserSelectionProvider(this.userSelectionProviders.getIfUnique())
.withNeo4jBookmarkManager(getBootBookmarkManager())
.build();
}
@@ -78,11 +78,6 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
return this.bookmarkManagerProviders.getIfAvailable(Neo4jBookmarkManager::create);
}
@Nullable
private UserSelectionProvider getUserSelectionProvider() {
return this.userSelectionProviders == null ? null : this.userSelectionProviders.getIfUnique();
}
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public Neo4jOperations neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext) {
@@ -97,12 +92,12 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
* @return A platform transaction manager
*/
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseSelectionProvider) {
public PlatformTransactionManager transactionManager(Driver driver, @Nullable DatabaseSelectionProvider databaseSelectionProvider) {
return Neo4jTransactionManager
.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withUserSelectionProvider(getUserSelectionProvider())
.withUserSelectionProvider(this.userSelectionProviders.getIfUnique())
.withBookmarkManager(getBootBookmarkManager())
.build();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.config;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,7 +30,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;

View File

@@ -90,18 +90,4 @@ abstract class Neo4jConfigurationSupport {
return Neo4jEntityScanner.get().scan(getMappingBasePackages());
}
/**
* Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}.
*
* @param basePackage must not be {@literal null}.
* @return found entities in the package to scan.
* @throws ClassNotFoundException if the given class cannot be loaded by the class loader.
* @deprecated since 6.0.2 Use {@link Neo4jEntityScanner} instead.
*/
@Deprecated
protected final Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
return Neo4jEntityScanner.get().scan(basePackage);
}
}

View File

@@ -23,6 +23,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@@ -31,7 +32,6 @@ import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -55,7 +55,8 @@ public final class Neo4jEntityScanner {
return new Neo4jEntityScanner(resourceLoader);
}
private @Nullable final ResourceLoader resourceLoader;
@Nullable
private final ResourceLoader resourceLoader;
/**
* Create a new {@link Neo4jEntityScanner} instance.
@@ -121,8 +122,7 @@ public final class Neo4jEntityScanner {
* @param resourceLoader an optional {@link ResourceLoader} to use
* @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan for Neo4j entities
*/
private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(
@Nullable ResourceLoader resourceLoader) {
private static ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider(@Nullable ResourceLoader resourceLoader) {
ClassPathScanningCandidateComponentProvider delegate = new ClassPathScanningCandidateComponentProvider(false);
if (resourceLoader != null) {

View File

@@ -5,7 +5,7 @@
The package provides some additional annotations that enable auditing.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.config;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -18,7 +18,7 @@ package org.springframework.data.neo4j.core;
import java.util.Objects;
import org.apiguardian.api.API;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
/**
* A value holder indicating a database selection based on an optional name. {@literal null} indicates to let the server
@@ -33,7 +33,8 @@ public final class DatabaseSelection {
private static final DatabaseSelection DEFAULT_DATABASE_NAME = new DatabaseSelection(null);
@Nullable private final String value;
@Nullable
private final String value;
public static DatabaseSelection undecided() {

View File

@@ -26,6 +26,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Query;
@@ -49,7 +50,6 @@ import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -64,8 +64,10 @@ import org.springframework.util.StringUtils;
final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
private final Driver driver;
private @Nullable final DatabaseSelectionProvider databaseSelectionProvider;
private @Nullable final UserSelectionProvider userSelectionProvider;
@Nullable
private final DatabaseSelectionProvider databaseSelectionProvider;
@Nullable
private final UserSelectionProvider userSelectionProvider;
private final ConversionService conversionService;
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
@@ -276,7 +278,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
}
@Override
public <T> OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return new DefaultOngoingBindSpec<>(value);
}
@@ -314,7 +316,8 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
class DefaultOngoingBindSpec<T> implements OngoingBindSpec<T, RunnableSpec> {
@Nullable private final T value;
@Nullable
private final T value;
DefaultOngoingBindSpec(@Nullable T value) {
this.value = value;
@@ -360,7 +363,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
}
@Override
public <T> OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return DefaultRunnableSpec.this.bind(value);
}
@@ -395,7 +398,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
}
@Override
public <T> OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return DefaultRunnableSpec.this.bind(value);
}
@@ -410,7 +413,6 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
private final DatabaseSelection databaseSelection;
@Nullable
private final UserSelection impersonatedUser;
private final RunnableStatement runnableStatement;
@@ -418,7 +420,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
private BiFunction<TypeSystem, Record, T> mappingFunction;
DefaultRecordFetchSpec(DatabaseSelection databaseSelection,
@Nullable UserSelection impersonatedUser,
UserSelection impersonatedUser,
RunnableStatement runnableStatement,
BiFunction<TypeSystem, Record, T> mappingFunction) {
@@ -501,7 +503,6 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
private DatabaseSelection databaseSelection;
@Nullable
private UserSelection impersonatedUser;
private final Function<QueryRunner, Optional<T>> callback;
@@ -513,7 +514,7 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
}
@Override
public RunnableDelegation<T> in(@Nullable String targetDatabase) {
public RunnableDelegation<T> in(String targetDatabase) {
this.databaseSelection = resolveTargetDatabaseName(targetDatabase);
return this;

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.neo4j.core;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Query;
@@ -38,7 +39,6 @@ import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -66,8 +66,10 @@ import java.util.function.Supplier;
final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, ApplicationContextAware {
private final Driver driver;
private @Nullable final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
private @Nullable final ReactiveUserSelectionProvider userSelectionProvider;
@Nullable
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
@Nullable
private final ReactiveUserSelectionProvider userSelectionProvider;
private final ConversionService conversionService;
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
@@ -241,7 +243,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
}
@Override
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return new DefaultOngoingBindSpec<>(value);
}
@@ -267,12 +269,13 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
@Override
public Mono<ResultSummary> run() {
return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, this.parameters, null).run();
return new DefaultRecordFetchSpec<>(databaseSelection, userSelection, cypherSupplier, this.parameters, (t, r) -> null).run();
}
class DefaultOngoingBindSpec<T> implements Neo4jClient.OngoingBindSpec<T, RunnableSpec> {
@Nullable private final T value;
@Nullable
private final T value;
DefaultOngoingBindSpec(@Nullable T value) {
this.value = value;
@@ -318,7 +321,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
}
@Override
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return DefaultRunnableSpec.this.bind(value);
}
@@ -353,7 +356,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
}
@Override
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(T value) {
public <T> Neo4jClient.OngoingBindSpec<T, RunnableSpec> bind(@Nullable T value) {
return DefaultRunnableSpec.this.bind(value);
}
@@ -376,8 +379,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
private BiFunction<TypeSystem, Record, T> mappingFunction;
DefaultRecordFetchSpec(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection, Supplier<String> cypherSupplier, NamedParameters parameters,
@Nullable BiFunction<TypeSystem, Record, T> mappingFunction) {
DefaultRecordFetchSpec(Mono<DatabaseSelection> databaseSelection, Mono<UserSelection> userSelection, Supplier<String> cypherSupplier, NamedParameters parameters, BiFunction<TypeSystem, Record, T> mappingFunction) {
this.databaseSelection = databaseSelection;
this.userSelection = userSelection;
@@ -477,7 +479,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
}
@Override
public RunnableDelegation<T> in(@Nullable @SuppressWarnings("HiddenField") String targetDatabase) {
public RunnableDelegation<T> in(@SuppressWarnings("HiddenField") String targetDatabase) {
this.databaseSelection = resolveTargetDatabaseName(targetDatabase);
return this;

View File

@@ -21,12 +21,12 @@ import java.util.Collections;
import java.util.List;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.springframework.lang.Nullable;
/**
* Decorator for an ongoing update statement that removes obsolete dynamic labels and adds new ones.
@@ -42,9 +42,9 @@ final class DynamicLabels implements UnaryOperator<OngoingMatchAndUpdate> {
private final List<String> oldLabels;
private final List<String> newLabels;
DynamicLabels(@Nullable NodeDescription<?> nodeDescription, Collection<String> oldLabels, Collection<String> newLabels) {
DynamicLabels(@Nullable NodeDescription<?> nodeDescription, Collection<String> oldLabels, @Nullable Collection<String> newLabels) {
this.oldLabels = new ArrayList<>(oldLabels);
this.newLabels = new ArrayList<>(newLabels);
this.newLabels = (newLabels != null) ? new ArrayList<>(newLabels) : List.of();
this.rootNode = Cypher.anyNode(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription));
}

View File

@@ -21,9 +21,9 @@ import java.util.Map;
import java.util.Optional;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.lang.Nullable;
/**
* {@link FluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style.
@@ -105,7 +105,7 @@ public interface FluentFindOperation {
* @return new instance of {@link TerminatingFind}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingFind<T> matching(String query, @Nullable Map<String, Object> parameter);
TerminatingFind<T> matching(String query, Map<String, Object> parameter);
/**
* Creates an executable query based on fragments and parameters. Hardly useful outside framework-code
@@ -136,7 +136,7 @@ public interface FluentFindOperation {
* @return new instance of {@link TerminatingFind}.
* @throws IllegalArgumentException if statement is {@literal null}.
*/
TerminatingFind<T> matching(Statement statement, @Nullable Map<String, Object> parameter);
TerminatingFind<T> matching(Statement statement, Map<String, Object> parameter);
/**
* Set the filter {@link Statement statement} to be used.

View File

@@ -19,6 +19,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.util.Assert;
@@ -51,12 +52,15 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
private final Neo4jTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
@Nullable
private final String query;
@Nullable
private final Map<String, Object> parameters;
@Nullable
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> returnType, String query,
Map<String, Object> parameters) {
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String query,
@Nullable Map<String, Object> parameters) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
@@ -65,7 +69,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
this.queryFragmentsAndParameters = null;
}
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) {
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
@@ -107,6 +111,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
}
@Override
@Nullable
public T oneValue() {
List<T> result = doFind(TemplateSupport.FetchType.ONE);
@@ -149,7 +154,7 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
List<T> result = doSave(Collections.singleton(instance));
if (result.isEmpty()) {
return null;
throw new IllegalStateException("Instance was not saved");
}
return result.get(0);
}

View File

@@ -24,11 +24,11 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.driver.Value;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.MapValueWrapper;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
@@ -141,7 +141,7 @@ final class NamedParameters {
}
@Nullable
private static String formatValue(@Nullable Object value) {
private static String formatValue(Object value) {
if (value == null) {
return null;
} else if (value instanceof String) {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.neo4j.core;
import java.io.Serial;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
@@ -25,6 +26,7 @@ import java.util.function.Supplier;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Driver;
import org.neo4j.driver.QueryRunner;
import org.neo4j.driver.Record;
@@ -33,7 +35,6 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.log.LogAccessor;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.lang.Nullable;
/**
* Definition of a modern Neo4j client.
@@ -127,7 +128,7 @@ public interface Neo4jClient {
* @return The builder
* @since 6.3.3
*/
public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) {
public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) {
this.neo4jConversions = neo4jConversions;
return this;
}
@@ -141,7 +142,7 @@ public interface Neo4jClient {
* @return The builder
* @since 7.1.2
*/
public Builder withNeo4jBookmarkManager(Neo4jBookmarkManager bookmarkManager) {
public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) {
this.bookmarkManager = bookmarkManager;
return this;
}
@@ -266,7 +267,7 @@ public interface Neo4jClient {
* @param targetDatabase selected database to use. A {@literal null} value indicates the default database.
* @return A runnable query specification that is now bound to a given database.
*/
RunnableSpecBoundToDatabase in(@Nullable String targetDatabase);
RunnableSpecBoundToDatabase in(String targetDatabase);
/**
* Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning
@@ -275,7 +276,7 @@ public interface Neo4jClient {
* @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user.
* @return A runnable query specification that is now bound to a given database.
*/
RunnableSpecBoundToUser asUser(@Nullable String asUser);
RunnableSpecBoundToUser asUser(String asUser);
}
/**
@@ -408,7 +409,7 @@ public interface Neo4jClient {
* @param targetDatabase selected database to use. A {@literal null} value indicates the default database.
* @return An ongoing delegation
*/
RunnableDelegation<T> in(@Nullable String targetDatabase);
RunnableDelegation<T> in(String targetDatabase);
}
/**
@@ -451,6 +452,9 @@ public interface Neo4jClient {
@API(status = API.Status.STABLE, since = "6.1.5")
class IllegalDatabaseNameException extends IllegalArgumentException {
@Serial
private static final long serialVersionUID = 3496326026855204643L;
private final String illegalDatabaseName;
private IllegalDatabaseNameException(String illegalDatabaseName) {

View File

@@ -21,13 +21,13 @@ import java.util.Optional;
import java.util.function.BiPredicate;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.NoResultException;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.lang.Nullable;
/**
* Specifies operations one can perform on a database, based on an <em>Domain Type</em>.
@@ -207,6 +207,7 @@ public interface Neo4jOperations {
* @return the saved instance.
* @since 6.3
*/
@Nullable
default <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
throw new UnsupportedOperationException();
}
@@ -220,6 +221,7 @@ public interface Neo4jOperations {
* @return the saved, projected instance.
* @since 6.1
*/
@Nullable
default <T, R> R saveAs(T instance, Class<R> resultType) {
throw new UnsupportedOperationException();
}

View File

@@ -23,6 +23,7 @@ import java.util.function.BiFunction;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.exceptions.AuthenticationException;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.DatabaseException;
@@ -61,6 +62,7 @@ public final class Neo4jPersistenceExceptionTranslator implements PersistenceExc
private static final Map<String, Optional<BiFunction<String, Throwable, DataAccessException>>> ERROR_CODE_MAPPINGS;
@Override
@Nullable
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
if (ex instanceof DataAccessException) {

View File

@@ -40,6 +40,8 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.FunctionInvocation;
@@ -49,7 +51,6 @@ import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.exceptions.NoSuchRecordException;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.types.Entity;
@@ -96,11 +97,11 @@ import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParamete
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
@@ -135,18 +136,22 @@ public final class Neo4jTemplate implements
private final CypherGenerator cypherGenerator;
@Nullable
private ClassLoader beanClassLoader;
private EventSupport eventSupport;
@Nullable
private ProjectionFactory projectionFactory;
private Renderer renderer;
private Function<Named, FunctionInvocation> elementIdOrIdFunction;
@Nullable
private TransactionTemplate transactionTemplate;
@Nullable
private TransactionTemplate transactionTemplateReadOnly;
public Neo4jTemplate(Neo4jClient neo4jClient) {
@@ -185,6 +190,18 @@ public final class Neo4jTemplate implements
return Objects.requireNonNull(this.projectionFactory, "Projection support for the Neo4j template is only available when the template is a proper and fully initialized Spring bean.");
}
private <T> T execute(TransactionCallback<T> action) throws TransactionException {
return Objects.requireNonNull(Objects.requireNonNull(this.transactionTemplate).execute(action));
}
private <T> T executeReadOnly(TransactionCallback<T> action) throws TransactionException {
return Objects.requireNonNull(Objects.requireNonNull(this.transactionTemplateReadOnly).execute(action));
}
private void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException {
Objects.requireNonNull(this.transactionTemplate).executeWithoutResult(action);
}
@Override
public long count(Class<?> domainType) {
@@ -213,7 +230,7 @@ public final class Neo4jTemplate implements
@Override
public long count(String cypherQuery, Map<String, Object> parameters) {
return transactionTemplateReadOnly.execute(tx -> {
return executeReadOnly(tx -> {
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery)
.withParameters(parameters).build();
return toExecutableQuery(preparedQuery, true).getRequiredSingleResult();
@@ -227,8 +244,7 @@ public final class Neo4jTemplate implements
}
private <T> List<T> doFindAll(Class<T> domainType, @Nullable Class<?> resultType) {
return transactionTemplateReadOnly
.execute(tx -> {
return executeReadOnly(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
return createExecutableQuery(
domainType, resultType, QueryFragmentsAndParameters.forFindAll(entityMetaData), true)
@@ -238,61 +254,54 @@ public final class Neo4jTemplate implements
@Override
public <T> List<T> findAll(Statement statement, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, statement, true).getResults());
return executeReadOnly(tx -> createExecutableQuery(domainType, statement, true).getResults());
}
@Override
public <T> List<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getResults());
return executeReadOnly(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getResults());
}
@Override
public <T> Optional<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult());
return executeReadOnly(tx -> createExecutableQuery(domainType, null, statement, parameters, true).getSingleResult());
}
@Override
public <T> List<T> findAll(String cypherQuery, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, cypherQuery, true).getResults());
return executeReadOnly(tx -> createExecutableQuery(domainType, cypherQuery, true).getResults());
}
@Override
public <T> List<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults());
return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getResults());
}
@Override
public <T> Optional<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult());
return executeReadOnly(tx -> createExecutableQuery(domainType, null, cypherQuery, parameters, true).getSingleResult());
}
@Override
public <T> ExecutableFind<T> find(Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> new FluentOperationSupport(this).find(domainType));
return new FluentOperationSupport(this).find(domainType);
}
@SuppressWarnings("unchecked")
<T, R> List<R> doFind(@Nullable String cypherQuery, @Nullable Map<String, Object> parameters, Class<T> domainType, Class<R> resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
return transactionTemplateReadOnly.execute(tx -> {
List<T> intermediaResults = Collections.emptyList();
return executeReadOnly(tx -> {
List<T> intermediaResults;
if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) {
intermediaResults = doFindAll(domainType, resultType);
} else {
ExecutableQuery<T> executableQuery;
if (queryFragmentsAndParameters == null) {
if (queryFragmentsAndParameters == null && cypherQuery != null) {
executableQuery = createExecutableQuery(domainType, resultType, cypherQuery,
parameters == null ? Collections.emptyMap() : parameters,
true);
} else {
executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters, true);
executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(queryFragmentsAndParameters), true);
}
intermediaResults = switch (fetchType) {
case ALL -> executableQuery.getResults();
@@ -327,7 +336,7 @@ public final class Neo4jTemplate implements
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters
.forExistsById(entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), id));
.forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id));
Statement statement = fragmentsAndParameters.getQueryFragments().toStatement();
Map<String, Object> parameters = fragmentsAndParameters.getParameters();
@@ -337,13 +346,12 @@ public final class Neo4jTemplate implements
@Override
public <T> Optional<T> findById(Object id, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> {
return executeReadOnly(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
return createExecutableQuery(domainType, null,
QueryFragmentsAndParameters.forFindById(entityMetaData,
convertIdValues(entityMetaData.getRequiredIdProperty(), id)),
TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)),
true)
.getSingleResult();
});
@@ -351,62 +359,44 @@ public final class Neo4jTemplate implements
@Override
public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return transactionTemplateReadOnly
.execute(tx -> {
return executeReadOnly(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
return createExecutableQuery(domainType, null,
QueryFragmentsAndParameters.forFindByAllId(
entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), ids)),
entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)),
true)
.getResults();
});
}
private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) {
if (idProperty != null && ((Neo4jPersistentEntity<?>) idProperty.getOwner()).isUsingInternalIds()) {
return idValues;
}
if (idValues != null) {
return neo4jMappingContext.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter());
} else if (idProperty != null) {
return neo4jMappingContext.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter());
} else {
// Not much we can convert here
return Values.NULL;
}
}
@Override
public <T> T save(T instance) {
return transactionTemplate
.execute(tx -> saveImpl(instance, Collections.emptySet(), null));
return execute(tx -> saveImpl(instance, Collections.emptySet(), null));
}
@Override
@Nullable
public <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
if (instance == null) {
return null;
}
return transactionTemplate
.execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null));
return execute(tx -> saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null));
}
@Override
@Nullable
public <T, R> R saveAs(T instance, Class<R> resultType) {
return transactionTemplate.execute(tx -> {
Assert.notNull(resultType, "ResultType must not be null");
if (instance == null) {
return null;
}
Assert.notNull(resultType, "ResultType must not be null");
if (instance == null) {
return null;
}
return execute(tx -> {
if (resultType.equals(instance.getClass())) {
return resultType.cast(save(instance));
@@ -427,14 +417,14 @@ public final class Neo4jTemplate implements
}
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass());
Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty();
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance);
return localProjectionFactory.createProjection(resultType,
this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass()).get());
this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass()).orElseThrow());
});
}
private <T> T saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
private <T> T saveImpl(T instance, Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
return instance;
@@ -471,8 +461,10 @@ public final class Neo4jTemplate implements
if (!entityMetaData.isUsingDeprecatedInternalId() && TemplateSupport.rendererRendersElementId(renderer)) {
return IdentitySupport.getElementId(node);
}
return node.id();
}).get();
@SuppressWarnings("deprecation")
var id = node.id();
return id;
}).orElseThrow();
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, newOrUpdatedNode);
@@ -498,7 +490,7 @@ public final class Neo4jTemplate implements
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
Neo4jClient.RunnableSpec runnableQuery = neo4jClient
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty)))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty)))
.to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels())
.to(Constants.NAME_OF_STATIC_LABELS_PARAM);
@@ -516,8 +508,7 @@ public final class Neo4jTemplate implements
@Override
public <T> List<T> saveAll(Iterable<T> instances) {
return transactionTemplate
.execute(tx -> saveAllImpl(instances, Collections.emptySet(), null));
return execute(tx -> saveAllImpl(instances, Collections.emptySet(), null));
}
private boolean requiresSingleStatements(boolean heterogeneousCollection, Neo4jPersistentEntity<?> entityMetaData) {
@@ -544,7 +535,7 @@ public final class Neo4jTemplate implements
Class<?> domainClass = types.iterator().next();
Collection<PropertyFilter.ProjectedPath> pps = includeProperty == null ?
includedProperties :
Objects.requireNonNullElseGet(includedProperties, List::of) :
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
includeProperty);
@@ -593,8 +584,8 @@ public final class Neo4jTemplate implements
return entitiesToBeSaved.stream().map(t -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance);
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
String internalId = idToInternalIdMapping.get(id);
Object id = TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty));
String internalId = Objects.requireNonNull(idToInternalIdMapping.get(id));
stateMachine.registerInitialObject(t.originalInstance, internalId);
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, stateMachine, TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
}).collect(Collectors.toList());
@@ -603,17 +594,15 @@ public final class Neo4jTemplate implements
@Override
public <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
return transactionTemplate
.execute(tx -> saveAllImpl(instances, null, includeProperty));
return execute(tx -> saveAllImpl(instances, null, includeProperty));
}
@Override
public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
return transactionTemplate
.execute(tx -> {
Assert.notNull(resultType, "ResultType must not be null");
Assert.notNull(resultType, "ResultType must not be null");
return execute(tx -> {
Class<?> commonElementType = TemplateSupport.findCommonElementType(instances);
@@ -645,7 +634,7 @@ public final class Neo4jTemplate implements
}
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType);
Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty();
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
List<Object> ids = savedInstances.stream().map(savedInstance -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance);
@@ -661,8 +650,7 @@ public final class Neo4jTemplate implements
@Override
public <T> void deleteById(Object id, Class<T> domainType) {
transactionTemplate
.executeWithoutResult(tx -> {
executeWithoutResult(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
String nameOfParameter = "id";
@@ -672,7 +660,7 @@ public final class Neo4jTemplate implements
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id))
.to(nameOfParameter).run();
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
@@ -682,38 +670,37 @@ public final class Neo4jTemplate implements
@Override
public <T> void deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty,
Object versionValue) {
@Nullable Object versionValue) {
transactionTemplate
.executeWithoutResult(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
executeWithoutResult(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
String nameOfParameter = "id";
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter))
.and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName())
.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))
.or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull()));
String nameOfParameter = "id";
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter))
.and(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName())
.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))
.or(Cypher.property(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData), versionProperty.getPropertyName()).isNull()));
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition)
.returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build();
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData, condition)
.returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build();
Map<String, Object> parameters = new HashMap<>();
parameters.put(nameOfParameter, convertIdValues(entityMetaData.getRequiredIdProperty(), id));
parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue);
Map<String, Object> parameters = new HashMap<>();
parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id));
parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue);
createExecutableQuery(domainType, null, statement, parameters, false).getSingleResult().orElseThrow(
() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE)
);
var lockedEntity = createExecutableQuery(domainType, null, statement, parameters, false).getSingleResult();
if (lockedEntity.isEmpty()) {
throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE);
}
deleteById(id, domainType);
});
deleteById(id, domainType);
});
}
@Override
public <T> void deleteAllById(Iterable<?> ids, Class<T> domainType) {
transactionTemplate
.executeWithoutResult(tx -> {
executeWithoutResult(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
String nameOfParameter = "ids";
@@ -723,7 +710,7 @@ public final class Neo4jTemplate implements
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids))
.to(nameOfParameter).run();
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
@@ -734,8 +721,7 @@ public final class Neo4jTemplate implements
@Override
public void deleteAll(Class<?> domainType) {
transactionTemplate
.executeWithoutResult(tx -> {
executeWithoutResult(tx -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
log.debug(() -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel()));
@@ -762,8 +748,9 @@ public final class Neo4jTemplate implements
}
private <T> ExecutableQuery<T> createExecutableQuery(
Class<T> domainType, @Nullable Class<?> resultType,
@Nullable String cypherStatement,
Class<T> domainType,
@Nullable Class<?> resultType,
String cypherStatement,
Map<String, Object> parameters,
boolean readOnly) {
@@ -802,6 +789,7 @@ public final class Neo4jTemplate implements
stateMachine, includeProperty, startingPropertyPath);
}
@SuppressWarnings("deprecation")
private <T> T processNestedRelations(
Neo4jPersistentEntity<?> sourceEntity,
PersistentPropertyAccessor<?> propertyAccessor,
@@ -837,7 +825,7 @@ public final class Neo4jTemplate implements
idProperty = null;
} else {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
idProperty = relationshipPropertiesEntity.getIdProperty();
idProperty = (relationshipPropertiesEntity == null) ? null : relationshipPropertiesEntity.getIdProperty();
}
// break recursive procession and deletion of previously created relationships
@@ -859,7 +847,11 @@ public final class Neo4jTemplate implements
continue;
}
Object id = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty);
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore);
if (relationshipPropertiesPropertyAccessor == null) {
continue;
}
Object id = relationshipPropertiesPropertyAccessor.getProperty(idProperty);
if (id != null) {
knownRelationshipsIds.add(id);
}
@@ -869,7 +861,7 @@ public final class Neo4jTemplate implements
Statement relationshipRemoveQuery = cypherGenerator.prepareDeleteOf(sourceEntity, relationshipDescription, canUseElementId);
neo4jClient.query(renderer.render(relationshipRemoveQuery))
.bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(knownRelationshipsIds) //
.to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) //
@@ -935,13 +927,17 @@ public final class Neo4jTemplate implements
stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId,
relationshipDescription.getRelationshipObverse());
Object idValue = idProperty != null
? relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty)
: null;
Object idValue;
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore);
if (idProperty == null || relationshipPropertiesPropertyAccessor == null) {
idValue = null;
} else {
idValue = relationshipPropertiesPropertyAccessor.getProperty(idProperty);
}
Map<String, Object> properties = new HashMap<>();
properties.put(Constants.FROM_ID_PARAMETER_NAME, convertIdValues(sourceEntity.getRequiredIdProperty(), fromId));
properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId));
properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId);
properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue);
boolean isNewRelationship = idValue == null;
@@ -955,7 +951,7 @@ public final class Neo4jTemplate implements
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
Optional<Object> relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
@@ -975,7 +971,7 @@ public final class Neo4jTemplate implements
List<Object> row = Collections.singletonList(properties);
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
@@ -984,7 +980,7 @@ public final class Neo4jTemplate implements
.bindAll(statementHolder.getProperties())
.run();
}
} else if (relationshipDescription.hasRelationshipProperties()) {
} else if (relationshipDescription.hasRelationshipProperties() && fromId != null) {
// check if bidi mapped already
var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore);
var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(propertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship());
@@ -1034,7 +1030,7 @@ public final class Neo4jTemplate implements
.bindAll(statementHolder.getProperties())
.run();
}
if (!newRelationshipPropertiesToStore.isEmpty()) {
if (!(newRelationshipPropertiesToStore.isEmpty() || idProperty == null)) {
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true,
sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId);
List<Object> all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement()))
@@ -1061,10 +1057,10 @@ public final class Neo4jTemplate implements
return finalSubgraphRoot;
}
private Optional<Object> getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) {
private Optional<Object> getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) {
return neo4jClient.query(renderer.render(statement))
.bind(convertIdValues(idProperty, fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(toId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
@@ -1076,10 +1072,10 @@ public final class Neo4jTemplate implements
// The pendant to {@link #saveRelatedNode(Object, NodeDescription, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
// We can't do without a query, as we need to refresh the internal id
private Entity loadRelatedNode(NodeDescription<?> targetNodeDescription, Object relatedInternalId) {
private Entity loadRelatedNode(NodeDescription<?> targetNodeDescription, @Nullable Object relatedInternalId) {
var targetPersistentEntity = (Neo4jPersistentEntity<?>) targetNodeDescription;
var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue();
return neo4jClient
@@ -1097,9 +1093,12 @@ public final class Neo4jTemplate implements
Neo4jPersistentProperty idProperty,
Object relationshipInternalId
) {
relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.setProperty(idProperty, relationshipInternalId);
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore);
if (relationshipPropertiesPropertyAccessor != null) {
relationshipPropertiesPropertyAccessor
.setProperty(idProperty, relationshipInternalId);
}
}
private Entity saveRelatedNode(Object entity, NodeDescription<?> targetNodeDescription, PropertyFilter includeProperty, PropertyFilter.RelaxedPropertyPath currentPropertyPath) {
@@ -1113,10 +1112,10 @@ public final class Neo4jTemplate implements
binderFunction = binderFunction.andThen(tree -> {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
String idPropertyName = targetPersistentEntity.getIdProperty().getPropertyName();
String idPropertyName = targetPersistentEntity.getRequiredIdProperty().getPropertyName();
IdDescription idDescription = targetPersistentEntity.getIdDescription();
boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId();
if (!includeProperty.isNotFiltering()) {
boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId());
if (properties != null && !includeProperty.isNotFiltering()) {
properties.entrySet()
.removeIf(e -> {
// we cannot skip the id property if it is an assigned id
@@ -1143,6 +1142,7 @@ public final class Neo4jTemplate implements
@Override
public void setBeanClassLoader(ClassLoader beanClassLoader) {
//noinspection ConstantValue
this.beanClassLoader = beanClassLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : beanClassLoader;
}
@@ -1152,7 +1152,7 @@ public final class Neo4jTemplate implements
this.eventSupport = EventSupport.discoverCallbacks(neo4jMappingContext, beanFactory);
SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory();
spelAwareProxyProjectionFactory.setBeanClassLoader(beanClassLoader);
spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader));
spelAwareProxyProjectionFactory.setBeanFactory(beanFactory);
this.projectionFactory = spelAwareProxyProjectionFactory;
@@ -1166,6 +1166,7 @@ public final class Neo4jTemplate implements
if (this.transactionTemplate != null && this.transactionTemplateReadOnly != null) {
return;
}
PlatformTransactionManager transactionManager = null;
var it = beanFactory.getBeanProvider(PlatformTransactionManager.class).stream().iterator();
while (it.hasNext()) {
@@ -1203,7 +1204,8 @@ public final class Neo4jTemplate implements
private <T> ExecutableQuery<T> createExecutableQuery(
Class<T> domainType, @Nullable Class<?> resultType,
Class<T> domainType,
@Nullable Class<?> resultType,
QueryFragmentsAndParameters queryFragmentsAndParameters,
boolean readOnlyTransaction) {
@@ -1232,15 +1234,13 @@ public final class Neo4jTemplate implements
}
<T, R> List<R> doSave(Iterable<R> instances, Class<T> domainType) {
return transactionTemplate
.execute(tx -> {
// empty check
if (!instances.iterator().hasNext()) {
return Collections.emptyList();
}
if (!instances.iterator().hasNext()) {
return Collections.emptyList();
}
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
Class<?> resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType));
return execute(tx -> {
Collection<PropertyFilter.ProjectedPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
getProjectionFactory(), neo4jMappingContext);
@@ -1249,7 +1249,9 @@ public final class Neo4jTemplate implements
EntityFromDtoInstantiatingConverter<T> converter = new EntityFromDtoInstantiatingConverter<>(domainType, neo4jMappingContext);
for (R instance : instances) {
T domainObject = converter.convert(instance);
if (domainObject == null) {
continue;
}
T savedEntity = saveImpl(domainObject, pps, stateMachine);
@SuppressWarnings("unchecked")
@@ -1271,11 +1273,12 @@ public final class Neo4jTemplate implements
DefaultExecutableQuery(PreparedQuery<T> preparedQuery, boolean readOnly) {
this.preparedQuery = preparedQuery;
this.txTemplate = readOnly ? transactionTemplateReadOnly : transactionTemplate;
// At this time, both must be initialized
this.txTemplate = Objects.requireNonNull(readOnly ? transactionTemplateReadOnly : transactionTemplate);
}
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "NullAway"})
public List<T> getResults() {
return txTemplate
.execute(tx -> {
@@ -1287,7 +1290,7 @@ public final class Neo4jTemplate implements
});
}
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "NullAway"})
public Optional<T> getSingleResult() {
return txTemplate.execute(tx -> {
try {
@@ -1304,7 +1307,7 @@ public final class Neo4jTemplate implements
});
}
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "NullAway"})
public T getRequiredSingleResult() {
return txTemplate.execute(tx -> {
Optional<T> one = createFetchSpec().flatMap(Neo4jClient.RecordFetchSpec::one);
@@ -1326,11 +1329,12 @@ public final class Neo4jTemplate implements
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
if (cypherQuery == null || containsPossibleCircles) {
Statement statement;
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
// The null check for the metadata is superfluous, but the easiest way to make NullAway happy
if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
NodesAndRelationshipsByIdStatementProvider nodesAndRelationshipsById =
createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
if (nodesAndRelationshipsById.hasRootNodeIds()) {
if (!nodesAndRelationshipsById.hasRootNodeIds()) {
return Optional.empty();
}
statement = nodesAndRelationshipsById.toStatement(entityMetaData);
@@ -1341,7 +1345,7 @@ public final class Neo4jTemplate implements
finalParameters = TemplateSupport.mergeParameters(statement, finalParameters);
}
Neo4jClient.MappingSpec<T> newMappingSpec = neo4jClient.query(cypherQuery)
Neo4jClient.MappingSpec<T> newMappingSpec = neo4jClient.query(Objects.requireNonNull(cypherQuery, "Could not compute a query"))
.bindAll(finalParameters).fetchAs(preparedQuery.getResultType());
return preparedQuery.getOptionalMappingFunction()
.map(newMappingSpec::mappedBy).or(() -> Optional.of(newMappingSpec));
@@ -1364,7 +1368,7 @@ public final class Neo4jTemplate implements
.fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
.one()
.map(value -> value.asList(TemplateSupport::convertIdOrElementIdToString))
.get());
.orElseThrow());
if (rootNodeIds.isEmpty()) {
// fast return if no matching root node(s) are found
@@ -1397,11 +1401,15 @@ public final class Neo4jTemplate implements
Neo4jPersistentEntity<?> target = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getTarget();
@SuppressWarnings("unchecked")
String fieldName = ((Association<Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ?
fieldName + "." + ((Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRelationshipPropertiesEntity())
.getPersistentProperty(TargetNode.class).getFieldName() : fieldName));
String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
PropertyPathWalkStep nextPathStep;
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRelationshipPropertiesEntity();
if (sourceRelationshipDescription.hasRelationshipProperties() && relationshipPropertiesEntity != null) {
var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType()));
nextPathStep = currentPathStep.with(fieldName + "." + targetNodeProperty.getFieldName());
} else {
nextPathStep = currentPathStep.with(fieldName);
}
Collection<RelationshipDescription> relationships = target
.getRelationshipsInHierarchy(
@@ -1430,7 +1438,6 @@ public final class Neo4jTemplate implements
}
}
@NonNull
private Consumer<Map<String, Object>> iterateAndMapNextLevel(Map<String, Set<String>> relationshipsToRelatedNodes,
RelationshipDescription relationshipDescription,
PropertyPathWalkStep currentPathStep) {
@@ -1439,9 +1446,11 @@ public final class Neo4jTemplate implements
Map<String, Set<String>> relatedNodesVisited = new HashMap<>(relationshipsToRelatedNodes);
@SuppressWarnings("unchecked")
List<String> newRelationshipIds = ((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList();
var sr = (List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
List<String> newRelationshipIds = (sr != null) ? sr.stream().map(TemplateSupport::convertIdOrElementIdToString).toList() : List.of();
@SuppressWarnings("unchecked")
Set<String> relatedIds = new HashSet<>(((List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES)).stream().map(TemplateSupport::convertIdOrElementIdToString).toList());
var srn = (List<Object>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
Set<String> relatedIds = (srn != null) ? new HashSet<>(srn.stream().map(TemplateSupport::convertIdOrElementIdToString).toList()) : Set.of();
// use this list to get down the road
// 1. remove already visited ones;

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
@@ -27,7 +28,6 @@ import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.MappingSupport;
import org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.Collections;
@@ -35,6 +35,7 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -66,7 +67,9 @@ public final class PreparedQuery<T> {
private final Class<T> resultType;
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
private final @Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
@Nullable
private final Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private volatile Optional<BiFunction<TypeSystem, Record, T>> lastMappingFunction = Optional.empty();
private PreparedQuery(OptionalBuildSteps<T> optionalBuildSteps) {
@@ -127,7 +130,8 @@ public final class PreparedQuery<T> {
final Class<CT> resultType;
final QueryFragmentsAndParameters queryFragmentsAndParameters;
@Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
@Nullable
Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunctionSupplier;
OptionalBuildSteps(Class<CT> resultType, QueryFragmentsAndParameters queryFragmentsAndParameters) {
this.resultType = resultType;
@@ -140,8 +144,8 @@ public final class PreparedQuery<T> {
* @param newParameters The new parameters for the prepared query.
* @return This builder.
*/
public OptionalBuildSteps<CT> withParameters(Map<String, Object> newParameters) {
this.queryFragmentsAndParameters.setParameters(newParameters);
public OptionalBuildSteps<CT> withParameters(@Nullable Map<String, Object> newParameters) {
this.queryFragmentsAndParameters.setParameters(Objects.requireNonNullElseGet(newParameters, Map::of));
return this;
}
@@ -229,6 +233,9 @@ public final class PreparedQuery<T> {
}
@Override
// Suppressing the warnings for accessing `pathValues`: `partitioningBy`
// will always provide entries for `true` and `false`
@SuppressWarnings("NullAway")
public Object apply(TypeSystem t, Record r) {
if (r.size() == 1) {

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
@@ -27,12 +28,13 @@ import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
/**
* This class is responsible for creating a List of {@link PropertyPath} entries that contains all reachable
@@ -85,7 +87,7 @@ public final class PropertyFilterSupport {
// try to figure out the right property by name
for (GraphPropertyDescription graphProperty : domainEntity.getGraphProperties()) {
if (graphProperty.getPropertyName().equals(inputProperty.getName())) {
typeInformation = domainEntity.getPersistentProperty(graphProperty.getFieldName()).getTypeInformation();
typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(graphProperty.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null);
break;
}
}
@@ -93,13 +95,15 @@ public final class PropertyFilterSupport {
if (typeInformation == null) {
for (RelationshipDescription relationshipDescription : domainEntity.getRelationships()) {
if (relationshipDescription.getFieldName().equals(inputProperty.getName())) {
typeInformation = domainEntity.getPersistentProperty(relationshipDescription.getFieldName()).getTypeInformation();
typeInformation = Optional.ofNullable(domainEntity.getPersistentProperty(relationshipDescription.getFieldName())).map(PersistentProperty::getTypeInformation).orElse(null);
break;
}
}
}
}
addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext);
if (typeInformation != null) {
addPropertiesFrom(domainType, returnType, projectionFactory, propertyPaths, new ProjectionPathProcessor(inputProperty.getName(), typeInformation), neo4jMappingContext);
}
}
return propertyPaths;
}
@@ -127,16 +131,18 @@ public final class PropertyFilterSupport {
TypeInformation<?> mapValueType = projectionPathProcessor.typeInformation.getRequiredMapValueType();
if (mapValueType.isCollectionLike()) {
currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType().getComponentType();
propertyType = projectionPathProcessor.typeInformation.getRequiredMapValueType().getComponentType().getType();
propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType();
} else {
currentTypeInformation = projectionPathProcessor.typeInformation.getRequiredMapValueType();
propertyType = projectionPathProcessor.typeInformation.getRequiredMapValueType().getType();
propertyType = currentTypeInformation.getType();
}
} else if (projectionPathProcessor.typeInformation.isCollectionLike()) {
currentTypeInformation = projectionPathProcessor.typeInformation.getComponentType();
propertyType = projectionPathProcessor.typeInformation.getComponentType().getType();
propertyType = Objects.requireNonNull(currentTypeInformation, "Cannot retrieve collection type").getType();
}
Objects.requireNonNull(currentTypeInformation, "Property type is required");
// 1. Simple types can be added directly
// 2. Something that looks like an entity needs to get processed as such
// 3. Embedded projection
@@ -150,17 +156,18 @@ public final class PropertyFilterSupport {
if (nestedProjectionInformation.isClosed()) {
filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, false));
for (PropertyDescriptor nestedInputProperty : nestedProjectionInformation.getInputProperties()) {
TypeInformation<?> typeInformation = currentTypeInformation.getProperty(nestedInputProperty.getName());
TypeInformation<?> typeInformation = currentTypeInformation.getRequiredProperty(nestedInputProperty.getName());
ProjectionPathProcessor nextProjectionPathProcessor = projectionPathProcessor.next(nestedInputProperty, typeInformation);
TypeInformation<?> actualType = Objects.requireNonNull(nextProjectionPathProcessor.typeInformation.getActualType());
if (projectionPathProcessor.isChildLevel() &&
(domainType.equals(nextProjectionPathProcessor.typeInformation.getType())
|| returnedType.equals(nextProjectionPathProcessor.typeInformation.getActualType().getType())
|| returnedType.equals(actualType.getType())
|| returnedType.equals(nextProjectionPathProcessor.typeInformation.getType()))) {
break;
}
if (projectionPathProcessor.typeInformation.getActualType().getType().equals(nextProjectionPathProcessor.typeInformation.getActualType().getType())
if (projectionPathProcessor.typeInformation.getActualType() != null && projectionPathProcessor.typeInformation.getActualType().getType().equals(actualType.getType())
|| (!projectionPathProcessor.typeInformation.isCollectionLike() && !projectionPathProcessor.typeInformation.isMap() && projectionPathProcessor.typeInformation.getType().equals(nextProjectionPathProcessor.typeInformation.getType()))) {
filteredProperties.add(new PropertyFilter.ProjectedPath(propertyPath, true));
} else {
@@ -182,13 +189,13 @@ public final class PropertyFilterSupport {
final String path;
final String name;
private ProjectionPathProcessor(String name, String path, @Nullable TypeInformation<?> typeInformation) {
private ProjectionPathProcessor(String name, String path, TypeInformation<?> typeInformation) {
this.typeInformation = typeInformation;
this.path = path;
this.name = name;
}
private ProjectionPathProcessor(String name, @Nullable TypeInformation<?> typeInformation) {
private ProjectionPathProcessor(String name, TypeInformation<?> typeInformation) {
this(name, name, typeInformation);
}

View File

@@ -24,7 +24,6 @@ import java.util.Map;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.lang.Nullable;
/**
* {@link ReactiveFluentFindOperation} allows creation and execution of Neo4j find operations in a fluent API style.
@@ -95,7 +94,7 @@ public interface ReactiveFluentFindOperation {
* @return new instance of {@link TerminatingFind}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingFind<T> matching(String query, @Nullable Map<String, Object> parameter);
TerminatingFind<T> matching(String query, Map<String, Object> parameter);
/**
* Creates an executable query based on fragments and parameters. Hardly useful outside framework-code
@@ -126,7 +125,7 @@ public interface ReactiveFluentFindOperation {
* @return new instance of {@link TerminatingFind}.
* @throws IllegalArgumentException if statement is {@literal null}.
*/
TerminatingFind<T> matching(Statement statement, @Nullable Map<String, Object> parameter);
TerminatingFind<T> matching(Statement statement, Map<String, Object> parameter);
/**
* Set the filter {@link Statement statement} to be used.

View File

@@ -21,6 +21,7 @@ import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import org.springframework.util.Assert;
@@ -54,12 +55,15 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
private final ReactiveNeo4jTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
@Nullable
private final String query;
@Nullable
private final Map<String, Object> parameters;
@Nullable
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, String query,
Map<String, Object> parameters) {
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String query,
@Nullable Map<String, Object> parameters) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
@@ -68,7 +72,7 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
this.queryFragmentsAndParameters = null;
}
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) {
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.neo4j.core;
import org.jspecify.annotations.Nullable;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -34,7 +35,6 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.log.LogAccessor;
import org.springframework.data.neo4j.core.Neo4jClient.BindSpec;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.lang.Nullable;
/**
* Reactive Neo4j client. The main difference to the {@link Neo4jClient imperative Neo4j client} is the fact that all
@@ -123,7 +123,7 @@ public interface ReactiveNeo4jClient {
* @return The builder
* @since 6.3.3
*/
public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) {
public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) {
this.neo4jConversions = neo4jConversions;
return this;
}
@@ -137,7 +137,7 @@ public interface ReactiveNeo4jClient {
* @return The builder
* @since 7.1.2
*/
public Builder withNeo4jBookmarkManager(Neo4jBookmarkManager bookmarkManager) {
public Builder withNeo4jBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) {
this.bookmarkManager = bookmarkManager;
return this;
}
@@ -306,7 +306,7 @@ public interface ReactiveNeo4jClient {
* @param targetDatabase selected database to use. A {@literal null} value indicates the default database.
* @return A runnable query specification that is now bound to a given database.
*/
RunnableSpecBoundToDatabase in(@Nullable String targetDatabase);
RunnableSpecBoundToDatabase in(String targetDatabase);
/**
* Pins the previously defined query to an impersonated user. A value of {@literal null} chooses the user owning
@@ -315,7 +315,7 @@ public interface ReactiveNeo4jClient {
* @param asUser The name of the user to impersonate. A {@literal null} value indicates the connected user.
* @return A runnable query specification that is now bound to a given database.
*/
RunnableSpecBoundToUser asUser(@Nullable String asUser);
RunnableSpecBoundToUser asUser(String asUser);
}
/**
@@ -361,7 +361,7 @@ public interface ReactiveNeo4jClient {
* @param targetDatabase selected database to use. A {@literal null} value indicates the default database.
* @return An ongoing delegation
*/
RunnableDelegation<T> in(@Nullable String targetDatabase);
RunnableDelegation<T> in(String targetDatabase);
}
/**

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.neo4j.core;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
@@ -27,7 +28,6 @@ import java.util.function.BiPredicate;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.lang.Nullable;
/**
* Specifies reactive operations one can perform on a database, based on an <em>Domain Type</em>.
@@ -272,8 +272,7 @@ public interface ReactiveNeo4jOperations {
*/
<T> Mono<Void> deleteById(Object id, Class<T> domainType);
<T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty,
@Nullable Object versionValue);
<T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue);
/**
* Deletes all entities with one of the given ids, including all entities related to that entity.

View File

@@ -15,8 +15,33 @@
*/
package org.springframework.data.neo4j.core;
import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.FunctionInvocation;
@@ -26,7 +51,6 @@ import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.Entity;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
@@ -72,42 +96,17 @@ import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParamete
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuple3;
import reactor.util.function.Tuples;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.neo4j.cypherdsl.core.Cypher.anyNode;
import static org.neo4j.cypherdsl.core.Cypher.asterisk;
import static org.neo4j.cypherdsl.core.Cypher.parameter;
/**
* @author Michael J. Simons
* @author Gerrit Meier
@@ -138,14 +137,18 @@ public final class ReactiveNeo4jTemplate implements
}
};
@Nullable
private TransactionalOperator transactionalOperatorReadOnly;
@Nullable
private TransactionalOperator transactionalOperator;
@Nullable
private ClassLoader beanClassLoader;
private ReactiveEventSupport eventSupport;
@Nullable
private ProjectionFactory projectionFactory;
private Renderer renderer;
@@ -202,13 +205,29 @@ public final class ReactiveNeo4jTemplate implements
public Mono<Long> count(String cypherQuery, Map<String, Object> parameters) {
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery)
.withParameters(parameters).build();
return transactionalOperatorReadOnly.transactional(this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult));
return executeReadOnly(this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult));
}
private <T> Mono<T> executeReadOnly(Mono<T> action) {
return Objects.requireNonNull(this.transactionalOperatorReadOnly).transactional(action);
}
private <T> Flux<T> executeReadOnly(Flux<T> action) {
return Objects.requireNonNull(this.transactionalOperatorReadOnly).transactional(action);
}
private <T> Mono<T> execute(Mono<T> action) {
return Objects.requireNonNull(this.transactionalOperator).transactional(action);
}
private <T> Flux<T> execute(Flux<T> action) {
return Objects.requireNonNull(this.transactionalOperator).transactional(action);
}
@Override
public <T> Flux<T> findAll(Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(doFindAll(domainType, null));
return executeReadOnly(doFindAll(domainType, null));
}
private <T> Flux<T> doFindAll(Class<T> domainType, @Nullable Class<?> resultType) {
@@ -221,34 +240,34 @@ public final class ReactiveNeo4jTemplate implements
@Override
public <T> Flux<T> findAll(Statement statement, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults));
return executeReadOnly(createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults));
}
@Override
public <T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, statement, parameters).flatMapMany(ExecutableQuery::getResults));
return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMapMany(ExecutableQuery::getResults));
}
@Override
public <T> Mono<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, statement, parameters).flatMap(ExecutableQuery::getSingleResult));
return executeReadOnly(createExecutableQuery(domainType, null, statement, parameters).flatMap(ExecutableQuery::getSingleResult));
}
@Override
public <T> Flux<T> findAll(String cypherQuery, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, cypherQuery).flatMapMany(ExecutableQuery::getResults));
return executeReadOnly(createExecutableQuery(domainType, cypherQuery).flatMapMany(ExecutableQuery::getResults));
}
@Override
public <T> Flux<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults));
return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMapMany(ExecutableQuery::getResults));
}
@Override
public <T> Mono<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType) {
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult));
return executeReadOnly(createExecutableQuery(domainType, null, cypherQuery, parameters).flatMap(ExecutableQuery::getSingleResult));
}
@Override
@@ -265,15 +284,15 @@ public final class ReactiveNeo4jTemplate implements
} else {
Mono<ExecutableQuery<T>> executableQuery;
if (queryFragmentsAndParameters == null) {
executableQuery = createExecutableQuery(domainType, resultType, cypherQuery,
executableQuery = createExecutableQuery(domainType, resultType, Objects.requireNonNull(cypherQuery),
parameters == null ? Collections.emptyMap() : parameters);
} else {
executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters);
}
intermediaResults = switch (fetchType) {
case ALL -> transactionalOperatorReadOnly.transactional(executableQuery.flatMapMany(ExecutableQuery::getResults));
case ONE -> transactionalOperatorReadOnly.transactional(executableQuery.flatMap(ExecutableQuery::getSingleResult).flux());
case ALL -> executeReadOnly(executableQuery.flatMapMany(ExecutableQuery::getResults));
case ONE -> executeReadOnly(executableQuery.flatMap(ExecutableQuery::getSingleResult).flux());
};
}
@@ -296,7 +315,7 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
QueryFragmentsAndParameters fragmentsAndParameters = QueryFragmentsAndParameters
.forExistsById(entityMetaData, convertIdValues(entityMetaData.getRequiredIdProperty(), id));
.forExistsById(entityMetaData, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id));
Statement statement = fragmentsAndParameters.getQueryFragments().toStatement();
Map<String, Object> parameters = fragmentsAndParameters.getParameters();
@@ -309,9 +328,9 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null,
return executeReadOnly(createExecutableQuery(domainType, null,
QueryFragmentsAndParameters.forFindById(entityMetaData,
convertIdValues(entityMetaData.getRequiredIdProperty(), id)))
TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id)))
.flatMap(ExecutableQuery::getSingleResult));
}
@@ -320,9 +339,9 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
return transactionalOperatorReadOnly.transactional(createExecutableQuery(domainType, null,
return executeReadOnly(createExecutableQuery(domainType, null,
QueryFragmentsAndParameters.forFindByAllId(entityMetaData,
convertIdValues(entityMetaData.getRequiredIdProperty(), ids)))
TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids)))
.flatMapMany(ExecutableQuery::getResults));
}
@@ -333,36 +352,20 @@ public final class ReactiveNeo4jTemplate implements
return createExecutableQuery(domainType, null, queryFragmentsAndParameters);
}
private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) {
if (idProperty != null && ((Neo4jPersistentEntity<?>) idProperty.getOwner()).isUsingInternalIds()) {
return idValues;
}
if (idValues != null) {
return neo4jMappingContext.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter());
} else if (idProperty != null) {
return neo4jMappingContext.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter());
} else {
// Not much we can convert here
return Values.NULL;
}
}
@Override
public <T> Mono<T> save(T instance) {
return transactionalOperator.transactional(saveImpl(instance, Collections.emptySet(), null));
return execute(saveImpl(instance, Collections.emptySet(), null));
}
@Override
public <T> Mono<T> saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
if (instance == null) {
return null;
return Mono.empty();
}
return transactionalOperator.transactional(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null));
return execute(saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null));
}
@Override
@@ -371,7 +374,7 @@ public final class ReactiveNeo4jTemplate implements
Assert.notNull(resultType, "ResultType must not be null");
if (instance == null) {
return null;
return Mono.empty();
}
if (resultType.equals(instance.getClass())) {
@@ -383,7 +386,7 @@ public final class ReactiveNeo4jTemplate implements
Collection<PropertyFilter.ProjectedPath> pps = PropertyFilterSupport.addPropertiesFrom(instance.getClass(), resultType,
localProjectionFactory, neo4jMappingContext);
Mono<T> savingPublisher = transactionalOperator.transactional(saveImpl(instance, pps, null));
Mono<T> savingPublisher = execute(saveImpl(instance, pps, null));
if (!resultType.isInterface()) {
return savingPublisher.map(savedInstance -> {
@@ -399,9 +402,9 @@ public final class ReactiveNeo4jTemplate implements
return savingPublisher.flatMap(savedInstance -> {
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(savedInstance.getClass());
Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty();
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance);
return transactionalOperatorReadOnly.transactional(this.findById(propertyAccessor.getProperty(idProperty), savedInstance.getClass())
return executeReadOnly(this.findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), savedInstance.getClass())
.map(loadedValue -> localProjectionFactory.createProjection(resultType, loadedValue)));
});
}
@@ -412,7 +415,7 @@ public final class ReactiveNeo4jTemplate implements
return Flux.empty();
}
Class<?> resultType = TemplateSupport.findCommonElementType(instances);
Class<?> resultType = Objects.requireNonNull(TemplateSupport.findCommonElementType(instances), () -> "Could not find a common type element to store and then project multiple instances of type %s".formatted(domainType));
Collection<PropertyFilter.ProjectedPath> pps = PropertyFilterSupport.addPropertiesFrom(domainType, resultType,
getProjectionFactory(), neo4jMappingContext);
@@ -423,20 +426,24 @@ public final class ReactiveNeo4jTemplate implements
return Flux.fromIterable(instances)
.concatMap(instance -> {
T domainObject = converter.convert(instance);
if (domainObject == null) {
return Mono.empty();
}
@SuppressWarnings("unchecked")
Mono<R> result = transactionalOperator.transactional(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds)
Mono<R> result = execute(saveImpl(domainObject, pps, stateMachine, knownRelationshipsIds)
.map(savedEntity -> (R) new DtoInstantiatingConverter(resultType, neo4jMappingContext).convertDirectly(savedEntity)));
return result;
});
}
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
private <T> Mono<T> saveImpl(T instance, Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine) {
return saveImpl(instance, includedProperties, stateMachine, new HashSet<>());
}
private <T> Mono<T> saveImpl(T instance, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection<Object> knownRelationshipsIds) {
@SuppressWarnings("deprecation")
private <T> Mono<T> saveImpl(T instance, Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable NestedRelationshipProcessingStateMachine stateMachine, Collection<Object> knownRelationshipsIds) {
if (stateMachine != null && stateMachine.hasProcessedValue(instance)) {
return Mono.just(instance);
@@ -500,7 +507,7 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
ReactiveNeo4jClient.RunnableSpec runnableQuery = neo4jClient
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty)))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, propertyAccessor.getProperty(idProperty)))
.to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM);
if (entityMetaData.hasVersionProperty()) {
@@ -518,13 +525,13 @@ public final class ReactiveNeo4jTemplate implements
@Override
public <T> Flux<T> saveAll(Iterable<T> instances) {
return transactionalOperator.transactional(saveAllImpl(instances, Collections.emptySet(), null));
return execute(saveAllImpl(instances, Collections.emptySet(), null));
}
@Override
public <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
return transactionalOperator.transactional(saveAllImpl(instances, null, includeProperty));
return execute(saveAllImpl(instances, null, includeProperty));
}
@Override
@@ -552,20 +559,21 @@ public final class ReactiveNeo4jTemplate implements
Collection<PropertyFilter.ProjectedPath> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
localProjectionFactory, neo4jMappingContext);
Flux<T> savedInstances = transactionalOperator.transactional(saveAllImpl(instances, pps, null));
Flux<T> savedInstances = execute(saveAllImpl(instances, pps, null));
if (projectionInformation.isClosed()) {
return savedInstances.map(instance -> localProjectionFactory.createProjection(resultType, instance));
}
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(commonElementType);
Neo4jPersistentProperty idProperty = entityMetaData.getIdProperty();
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
return savedInstances.concatMap(savedInstance -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(savedInstance);
return transactionalOperatorReadOnly.transactional(findById(propertyAccessor.getProperty(idProperty), commonElementType));
return executeReadOnly(findById(Objects.requireNonNull(propertyAccessor.getProperty(idProperty)), commonElementType));
}).map(instance -> localProjectionFactory.createProjection(resultType, instance));
}
@SuppressWarnings("unchecked")
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Collection<PropertyFilter.ProjectedPath> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
Set<Class<?>> types = new HashSet<>();
@@ -583,7 +591,7 @@ public final class ReactiveNeo4jTemplate implements
Class<?> domainClass = types.iterator().next();
Collection<PropertyFilter.ProjectedPath> pps = includeProperty == null ?
includedProperties :
Objects.requireNonNullElseGet(includedProperties, List::of) :
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
includeProperty);
@@ -645,9 +653,9 @@ public final class ReactiveNeo4jTemplate implements
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
return transactionalOperator.transactional(Mono.defer(() ->
return execute(Mono.defer(() ->
this.neo4jClient.query(() -> renderer.render(statement))
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), ids))
.to(nameOfParameter).run().then()));
}
@@ -661,15 +669,14 @@ public final class ReactiveNeo4jTemplate implements
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
return transactionalOperator.transactional(Mono.defer(() ->
return execute(Mono.defer(() ->
this.neo4jClient.query(() -> renderer.render(statement))
.bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id))
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id))
.to(nameOfParameter).run().then()));
}
@Override
public <T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty,
Object versionValue) {
public <T> Mono<Void> deleteByIdWithVersion(Object id, Class<T> domainType, Neo4jPersistentProperty versionProperty, @Nullable Object versionValue) {
String nameOfParameter = "id";
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
@@ -682,10 +689,10 @@ public final class ReactiveNeo4jTemplate implements
.returning(Constants.NAME_OF_TYPED_ROOT_NODE.apply(entityMetaData)).build();
Map<String, Object> parameters = new HashMap<>();
parameters.put(nameOfParameter, convertIdValues(entityMetaData.getRequiredIdProperty(), id));
parameters.put(nameOfParameter, TemplateSupport.convertIdValues(this.neo4jMappingContext, entityMetaData.getRequiredIdProperty(), id));
parameters.put(Constants.NAME_OF_VERSION_PARAM, versionValue);
return transactionalOperator.transactional(Mono.defer(() ->
return execute(Mono.defer(() ->
this.neo4jClient.query(() -> renderer.render(statement))
.bindAll(parameters)
.fetch().one().switchIfEmpty(Mono.defer(() -> {
@@ -702,7 +709,7 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData);
return transactionalOperator.transactional(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)).run().then()));
return execute(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)).run().then()));
}
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, Statement statement) {
@@ -719,7 +726,7 @@ public final class ReactiveNeo4jTemplate implements
return createExecutableQuery(domainType, resultType, renderer.render(statement), TemplateSupport.mergeParameters(statement, parameters));
}
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, @Nullable Class<?> resultType, @Nullable String cypherQuery,
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, @Nullable Class<?> resultType, String cypherQuery,
Map<String, Object> parameters) {
Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction = TemplateSupport
@@ -748,8 +755,9 @@ public final class ReactiveNeo4jTemplate implements
return createExecutableQuery(domainType, resultType, queryFragments.toStatement(), queryFragmentsAndParameters.getParameters());
}
@SuppressWarnings({"unchecked"})
private Mono<NodesAndRelationshipsByIdStatementProvider> createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity<?> entityMetaData,
QueryFragments queryFragments, Map<String, Object> parameters) {
QueryFragments queryFragments, Map<String, Object> parameters) {
return Mono.deferContextual(ctx -> {
Class<?> rootClass = entityMetaData.getUnderlyingClass();
@@ -776,10 +784,7 @@ public final class ReactiveNeo4jTemplate implements
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
.one()
.map((t) -> {
//noinspection unchecked
return (Tuple2<Collection<String>, Collection<String>>) t;
})
.map((t) -> (Tuple2<Collection<String>, Collection<String>>) t)
.expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty()));
})
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipsToRelatedNodeIds.keySet(), relationshipsToRelatedNodeIds.values().stream().flatMap(Collection::stream).toList(), queryFragments, elementIdOrIdFunction)));
@@ -790,18 +795,23 @@ public final class ReactiveNeo4jTemplate implements
}
@SuppressWarnings("unchecked")
private Flux<Tuple2<Collection<String>, Collection<String>>> iterateNextLevel(Collection<String> relatedNodeIds,
RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments,
Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments,
Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
NodeDescription<?> target = sourceRelationshipDescription.getTarget();
@SuppressWarnings("unchecked")
String fieldName = ((Association<Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
String fieldName = ((Association<@NonNull Neo4jPersistentProperty>) sourceRelationshipDescription).getInverse().getFieldName();
PropertyPathWalkStep nextPathStep = currentPathStep.with((sourceRelationshipDescription.hasRelationshipProperties() ?
fieldName + "." + ((Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRelationshipPropertiesEntity())
.getPersistentProperty(TargetNode.class).getFieldName() : fieldName));
PropertyPathWalkStep nextPathStep;
if (sourceRelationshipDescription.hasRelationshipProperties()) {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getRequiredRelationshipPropertiesEntity();
nextPathStep = currentPathStep.with(fieldName + "." + Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType())).getFieldName());
} else {
nextPathStep = currentPathStep.with(fieldName);
}
return Flux.fromIterable(target
.getRelationshipsInHierarchy(
@@ -829,16 +839,12 @@ public final class ReactiveNeo4jTemplate implements
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
.one()
.map((t) -> {
//noinspection unchecked
return (Tuple2<Collection<String>, Collection<String>>) t;
})
.map((t) -> (Tuple2<Collection<String>, Collection<String>>) t)
.expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object));
});
}
@NonNull
private Function<Tuple2<Collection<String>, Collection<String>>,
Publisher<Tuple2<Collection<String>, Collection<String>>>> iterateAndMapNextLevel(
RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
@@ -900,6 +906,7 @@ public final class ReactiveNeo4jTemplate implements
stateMachine, knownRelationshipsIds, includeProperty, startingPropertyPath);
}
@SuppressWarnings("deprecation")
private <T> Mono<T> processNestedRelations(Neo4jPersistentEntity<?> sourceEntity, PersistentPropertyAccessor<?> parentPropertyAccessor,
boolean isParentObjectNew, NestedRelationshipProcessingStateMachine stateMachine,
Collection<Object> knownRelationshipsIds,
@@ -933,7 +940,7 @@ public final class ReactiveNeo4jTemplate implements
idProperty = null;
} else {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
idProperty = relationshipPropertiesEntity.getIdProperty();
idProperty = relationshipPropertiesEntity == null ? null : relationshipPropertiesEntity.getIdProperty();
}
// break recursive procession and deletion of previously created relationships
@@ -950,12 +957,13 @@ public final class ReactiveNeo4jTemplate implements
if (idProperty != null) {
for (Object relatedValueToStore : relatedValuesToStore) {
//noinspection ConstantValue
if (relatedValueToStore == null) {
continue;
}
Object id = relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
Object id = Objects.requireNonNull(relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore))
.getProperty(idProperty);
if (id != null) {
knownRelationshipsIds.add(id);
@@ -967,7 +975,7 @@ public final class ReactiveNeo4jTemplate implements
relationshipDeleteMonos.add(
neo4jClient.query(renderer.render(relationshipRemoveQuery))
.bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(knownRelationshipsIds) //
.to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) //
@@ -1016,7 +1024,7 @@ public final class ReactiveNeo4jTemplate implements
queryOrSave = savedEntity
.map(entity -> Tuples.of(new AtomicReference<>((Object) (TemplateSupport.rendererCanUseElementIdIfPresent(renderer, targetEntity) ? entity.elementId() : entity.id())), new AtomicReference<>(entity)))
.doOnNext(t -> {
var relatedInternalId = t.getT1().get();
var relatedInternalId = Objects.requireNonNull(t.getT1().get(), "Related internal id is null");
stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId);
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
@@ -1031,7 +1039,9 @@ public final class ReactiveNeo4jTemplate implements
Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty();
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty);
//noinspection OptionalOfNullableMisuse
relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId);
//noinspection ConstantValue
if (savedEntity != null) {
TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity);
}
@@ -1039,21 +1049,22 @@ public final class ReactiveNeo4jTemplate implements
stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId,
relationshipDescription.getRelationshipObverse());
Object idValue = idProperty != null
? relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty)
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore);
Object idValue = (idProperty != null && relationshipPropertiesPropertyAccessor != null)
? relationshipPropertiesPropertyAccessor.getProperty(idProperty)
: null;
boolean isNewRelationship = idValue == null;
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship(
sourceEntity, relationshipDescription, relatedValueToStore, isNewRelationship, canUseElementId);
Map<String, Object> properties = new HashMap<>();
properties.put(Constants.FROM_ID_PARAMETER_NAME, convertIdValues(sourceEntity.getRequiredIdProperty(), fromId));
Map<String, @Nullable Object> properties = new HashMap<>();
properties.put(Constants.FROM_ID_PARAMETER_NAME, TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId));
properties.put(Constants.TO_ID_PARAMETER_NAME, relatedInternalId);
properties.put(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM, idValue);
var update = true;
if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties()) {
if (!relationshipDescription.isDynamic() && relationshipDescription.hasRelationshipProperties() && fromId != null) {
var hlp = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore);
var hasProcessedRelationshipEntity = stateMachine.hasProcessedRelationshipEntity(parentPropertyAccessor.getBean(), hlp.getRelatedEntity(), relationshipContext.getRelationship());
if (hasProcessedRelationshipEntity) {
@@ -1070,7 +1081,7 @@ public final class ReactiveNeo4jTemplate implements
if (update) {
return neo4jClient
.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
@@ -1081,9 +1092,8 @@ public final class ReactiveNeo4jTemplate implements
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
.one()
.flatMap(relationshipInternalId -> {
if (idProperty != null && isNewRelationship) {
relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
if (idProperty != null && isNewRelationship && relationshipPropertiesPropertyAccessor != null) {
relationshipPropertiesPropertyAccessor
.setProperty(idProperty, relationshipInternalId);
knownRelationshipsIds.add(relationshipInternalId);
}
@@ -1130,16 +1140,16 @@ public final class ReactiveNeo4jTemplate implements
.thenMany(Flux.concat(relationshipCreationCreations))
.doOnNext(objects -> objects.applyFinalResultToOwner(parentPropertyAccessor))
.checkpoint()
.then(stateMachine.updateRelationshipIds(this::getRelationshipId))
.then(stateMachine.updateRelationshipIdsReactive(this::getRelationshipId))
.then(Mono.fromSupplier(parentPropertyAccessor::getBean));
return deleteAndThanCreateANew;
}
private Mono<Object> getRelationshipId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId) {
private Mono<Object> getRelationshipId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId) {
return neo4jClient.query(renderer.render(statement))
.bind(convertIdValues(idProperty, fromId)) //
.bind(TemplateSupport.convertIdValues(this.neo4jMappingContext, idProperty, fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(toId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
@@ -1150,10 +1160,10 @@ public final class ReactiveNeo4jTemplate implements
// The pendant to {@link #saveRelatedNode(Object, Neo4jPersistentEntity, PropertyFilter, PropertyFilter.RelaxedPropertyPath)}
// We can't do without a query, as we need to refresh the internal id
private Mono<Entity> loadRelatedNode(NodeDescription<?> targetNodeDescription, Object relatedInternalId) {
private Mono<Entity> loadRelatedNode(NodeDescription<?> targetNodeDescription, @Nullable Object relatedInternalId) {
var targetPersistentEntity = (Neo4jPersistentEntity<?>) targetNodeDescription;
var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, TemplateSupport.convertIdValues(this.neo4jMappingContext, targetPersistentEntity.getRequiredIdProperty(), relatedInternalId));
var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue();
return neo4jClient
@@ -1175,14 +1185,14 @@ public final class ReactiveNeo4jTemplate implements
DynamicLabels dynamicLabels = t.getT2();
@SuppressWarnings("unchecked")
Function<Object, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(entityType);
String idPropertyName = targetNodeDescription.getIdProperty().getPropertyName();
String idPropertyName = targetNodeDescription.getRequiredIdProperty().getPropertyName();
IdDescription idDescription = targetNodeDescription.getIdDescription();
boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId();
boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId());
binderFunction = binderFunction.andThen(tree -> {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
if (!includeProperty.isNotFiltering()) {
if (properties != null && !includeProperty.isNotFiltering()) {
properties.entrySet().removeIf(e -> {
// we cannot skip the id property if it is an assigned id
boolean isIdProperty = e.getKey().equals(idPropertyName);
@@ -1219,7 +1229,7 @@ public final class ReactiveNeo4jTemplate implements
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
if (cypherQuery == null || containsPossibleCircles) {
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
if (entityMetaData != null && containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
return createNodesAndRelationshipsByIdStatementProvider(entityMetaData, queryFragments, finalParameters)
.map(nodesAndRelationshipsById -> {
var statement = nodesAndRelationshipsById.toStatement(entityMetaData);
@@ -1256,7 +1266,7 @@ public final class ReactiveNeo4jTemplate implements
this.eventSupport = ReactiveEventSupport.discoverCallbacks(neo4jMappingContext, beanFactory);
SpelAwareProxyProjectionFactory spelAwareProxyProjectionFactory = new SpelAwareProxyProjectionFactory();
spelAwareProxyProjectionFactory.setBeanClassLoader(beanClassLoader);
spelAwareProxyProjectionFactory.setBeanClassLoader(Objects.requireNonNull(this.beanClassLoader));
spelAwareProxyProjectionFactory.setBeanFactory(beanFactory);
this.projectionFactory = spelAwareProxyProjectionFactory;
@@ -1324,7 +1334,7 @@ public final class ReactiveNeo4jTemplate implements
@SuppressWarnings("unchecked")
public Flux<T> getResults() {
return transactionalOperator.transactional(fetchSpec.all().switchOnFirst((signal, f) -> {
return execute(fetchSpec.all().switchOnFirst((signal, f) -> {
if (signal.hasValue() && preparedQuery.resultsHaveBeenAggregated()) {
return f.concatMap(nested -> Flux.fromIterable((Collection<T>) nested).distinct()).distinct();
}
@@ -1337,14 +1347,14 @@ public final class ReactiveNeo4jTemplate implements
* @throws IncorrectResultSizeDataAccessException if there is no or more than one result
*/
public Mono<T> getSingleResult() {
return transactionalOperator.transactional(fetchSpec.one().map(t -> {
return execute(fetchSpec.one().map(t -> {
if (t instanceof LinkedHashSet) {
@SuppressWarnings("unchecked")
T firstItem = (T) ((LinkedHashSet<?>) t).iterator().next();
return firstItem;
}
return t;
}).onErrorMap(IndexOutOfBoundsException.class, e -> new IncorrectResultSizeDataAccessException(e.getMessage(), 1)));
}).onErrorMap(IndexOutOfBoundsException.class, e -> new IncorrectResultSizeDataAccessException(Objects.requireNonNull(e.getMessage()), 1)));
}
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.core.CollectionFactory;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -34,6 +35,8 @@ import java.util.Optional;
@API(status = API.Status.INTERNAL, since = "6.1")
final class RelationshipHandler {
private static final int DEFAULT_SIZE = 32;
enum Cardinality {
ONE_TO_ONE,
@@ -42,7 +45,7 @@ final class RelationshipHandler {
DYNAMIC_ONE_TO_MANY
}
static RelationshipHandler forProperty(Neo4jPersistentProperty property, Object rawValue) {
static RelationshipHandler forProperty(Neo4jPersistentProperty property, @Nullable Object rawValue) {
Cardinality cardinality;
Collection<Object> newRelationshipObjectCollection = Collections.emptyList();
@@ -51,13 +54,16 @@ final class RelationshipHandler {
// Order is important here, all map based associations are dynamic, but not all dynamic associations are one to many
if (property.isCollectionLike()) {
cardinality = Cardinality.ONE_TO_MANY;
newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), ((Collection<?>) rawValue).size());
var size = rawValue == null ? DEFAULT_SIZE : ((Collection<?>) rawValue).size();
newRelationshipObjectCollection = CollectionFactory.createCollection(property.getType(), size);
} else if (property.isDynamicOneToManyAssociation()) {
cardinality = Cardinality.DYNAMIC_ONE_TO_MANY;
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map<?, ?>) rawValue).size());
var size = rawValue == null ? DEFAULT_SIZE : ((Map<?, ?>) rawValue).size();
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size);
} else if (property.isDynamicAssociation()) {
cardinality = Cardinality.DYNAMIC_ONE_TO_ONE;
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), ((Map<?, ?>) rawValue).size());
var size = rawValue == null ? DEFAULT_SIZE : ((Map<?, ?>) rawValue).size();
newRelationshipObjectCollectionMap = CollectionFactory.createMap(property.getType(), size);
} else {
cardinality = Cardinality.ONE_TO_ONE;
}
@@ -69,6 +75,7 @@ final class RelationshipHandler {
/**
* The raw value as passed to the template.
*/
@Nullable
private final Object rawValue;
private final Cardinality cardinality;
@@ -76,7 +83,7 @@ final class RelationshipHandler {
private final Map<Object, Object> newRelatedObjectsByType;
RelationshipHandler(Neo4jPersistentProperty property,
Object rawValue, Cardinality cardinality,
@Nullable Object rawValue, Cardinality cardinality,
Collection<Object> newRelatedObjects,
Map<Object, Object> newRelatedObjectsByType) {
this.property = property;
@@ -100,9 +107,12 @@ final class RelationshipHandler {
} else {
@SuppressWarnings("unchecked")
Collection<Object> newCollection = (Collection<Object>) newRelatedObjectsByType
.computeIfAbsent(key, k -> CollectionFactory.createCollection(
property.getTypeInformation().getRequiredActualType().getType(),
((Collection<?>) ((Map<?, ?>) rawValue).get(key)).size()));
.computeIfAbsent(key, k -> {
Collection<?> objects = rawValue == null ? null : (Collection<?>) ((Map<?, ?>) rawValue).get(key);
return CollectionFactory.createCollection(
property.getTypeInformation().getRequiredActualType().getType(),
objects != null ? objects.size() : DEFAULT_SIZE);
});
newCollection.add(potentiallyRecreatedRelatedObject);
}
}

View File

@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.NotificationClassification;
import org.neo4j.driver.NotificationSeverity;
import org.neo4j.driver.summary.InputPosition;
@@ -29,7 +30,6 @@ import org.neo4j.driver.summary.Notification;
import org.neo4j.driver.summary.Plan;
import org.neo4j.driver.summary.ResultSummary;
import org.springframework.core.log.LogAccessor;
import org.springframework.lang.Nullable;
/**
* Utility class for dealing with result summaries.

View File

@@ -17,12 +17,12 @@ package org.springframework.data.neo4j.core;
import java.util.function.BiFunction;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
/**
* Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}.
@@ -42,8 +42,8 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
this.targetClass = targetClass;
}
@Nullable
@Override
@Nullable
public T apply(TypeSystem typeSystem, Record record) {
if (record.size() == 0) {
@@ -58,7 +58,7 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
}
@Nullable
T convertValue(@Nullable Value source) {
T convertValue(Value source) {
if (targetClass == Void.class || targetClass == void.class) {
return null;
}

View File

@@ -35,6 +35,7 @@ import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.FunctionInvocation;
@@ -45,6 +46,7 @@ import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Dialect;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.Entity;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
@@ -62,7 +64,7 @@ import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.core.mapping.PropertyTraverser;
import org.springframework.data.neo4j.core.mapping.SpringDataCypherDsl;
import org.springframework.data.neo4j.repository.query.QueryFragments;
import org.springframework.lang.Nullable;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
@@ -75,7 +77,6 @@ import org.springframework.util.Assert;
@API(status = API.Status.INTERNAL, since = "6.0.9")
public final class TemplateSupport {
/**
* Indicator for an empty collection
*/
@@ -98,7 +99,7 @@ public final class TemplateSupport {
}
Collection<Class<?>> allClasses = StreamSupport.stream(collection.spliterator(), true)
.filter(o -> o != null)
.filter(Objects::nonNull)
.map(Object::getClass).collect(Collectors.toSet());
if (allClasses.isEmpty()) {
@@ -156,8 +157,9 @@ public final class TemplateSupport {
Entity newOrUpdatedNode
) {
if (entityMetaData.hasVersionProperty()) {
var versionProperty = entityMetaData.getRequiredVersionProperty();
propertyAccessor.setProperty(
entityMetaData.getVersionProperty(), newOrUpdatedNode.get(entityMetaData.getVersionProperty().getPropertyName()).asLong());
versionProperty, newOrUpdatedNode.get(versionProperty.getPropertyName()).asLong());
}
}
@@ -168,7 +170,7 @@ public final class TemplateSupport {
* @param parameters The original parameters
* @return Merged parameters
*/
static Map<String, Object> mergeParameters(Statement statement, @Nullable Map<String, Object> parameters) {
static Map<String, Object> mergeParameters(Statement statement, Map<String, Object> parameters) {
Map<String, Object> mergedParameters = new HashMap<>(statement.getCatalog().getParameters());
if (parameters != null) {
@@ -205,7 +207,8 @@ public final class TemplateSupport {
}
boolean hasRootNodeIds() {
return parameters.get(ROOT_NODE_IDS).isEmpty();
var ids = parameters.get(ROOT_NODE_IDS);
return ids != null && !ids.isEmpty();
}
Statement toStatement(NodeDescription<?> nodeDescription) {
@@ -291,10 +294,10 @@ public final class TemplateSupport {
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) tree.get(Constants.NAME_OF_PROPERTIES_PARAM);
String idPropertyName = entityMetaData.getIdProperty().getPropertyName();
String idPropertyName = entityMetaData.getRequiredIdProperty().getPropertyName();
IdDescription idDescription = entityMetaData.getIdDescription();
boolean assignedId = idDescription.isAssignedId() || idDescription.isExternallyGeneratedId();
if (!includeProperty.isNotFiltering()) {
boolean assignedId = idDescription != null && (idDescription.isAssignedId() || idDescription.isExternallyGeneratedId());
if (!(includeProperty.isNotFiltering() || properties == null)) {
properties.entrySet()
.removeIf(e -> {
// we cannot skip the id property if it is an assigned id
@@ -317,7 +320,7 @@ public final class TemplateSupport {
* @return A map as expected by the property filter.
*/
static <T> Collection<PropertyFilter.ProjectedPath> computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext,
Class<T> domainType, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
Class<T> domainType, BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
if (predicate == null) {
return Collections.emptySet();
}
@@ -357,6 +360,7 @@ public final class TemplateSupport {
* @param databaseEntity A fallback entity to retrieve the deprecated internal long id
* @param <T> The type of the entity
*/
@SuppressWarnings("deprecation")
static <T> void setGeneratedIdIfNecessary(
Neo4jPersistentEntity<?> entityMetaData,
PersistentPropertyAccessor<T> propertyAccessor,
@@ -388,10 +392,11 @@ public final class TemplateSupport {
* @param <T> The type of the entity
* @return The actual related internal id being used.
*/
@SuppressWarnings("deprecation")
static <T> Object retrieveOrSetRelatedId(
Neo4jPersistentEntity<?> entityMetadata,
PersistentPropertyAccessor<T> propertyAccessor,
Optional<Entity> databaseEntity,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<Entity> databaseEntity,
@Nullable Object relatedInternalId
) {
if (!entityMetadata.isUsingInternalIds()) {
@@ -442,7 +447,11 @@ public final class TemplateSupport {
return value.toString();
}
static Object convertToLongIdOrStringElementId(Collection<String> ids) {
@Nullable
static Object convertToLongIdOrStringElementId(@Nullable Collection<String> ids) {
if (ids == null) {
return null;
}
try {
return ids.stream()
.map(Long::valueOf).collect(Collectors.toSet());
@@ -452,6 +461,22 @@ public final class TemplateSupport {
}
}
static Object convertIdValues(Neo4jMappingContext ctx, @Nullable Neo4jPersistentProperty idProperty, @Nullable Object idValues) {
if (idProperty != null && ((Neo4jPersistentEntity<?>) idProperty.getOwner()).isUsingInternalIds()) {
return (idValues != null) ? idValues : Values.NULL;
}
if (idValues != null) {
return ctx.getConversionService().writeValue(idValues, TypeInformation.of(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalConverter());
} else if (idProperty != null) {
return ctx.getConversionService().writeValue(idValues, idProperty.getTypeInformation(), idProperty.getOptionalConverter());
} else {
// Not much we can convert here
return Values.NULL;
}
}
private TemplateSupport() {
}
}

View File

@@ -18,7 +18,7 @@ package org.springframework.data.neo4j.core;
import java.util.Objects;
import org.apiguardian.api.API;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@@ -57,7 +57,8 @@ public final class UserSelection {
return new UserSelection(value);
}
@Nullable private final String value;
@Nullable
private final String value;
private UserSelection(@Nullable String value) {
this.value = value;

View File

@@ -33,10 +33,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.exceptions.value.LossyCoercion;
@@ -267,7 +269,8 @@ final class AdditionalTypes {
@SuppressWarnings({"raw", "unchecked"}) // Due to dynamic enum retrieval
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null;
@@ -299,6 +302,7 @@ final class AdditionalTypes {
}
@Override
@Nullable
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
@@ -315,12 +319,14 @@ final class AdditionalTypes {
}
private static boolean describesSupportedEnumVariant(TypeDescriptor typeDescriptor) {
var elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor();
return typeDescriptor.isArray()
&& Enum.class.isAssignableFrom(typeDescriptor.getElementTypeDescriptor().getType());
&& elementTypeDescriptor != null && Enum.class.isAssignableFrom(elementTypeDescriptor.getType());
}
@Override
public Object convert(Object object, TypeDescriptor sourceType, TypeDescriptor targetType) {
@Nullable
public Object convert(@Nullable Object object, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (object == null) {
return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null;
@@ -329,17 +335,17 @@ final class AdditionalTypes {
if (Value.class.isAssignableFrom(sourceType.getType())) {
Value source = (Value) object;
TypeDescriptor elementTypeDescriptor = targetType.getElementTypeDescriptor();
TypeDescriptor elementTypeDescriptor = Objects.requireNonNull(targetType.getElementTypeDescriptor());
Object[] targetArray = (Object[]) Array.newInstance(elementTypeDescriptor.getType(), source.size());
Arrays.setAll(targetArray,
i -> delegate.convert(source.get(i), TypeDescriptor.valueOf(Value.class), elementTypeDescriptor));
return targetArray;
} else {
Enum[] source = (Enum[]) object;
Enum<?>[] source = (Enum<?>[]) object;
return Values.value(Arrays.stream(source)
.map(e -> delegate.convert(e, sourceType.getElementTypeDescriptor(), TypeDescriptor.valueOf(Value.class)))
.map(e -> delegate.convert(e, Objects.requireNonNull(sourceType.getElementTypeDescriptor()), TypeDescriptor.valueOf(Value.class)))
.toArray());
}
}
@@ -357,6 +363,7 @@ final class AdditionalTypes {
return Values.value(aFloat.toString());
}
@Nullable
static Locale asLocale(Value value) {
return StringUtils.parseLocale(value.asString());

View File

@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
@@ -73,11 +74,13 @@ public @interface ConvertWith {
*/
final class UnsetConverter implements Neo4jPersistentPropertyConverter<Object> {
@Override public Value write(Object source) {
@Override public Value write(@Nullable Object source) {
return Values.NULL;
}
@Override public Object read(Value source) {
@Override
@Nullable
public Object read(@Nullable Value source) {
return null;
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.data.neo4j.core.convert;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
@@ -28,10 +27,9 @@ import org.springframework.util.StringUtils;
*/
final class DefaultNeo4jPersistentPropertyConverterFactory implements Neo4jPersistentPropertyConverterFactory {
@Nullable
private final BeanFactory beanFactory;
DefaultNeo4jPersistentPropertyConverterFactory(@Nullable BeanFactory beanFactory) {
DefaultNeo4jPersistentPropertyConverterFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

View File

@@ -16,10 +16,10 @@
package org.springframework.data.neo4j.core.convert;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.springframework.dao.TypeMismatchDataAccessException;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* This service orchestrates a standard Spring conversion service with {@link org.springframework.data.neo4j.core.convert.Neo4jConversions} registered.
@@ -40,7 +40,8 @@ public interface Neo4jConversionService {
* @param <T> The type to be returned
* @return The converted value
*/
@Nullable <T> T convert(Object source, Class<T> targetType);
@Nullable
<T> T convert(Object source, Class<T> targetType);
/**
* Returns whether we have a custom conversion registered to read {@code sourceType} into a native type. The returned
@@ -66,9 +67,7 @@ public interface Neo4jConversionService {
* @throws TypeMismatchDataAccessException In case the value cannot be converted to the target type
*/
@Nullable
Object readValue(
@Nullable Value source, TypeInformation<?> targetType, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride
);
Object readValue(@Nullable Value source, TypeInformation<?> targetType, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride);
/**
* Converts an {@link Object} to a driver's value object.
@@ -77,9 +76,7 @@ public interface Neo4jConversionService {
* @param sourceType The type information describing the target type.
* @return A driver compatible value object.
*/
Value writeValue(
@Nullable Object value, TypeInformation<?> sourceType, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride
);
Value writeValue(@Nullable Object value, TypeInformation<?> sourceType, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride);
/**
* @param type A type that should be checked whether it's simple or not.

View File

@@ -16,8 +16,8 @@
package org.springframework.data.neo4j.core.convert;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.springframework.lang.Nullable;
/**
* This interface represents a pair of methods capable of converting values of type {@code T} to and from {@link Value values}.
@@ -41,5 +41,5 @@ public interface Neo4jPersistentPropertyConverter<T> {
* @param source The value to read, never null or {@link org.neo4j.driver.Values#NULL}
* @return The converted value, maybe null if {@code source} was equals to {@link org.neo4j.driver.Values#NULL}.
*/
@Nullable T read(Value source);
@Nullable T read(@Nullable Value source);
}

View File

@@ -18,8 +18,8 @@ package org.springframework.data.neo4j.core.convert;
import java.util.Map;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.springframework.lang.Nullable;
/**
* You need to provide an implementation of this interface in case you want to store a property of an entity as separate

View File

@@ -23,12 +23,12 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.IsoDuration;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.lang.Nullable;
/**
* This generic converter has been introduced to augment the {@link TemporalAmountAdapter} with the type information passed
@@ -53,14 +53,14 @@ final class TemporalAmountConverter implements GenericConverter {
}
@Override
@Nullable
public Object convert(@Nullable Object value, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (TemporalAmount.class.isAssignableFrom(sourceType.getType())) {
return Values.value(value);
}
boolean valueIsLiteralNullOrNullValue = value == null || value == Values.NULL;
Object convertedValue = valueIsLiteralNullOrNullValue ? null : adapter.apply(((Value) value).asIsoDuration());
Object convertedValue = value == null || value == Values.NULL ? null : adapter.apply(((Value) value).asIsoDuration());
if (convertedValue instanceof IsoDuration && isZero((IsoDuration) convertedValue)) {
if (Period.class.isAssignableFrom(targetType.getType())) {

View File

@@ -4,7 +4,7 @@
converters.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core.convert;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -22,7 +22,6 @@ import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate;
import org.springframework.lang.NonNull;
/**
* The {@link CreateRelationshipStatementHolder} holds the Cypher Statement to create a relationship as well as the optional
@@ -40,7 +39,7 @@ public final class CreateRelationshipStatementHolder {
private final Statement statement;
private final Map<String, Object> properties;
CreateRelationshipStatementHolder(@NonNull Statement statement, @NonNull Map<String, Object> properties) {
CreateRelationshipStatementHolder(Statement statement, Map<String, Object> properties) {
this.statement = statement;
this.properties = properties;
}

View File

@@ -31,6 +31,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
@@ -39,6 +40,7 @@ import java.util.regex.Pattern;
import javax.lang.model.SourceVersion;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
@@ -55,6 +57,7 @@ import org.neo4j.cypherdsl.core.SortItem;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.StatementBuilder;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingWithoutWhere;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingUpdate;
import org.neo4j.cypherdsl.core.SymbolicName;
import org.neo4j.cypherdsl.core.renderer.Configuration;
@@ -63,8 +66,6 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -131,15 +132,14 @@ public enum CypherGenerator {
* @param condition Optional conditions to add
* @return An ongoing match
*/
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription,
@Nullable Condition condition) {
@SuppressWarnings("deprecation")
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription, @Nullable Condition condition) {
Node rootNode = createRootNode(nodeDescription);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(rootNode.getRequiredSymbolicName());
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
//noinspection deprecation
expressions.add(rootNode.internalId().as(Constants.NAME_OF_INTERNAL_ID));
}
expressions.add(elementIdOrIdFunction.apply(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
@@ -148,11 +148,11 @@ public enum CypherGenerator {
}
public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription<?> nodeDescription,
@Nullable List<PatternElement> initialMatchOn,
List<PatternElement> initialMatchOn,
@Nullable Condition condition) {
Node rootNode = createRootNode(nodeDescription);
StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(Cypher.collect(elementIdOrIdFunction.apply(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
@@ -163,12 +163,11 @@ public enum CypherGenerator {
}
public StatementBuilder.OngoingReading prepareMatchOf(NodeDescription<?> nodeDescription,
RelationshipDescription relationshipDescription, @Nullable List<PatternElement> initialMatchOn,
@Nullable Condition condition) {
RelationshipDescription relationshipDescription, @Nullable List<PatternElement> initialMatchOn, @Nullable Condition condition) {
Node rootNode = createRootNode(nodeDescription);
StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
Node targetNode = node(relationshipDescription.getTarget().getPrimaryLabel(),
relationshipDescription.getTarget().getAdditionalLabels())
@@ -202,7 +201,6 @@ public enum CypherGenerator {
.with(expressions.toArray(IdentifiableElement[]::new));
}
@NonNull
public Node createRootNode(NodeDescription<?> nodeDescription) {
String primaryLabel = nodeDescription.getPrimaryLabel();
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
@@ -210,11 +208,11 @@ public enum CypherGenerator {
return node(primaryLabel, additionalLabels).named(Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription));
}
private StatementBuilder.OngoingReadingWithoutWhere prepareMatchOfRootNode(
private OngoingReadingWithoutWhere prepareMatchOfRootNode(
Node rootNode, @Nullable List<PatternElement> initialMatchOn
) {
StatementBuilder.OngoingReadingWithoutWhere match = null;
OngoingReadingWithoutWhere match = null;
if (initialMatchOn == null || initialMatchOn.isEmpty()) {
match = Cypher.match(rootNode);
} else {
@@ -226,7 +224,7 @@ public enum CypherGenerator {
}
}
}
return match;
return Objects.requireNonNull(match);
}
/**
@@ -239,8 +237,7 @@ public enum CypherGenerator {
*/
public Statement createStatementReturningDynamicLabels(NodeDescription<?> nodeDescription) {
IdDescription idDescription = nodeDescription.getIdDescription();
Assert.notNull(idDescription, "Cannot load specific nodes by id without a corresponding attribute");
IdDescription idDescription = Objects.requireNonNull(nodeDescription.getIdDescription(), "Cannot load specific nodes by id without a corresponding attribute");
final Node rootNode = createRootNode(nodeDescription);
@@ -290,7 +287,7 @@ public enum CypherGenerator {
Neo4jPersistentProperty property = (Neo4jPersistentProperty) idProperty;
Condition result = Cypher.noCondition();
for (String key : property.getOptionalConverter().write(null).keys()) {
for (String key : Objects.requireNonNull(property.getOptionalConverter()).write(null).keys()) {
Property expression = Cypher.property(containerName, key);
result = result.and(expression.isEqualTo(actualParameter.property(key)));
}
@@ -319,7 +316,7 @@ public enum CypherGenerator {
return bs.returning(rootNode).build();
};
if (!idDescription.isInternallyGeneratedId()) {
if (idDescription != null && !idDescription.isInternallyGeneratedId()) {
GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity<?>) nodeDescription).getRequiredIdProperty();
if (((Neo4jPersistentEntity<?>) nodeDescription).hasVersionProperty()) {
@@ -403,6 +400,7 @@ public enum CypherGenerator {
}
}
@SuppressWarnings("deprecation")
public Statement prepareSaveOfMultipleInstancesOf(NodeDescription<?> nodeDescription) {
Assert.isTrue(!nodeDescription.isUsingInternalIds(),
@@ -413,7 +411,7 @@ public enum CypherGenerator {
IdDescription idDescription = nodeDescription.getIdDescription();
@SuppressWarnings("ConstantConditions") // We now already that the node is using internal ids, and as such, an IdDescription must be present
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
String nameOfIdProperty = Optional.ofNullable(idDescription).flatMap(IdDescription::getOptionalGraphPropertyName)
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property"));
List<Expression> expressions = new ArrayList<>();
@@ -431,9 +429,8 @@ public enum CypherGenerator {
.build();
}
@NonNull
public Statement prepareSaveOfRelationship(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, @Nullable String dynamicRelationshipType, boolean canUseElementId) {
RelationshipDescription relationship, String dynamicRelationshipType, boolean canUseElementId) {
final Node startNode = neo4jPersistentEntity.isUsingInternalIds()
? anyNode(START_NODE_NAME)
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
@@ -457,7 +454,8 @@ public enum CypherGenerator {
.build();
}
private static Function<Node, Expression> getNodeIdFunction(@Nullable Neo4jPersistentEntity<?> entity, boolean canUseElementId) {
@SuppressWarnings("deprecation")
private static Function<Node, Expression> getNodeIdFunction(Neo4jPersistentEntity<?> entity, boolean canUseElementId) {
Function<Node, Expression> startNodeIdFunction;
var idProperty = entity.getRequiredIdProperty();
@@ -473,7 +471,8 @@ public enum CypherGenerator {
return startNodeIdFunction;
}
private static Function<Node, Expression> getEndNodeIdFunction(@Nullable Neo4jPersistentEntity<?> entity, boolean canUseElementId) {
@SuppressWarnings("deprecation")
private static Function<Node, Expression> getEndNodeIdFunction(Neo4jPersistentEntity<?> entity, boolean canUseElementId) {
Function<Node, Expression> startNodeIdFunction;
if (entity == null) {
@@ -505,7 +504,6 @@ public enum CypherGenerator {
return result;
}
@NonNull
public Statement prepareSaveOfRelationships(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, @Nullable String dynamicRelationshipType, boolean canUseElementId) {
@@ -518,7 +516,7 @@ public enum CypherGenerator {
String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType();
Relationship relationshipFragment = (relationship.isOutgoing() ?
startNode.relationshipTo(endNode, type) :
startNode.relationshipTo(endNode, type) : // CypherDSL is fine with a null type
startNode.relationshipFrom(endNode, type)).named(RELATIONSHIP_NAME);
String row = "relationship";
@@ -534,7 +532,6 @@ public enum CypherGenerator {
.build();
}
@NonNull
public Statement prepareSaveOfRelationshipWithProperties(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship,
boolean isNew,
@@ -582,7 +579,6 @@ public enum CypherGenerator {
.build();
}
@NonNull
public Statement prepareUpdateOfRelationshipsWithProperties(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, boolean isNew, boolean canUseElementId) {
@@ -638,7 +634,6 @@ public enum CypherGenerator {
return result;
}
@NonNull
public Statement prepareDeleteOf(
Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationshipDescription,
@@ -683,13 +678,14 @@ public enum CypherGenerator {
* @param sort The {@link Sort sort} that should be turned into a valid Cypher {@code ORDER}-clause
* @return An optional order clause. Will be {@literal null} on sorts that are {@literal null} or unsorted.
*/
public @Nullable String createOrderByFragment(@Nullable Sort sort) {
@Nullable
public String createOrderByFragment(@Nullable Sort sort) {
if (sort == null || sort.isUnsorted()) {
return null;
}
Statement statement = match(anyNode()).returning("n")
.orderBy(sort.stream().filter(Objects::nonNull).map(order -> {
.orderBy(sort.stream().map(order -> {
String property = order.getProperty().trim();
Expression expression;
if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) {
@@ -711,12 +707,12 @@ public enum CypherGenerator {
try {
Assert.isTrue(SourceVersion.isIdentifier(property), "Name must be a valid identifier.");
expression = Cypher.name(property);
} catch (IllegalArgumentException e) {
if (e.getMessage().endsWith(".")) {
throw new IllegalArgumentException(
e.getMessage().substring(0, e.getMessage().length() - 1));
} catch (IllegalArgumentException ex) {
var msg = Optional.ofNullable(ex.getMessage()).orElse("");
if (msg.endsWith(".")) {
throw new IllegalArgumentException(msg.substring(0, msg.length() - 1));
}
throw e;
throw ex;
}
}
if (order.isIgnoreCase()) {
@@ -750,7 +746,6 @@ public enum CypherGenerator {
nodeDescription,
Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription),
includeField,
null,
processedRelationships));
Collections.addAll(returnContent, additionalExpressions);
return returnContent;
@@ -767,18 +762,18 @@ public enum CypherGenerator {
}
public StatementBuilder.OngoingReading prepareFindOf(NodeDescription<?> nodeDescription, @Nullable List<PatternElement> initialMatchOn, @Nullable Condition condition) {
public StatementBuilder.OngoingReading prepareFindOf(NodeDescription<?> nodeDescription, List<PatternElement> initialMatchOn, @Nullable Condition condition) {
var rootNode = createRootNode(nodeDescription);
return prepareMatchOfRootNode(rootNode, initialMatchOn).where(conditionOrNoCondition(condition));
}
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, SymbolicName nodeName,
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships, Expression... additionalExpressions) {
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable List<RelationshipDescription> processedRelationships) {
Collection<RelationshipDescription> relationships = ((DefaultNeo4jPersistentEntity<?>) nodeDescription).getRelationshipsInHierarchy(includedProperties, parentPath);
relationships.removeIf(r -> !includedProperties.test(parentPath.append(r.getFieldName())));
List<Object> propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, relationshipDescription, includedProperties);
List<Object> propertiesProjection = projectNodeProperties(parentPath, nodeDescription, nodeName, includedProperties);
List<Object> contentOfProjection = new ArrayList<>(propertiesProjection);
contentOfProjection.addAll(generateListsFor(parentPath, nodeDescription, relationships, nodeName, includedProperties, processedRelationships));
@@ -790,8 +785,9 @@ public enum CypherGenerator {
* this list can also contain two "keys" in a row. The {@link MapProjection} will take care to handle them as
* self-reflecting fields. Example with self-reflection and explicit value: {@code n {.id, name: n.name}}.
*/
@SuppressWarnings("deprecation")
private List<Object> projectNodeProperties(PropertyFilter.RelaxedPropertyPath parentPath, NodeDescription<?> nodeDescription, SymbolicName nodeName,
@Nullable RelationshipDescription relationshipDescription, Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
List<Object> nodePropertiesProjection = new ArrayList<>();
Node node = anyNode(nodeName);
@@ -827,7 +823,6 @@ public enum CypherGenerator {
nodePropertiesProjection.add(Cypher.labels(node));
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID);
//noinspection deprecation
nodePropertiesProjection.add(node.internalId());
}
nodePropertiesProjection.add(Constants.NAME_OF_ELEMENT_ID);
@@ -839,9 +834,10 @@ public enum CypherGenerator {
* @see CypherGenerator#projectNodeProperties
*/
private List<Object> generateListsFor(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, Collection<RelationshipDescription> relationships, SymbolicName nodeName,
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, List<RelationshipDescription> processedRelationships) {
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable List<RelationshipDescription> processedRelationships) {
List<Object> mapProjectionLists = new ArrayList<>();
List<RelationshipDescription> processed = Objects.requireNonNullElseGet(processedRelationships, ArrayList::new);
for (RelationshipDescription relationshipDescription : relationships) {
@@ -850,11 +846,11 @@ public enum CypherGenerator {
// if we already processed the other way before, do not try to jump in the infinite loop
// unless it is a root node relationship
if (relationshipDescription.hasRelationshipObverse()
&& processedRelationships.contains(relationshipDescription.getRelationshipObverse())) {
&& processed.contains(relationshipDescription.getRelationshipObverse())) {
continue;
}
generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processedRelationships, fieldName, mapProjectionLists, includedProperties);
generateListFor(parentPath, nodeDescription, relationshipDescription, nodeName, processed, fieldName, mapProjectionLists, includedProperties);
}
return mapProjectionLists;
@@ -876,10 +872,15 @@ public enum CypherGenerator {
Neo4jPersistentEntity<?> endNodeDescription = (Neo4jPersistentEntity<?>) relationshipDescription.getTarget();
processedRelationships.add(relationshipDescription);
PropertyFilter.RelaxedPropertyPath newParentPath = relationshipDescription.hasRelationshipProperties()
? parentPath.append(relationshipDescription.getFieldName()).append(((Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity())
.getPersistentProperty(TargetNode.class).getFieldName())
: parentPath.append(relationshipDescription.getFieldName());
PropertyFilter.RelaxedPropertyPath newParentPath;
newParentPath = parentPath.append(relationshipDescription.getFieldName());
if (relationshipDescription.hasRelationshipProperties()) {
var persistentProperty = ((Neo4jPersistentEntity<?>) relationshipDescription.getRequiredRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class);
if (persistentProperty != null) {
newParentPath = newParentPath
.append(persistentProperty.getFieldName());
}
}
if (relationshipDescription.isDynamic()) {
Relationship relationship = relationshipDescription.isOutgoing()
@@ -888,7 +889,7 @@ public enum CypherGenerator {
relationship = relationship.named(relationshipTargetName);
MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName,
includedProperties, relationshipDescription, new ArrayList<>(processedRelationships));
includedProperties, new ArrayList<>(processedRelationships));
if (relationshipDescription.hasRelationshipProperties()) {
relationship = relationship.named(relationshipSymbolicName);
@@ -906,7 +907,7 @@ public enum CypherGenerator {
: startNode.relationshipFrom(endNode, relationshipType);
MapProjection mapProjection = projectPropertiesAndRelationships(newParentPath, endNodeDescription, relationshipFieldName,
includedProperties, relationshipDescription, new ArrayList<>(processedRelationships));
includedProperties, new ArrayList<>(processedRelationships));
if (relationshipDescription.hasRelationshipProperties()) {
relationship = relationship.named(relationshipSymbolicName);

View File

@@ -16,10 +16,12 @@
package org.springframework.data.neo4j.core.mapping;
import java.util.Collection;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.springframework.core.CollectionFactory;
@@ -32,7 +34,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
@@ -68,13 +69,12 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService {
@Override
@Nullable
public Object readValue(@Nullable Value source, TypeInformation<?> targetType,
@Nullable Neo4jPersistentPropertyConverter<?> conversionOverride) {
public Object readValue(@Nullable Value source, TypeInformation<?> targetType, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride) {
BiFunction<Value, Class<?>, Object> conversion;
boolean applyConversionToCompleteCollection = false;
if (conversionOverride == null) {
conversion = (v, t) -> conversionService.convert(v, t);
conversion = conversionService::convert;
} else {
applyConversionToCompleteCollection = conversionOverride instanceof NullSafeNeo4jPersistentPropertyConverter
&& ((NullSafeNeo4jPersistentPropertyConverter<?>) conversionOverride).isForCollection();
@@ -94,8 +94,10 @@ final class DefaultNeo4jConversionService implements Neo4jConversionService {
Class<?> rawType = type.getType();
if (!valueIsLiteralNullOrNullValue && isCollection(type) && !applyConversionToCompleteCollection) {
// value can't be null at this point in time
@SuppressWarnings("NullAway")
Collection<Object> target = CollectionFactory
.createCollection(rawType, type.getComponentType().getType(), value.size());
.createCollection(rawType, Objects.requireNonNull(type.getComponentType()).getType(), value.size());
value.values()
.forEach(element -> target.add(conversion.apply(element, type.getComponentType().getType())));
return target;

View File

@@ -23,6 +23,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -35,6 +36,8 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
@@ -57,8 +60,6 @@ import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -107,18 +108,20 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
}
@Override
@Nullable
public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
knownObjects.nextRecord();
labelNodeCache.clear();
@SuppressWarnings("unchecked") // ¯\_(ツ)_/¯
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity<R>) nodeDescriptionStore.getNodeDescription(targetType);
Neo4jPersistentEntity<R> rootNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity<R>) nodeDescriptionStore.getNodeDescription(targetType), () -> "Can't read an entity of type %s without description".formatted(targetType));
MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription, true);
if (queryRoot == null) {
throw new IllegalStateException("No query root");
}
try {
return queryRoot == null ? null : map(queryRoot, queryRoot, rootNodeDescription);
return map(queryRoot, queryRoot, rootNodeDescription);
} catch (Exception e) {
throw new MappingException("Error mapping " + mapAccessor, e);
}
@@ -229,10 +232,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
@Override
public void write(Object source, Map<String, Object> parameters) {
Map<String, Object> properties = new HashMap<>();
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionStore
.getNodeDescription(source.getClass());
if (nodeDescription == null) {
return;
}
Map<String, Object> properties = new HashMap<>();
if (nodeDescription.hasRelationshipPropertyPersistTypeInfoFlag()) {
// add type info when write to the database
properties.put(Constants.NAME_OF_RELATIONSHIP_TYPE, nodeDescription.getPrimaryLabel());
@@ -275,7 +283,6 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
Neo4jPersistentProperty vectorProperty = nodeDescription.getRequiredVectorProperty();
parameters.put(Constants.NAME_OF_VECTOR_PROPERTY, vectorProperty.getPropertyName());
parameters.put(Constants.NAME_OF_VECTOR_VALUE, conversionService.writeValue(propertyAccessor.getProperty(vectorProperty), vectorProperty.getTypeInformation(), vectorProperty.getOptionalConverter()));
return;
}
}
@@ -287,8 +294,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
* @param record Record that should be merged
* @return A map accessor combining a {@link Node} and an arbitrary record
*/
@SuppressWarnings("deprecation")
private static MapAccessor mergeRootNodeWithRecord(Node node, MapAccessor record) {
Map<String, Object> mergedAttributes = new HashMap<>(node.size() + record.size() + 1);
Map<String, @Nullable Object> mergedAttributes = new HashMap<>(node.size() + record.size() + 1);
mergedAttributes.put(Constants.NAME_OF_INTERNAL_ID, IdentitySupport.getInternalId(node));
mergedAttributes.put(Constants.NAME_OF_ELEMENT_ID, node.elementId());
@@ -312,8 +320,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
return map(queryResult, nodeDescription, nodeDescription, null, null, relationshipsFromResult, nodesFromResult);
}
@SuppressWarnings("unchecked")
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, NodeDescription<?> genericTargetNodeDescription,
@Nullable Object lastMappedEntity, @Nullable RelationshipDescription relationshipDescription, Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
@Nullable Object lastMappedEntity, @Nullable RelationshipDescription relationshipDescription, @Nullable Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
// prior to SDN 7 local `getInternalId` didn't check relationships, so in that case, they have never been a known
// object. The centralized methods checks those too now. The condition is to recreate the old behaviour without
@@ -339,7 +348,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
populateProperties(queryResult, (Neo4jPersistentEntity<ET>) genericTargetNodeDescription, nodeDescription, internalId, instance, lastMappedEntity, relationshipsFromResult, nodesFromResult, false);
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(getMostCurrentInstance(internalId, instance));
var mostCurrentInstance = Objects.requireNonNull(getMostCurrentInstance(internalId, instance), "Could not get the most current instance for the internal id %s".formatted(internalId));
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(mostCurrentInstance);
ET bean = propertyAccessor.getBean();
bean = eventSupport.maybeCallAfterConvert(bean, concreteNodeDescription, queryResult);
@@ -367,7 +377,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
populateProperties(queryResult, (Neo4jPersistentEntity<ET>) genericTargetNodeDescription, nodeDescription, internalId, mappedObject, lastMappedEntity, relationshipsFromResult, nodesFromResult, true);
}
// due to a needed side effect in `populateProperties`, the entity might have been changed
return getMostCurrentInstance(internalId, mappedObject);
return Objects.requireNonNull(getMostCurrentInstance(internalId, mappedObject), "Could not get mapped instance for internal id %s".formatted(internalId));
}
private boolean hasMoreFields(Map<String, Object> currentQueryResult, Set<Map<String, Object>> savedQueryResults) {
@@ -384,35 +394,37 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
return !currentFields.isEmpty();
}
@SuppressWarnings("unchecked")
@Nullable
private <ET> ET getMostCurrentInstance(String internalId, ET fallbackInstance) {
return (ET) (knownObjects.getObject(internalId) != null ? knownObjects.getObject(internalId) : fallbackInstance);
private <ET> ET getMostCurrentInstance(@Nullable String internalId, @Nullable ET fallbackInstance) {
return (ET) (internalId != null && knownObjects.getObject(internalId) != null ? knownObjects.getObject(internalId) : fallbackInstance);
}
private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity<ET> baseNodeDescription, Neo4jPersistentEntity<ET> moreConcreteNodeDescription, String internalId,
private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEntity<ET> baseNodeDescription, Neo4jPersistentEntity<ET> moreConcreteNodeDescription, @Nullable String internalId,
ET mappedObject, @Nullable Object lastMappedEntity,
Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult, boolean objectAlreadyMapped) {
@Nullable Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult, boolean objectAlreadyMapped) {
List<String> allLabels = getLabels(queryResult, moreConcreteNodeDescription);
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
.deriveConcreteNodeDescription(moreConcreteNodeDescription, allLabels);
@SuppressWarnings("unchecked")
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
.getNodeDescription();
Neo4jPersistentEntity<ET> concreteNodeDescription = Objects.requireNonNull((Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
.getNodeDescription(), "Couldn't find required node description");
if (!concreteNodeDescription.requiresPropertyPopulation()) {
return;
}
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(mappedObject);
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
.getInstanceCreatorMetadata()::isCreatorParameter;
Predicate<Neo4jPersistentProperty> isConstructorParameter = parameter -> {
var metadata = concreteNodeDescription.getInstanceCreatorMetadata();
return metadata != null && metadata.isCreatorParameter(parameter);
};
boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType());
// Fill simple properties
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
PropertyHandler<@NonNull Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType, objectAlreadyMapped);
PropertyHandlerSupport.of(concreteNodeDescription).doWithProperties(handler);
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
@@ -422,10 +434,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
knownObjects.mappedWithQueryResult(internalId, queryResult);
AssociationHandlerSupport.of(concreteNodeDescription).doWithAssociations(
populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult, internalId));
populateFrom(queryResult, baseNodeDescription, propertyAccessor, isConstructorParameter, objectAlreadyMapped, relationshipsFromResult, nodesFromResult));
}
@NonNull
private Neo4jPersistentEntity<?> getMostConcreteTargetNodeDescription(
Neo4jPersistentEntity<?> genericTargetNodeDescription, MapAccessor possibleValueNode) {
@@ -444,18 +455,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
* @param queryResult The complete query result
* @return The list of labels defined by the query variable {@link Constants#NAME_OF_LABELS}.
*/
@NonNull
private List<String> getLabels(MapAccessor queryResult, @Nullable NodeDescription<?> nodeDescription) {
Value labelsValue = queryResult.get(Constants.NAME_OF_LABELS);
List<String> labels = new ArrayList<>();
if (!labelsValue.isNull()) {
labels = labelsValue.asList(Value::asString);
} else if (queryResult instanceof Node) {
Node nodeRepresentation = (Node) queryResult;
} else if (queryResult instanceof Node nodeRepresentation) {
nodeRepresentation.labels().forEach(labels::add);
} else if (queryResult instanceof Relationship) {
Value value = queryResult.get(Constants.NAME_OF_RELATIONSHIP_TYPE);
if (value.isNull()) {
if (value.isNull() && nodeDescription != null) {
labels.addAll(nodeDescription.getStaticLabels());
} else {
labels.add(value.asString());
@@ -484,14 +493,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, NodeDescription<?> genericNodeDescription, MapAccessor values,
Collection<String> surplusLabels, @Nullable Object lastMappedEntity,
Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
@Nullable Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
ParameterValueProvider<@NonNull Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<>() {
@SuppressWarnings("unchecked") // Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives
@SuppressWarnings("unchecked")
// Needed for the last cast. It's easier that way than using the parameter type info and checking for primitives
@Override
public <T> T getParameterValue(Parameter<T, Neo4jPersistentProperty> parameter) {
Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(parameter.getName());
@Nullable
public <T> T getParameterValue(Parameter<T, @NonNull Neo4jPersistentProperty> parameter) {
Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available"));
Object result;
if (matchingProperty.isRelationship()) {
@@ -499,7 +510,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
.filter(r -> {
String propertyFieldName = matchingProperty.getFieldName();
return r.getFieldName().equals(propertyFieldName);
}).findFirst().get();
}).findFirst().orElseThrow();
// If we cannot find any value it does not mean that there isn't any.
// The result set might contain associations not named CONCRETE_TYPE_TARGET but ABSTRACT_TYPE_TARGET.
// For this we bubble up the hierarchy of NodeDescriptions.
@@ -522,7 +533,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
} else if (matchingProperty.isEntityWithRelationshipProperties()) {
result = lastMappedEntity;
} else {
result = conversionService.readValue(extractValueOf(matchingProperty, values), parameter.getType(), matchingProperty.getOptionalConverter());
result = conversionService.readValue(extractValueOf(matchingProperty, values), parameter.getType(), matchingProperty.getOptionalConverter());
}
return (T) result;
}
@@ -531,7 +542,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
return entityInstantiators.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider);
}
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
private PropertyHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<String> surplusLabels, @Nullable Object targetNode, boolean ownerIsKotlinType, boolean objectAlreadyMapped) {
@@ -561,15 +572,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
};
}
@Nullable
private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawType, @Nullable Object value) {
private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawType, Object value) {
return value == null && !ownerIsKotlinType && rawType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(rawType) : value;
}
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, NodeDescription<?> baseDescription,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
boolean objectAlreadyMapped, Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult, String internalId) {
private AssociationHandler<@NonNull Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, NodeDescription<?> baseDescription,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
boolean objectAlreadyMapped, @Nullable Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult
) {
return association -> {
@@ -590,41 +601,35 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
Object propertyValue = propertyAccessor.getProperty(persistentProperty);
boolean propertyValueNotNull = propertyValue != null;
if (propertyValue != null) {
boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike()
&& propertyValueNotNull
&& !((Collection<?>) propertyValue).isEmpty();
boolean populatedCollection = objectAlreadyMapped && persistentProperty.isCollectionLike() && !((Collection<?>) propertyValue).isEmpty();
boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap() && !((Map<?, ?>) propertyValue).isEmpty();
boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() && !persistentProperty.isMap();
boolean populatedMap = objectAlreadyMapped && persistentProperty.isMap()
&& propertyValueNotNull
&& !((Map<?, ?>) propertyValue).isEmpty();
if (populatedCollection) {
createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false)
.ifPresent(value -> {
Collection<?> providedCollection = (Collection<?>) value;
Collection<?> existingValue = (Collection<?>) propertyValue;
Collection<Object> newValue = CollectionFactory.createCollection(existingValue.getClass(), providedCollection.size() + existingValue.size());
boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty.isCollectionLike() && !persistentProperty.isMap()
&& propertyValueNotNull;
RelationshipDescription relationshipDescription = (RelationshipDescription) association;
Map<Object, Object> mergedValues = new HashMap<>();
mergeCollections(relationshipDescription, existingValue, mergedValues);
mergeCollections(relationshipDescription, providedCollection, mergedValues);
if (populatedCollection) {
createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult, false)
.ifPresent(value -> {
Collection<?> providedCollection = (Collection<?>) value;
Collection<?> existingValue = (Collection<?>) propertyValue;
Collection<Object> newValue = CollectionFactory.createCollection(existingValue.getClass(), providedCollection.size() + existingValue.size());
newValue.addAll(mergedValues.values());
propertyAccessor.setProperty(persistentProperty, newValue);
});
}
RelationshipDescription relationshipDescription = (RelationshipDescription) association;
Map<Object, Object> mergedValues = new HashMap<>();
mergeCollections(relationshipDescription, existingValue, mergedValues);
mergeCollections(relationshipDescription, providedCollection, mergedValues);
boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue;
newValue.addAll(mergedValues.values());
propertyAccessor.setProperty(persistentProperty, newValue);
});
}
boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue;
// avoid unnecessary re-assignment of values
if (propertyAlreadyPopulated) {
return;
// avoid unnecessary re-assignment of values
if (propertyAlreadyPopulated) {
return;
}
}
createInstanceOfRelationships(persistentProperty, queryResult, (RelationshipDescription) association, baseDescription, relationshipsFromResult, nodesFromResult)
@@ -636,15 +641,17 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
private void mergeCollections(RelationshipDescription relationshipDescription, Collection<?> values, Map<Object, Object> mergedValues) {
for (Object existingValueInCollection : values) {
if (relationshipDescription.hasRelationshipProperties()) {
Object existingIdPropertyValue = ((Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity())
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRequiredRelationshipPropertiesEntity();
Object existingIdPropertyValue = relationshipPropertiesEntity
.getPropertyAccessor(existingValueInCollection)
.getProperty(((Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity()).getIdProperty());
.getProperty(relationshipPropertiesEntity.getRequiredIdProperty());
mergedValues.put(existingIdPropertyValue, existingValueInCollection);
} else if (!relationshipDescription.isDynamic()) { // should not happen because this is all inside populatedCollection (but better safe than sorry)
Object existingIdPropertyValue = ((Neo4jPersistentEntity<?>) relationshipDescription.getTarget())
Neo4jPersistentEntity<?> target = (Neo4jPersistentEntity<?>) relationshipDescription.getTarget();
Object existingIdPropertyValue = target
.getPropertyAccessor(existingValueInCollection)
.getProperty(((Neo4jPersistentEntity<?>) relationshipDescription.getTarget()).getIdProperty());
.getProperty(target.getRequiredIdProperty());
mergedValues.put(existingIdPropertyValue, existingValueInCollection);
}
@@ -652,14 +659,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
}
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values,
RelationshipDescription relationshipDescription, NodeDescription<?> baseDescription, Collection<Relationship> relationshipsFromResult,
RelationshipDescription relationshipDescription, NodeDescription<?> baseDescription, @Nullable Collection<Relationship> relationshipsFromResult,
Collection<Node> nodesFromResult) {
return createInstanceOfRelationships(persistentProperty, values, relationshipDescription, baseDescription, relationshipsFromResult, nodesFromResult, true);
}
@SuppressWarnings("deprecation")
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values,
RelationshipDescription relationshipDescription, NodeDescription<?> baseDescription, Collection<Relationship> relationshipsFromResult,
Collection<Node> nodesFromResult, boolean fetchMore) {
RelationshipDescription relationshipDescription, NodeDescription<?> baseDescription, @Nullable Collection<Relationship> relationshipsFromResult,
Collection<Node> nodesFromResult, boolean fetchMore) {
String typeOfRelationship = relationshipDescription.getType();
String targetLabel = relationshipDescription.getTarget().getPrimaryLabel();
@@ -672,8 +680,9 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
BiConsumer<String, Object> mappedObjectHandler;
Function<String, ?> keyTransformer;
if (persistentProperty.isDynamicAssociation() && persistentProperty.getComponentType().isEnum()) {
keyTransformer = f -> conversionService.convert(f, persistentProperty.getComponentType());
Class<?> componentType = persistentProperty.getComponentType();
if (persistentProperty.isDynamicAssociation() && (componentType != null && componentType.isEnum())) {
keyTransformer = f -> conversionService.convert(f, componentType);
} else {
keyTransformer = Function.identity();
}
@@ -747,7 +756,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
// and therefor should not be in the list.
// Otherwise, for highly linked data it could potentially cause a StackOverflowError.
String direction = relationshipDescription.getDirection().name();
if (knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) {
if (relationshipsFromResult != null && knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) {
relationshipsFromResult.remove(possibleRelationship);
}
// If the target is the same(equal) node, get the related object from the cache.
@@ -769,7 +778,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
if (relationshipDescription.hasRelationshipProperties()) {
Object relationshipProperties;
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRequiredRelationshipPropertiesEntity();
if (fetchMore) {
relationshipProperties = map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult);
} else {
@@ -813,7 +822,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
.asRelationship();
Object relationshipProperties;
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationshipDescription.getRequiredRelationshipPropertiesEntity();
if (fetchMore) {
relationshipProperties = map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult);
} else {
@@ -834,7 +843,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
if (persistentProperty.getTypeInformation().isCollectionLike()) {
List<Object> returnedValues = relationshipDescription.hasRelationshipProperties() ? relationshipsAndProperties : value;
Collection<Object> target = CollectionFactory.createCollection(persistentProperty.getRawType(), persistentProperty.getComponentType(), returnedValues.size());
Collection<Object> target = CollectionFactory.createCollection(persistentProperty.getRawType(), componentType, returnedValues.size());
target.addAll(returnedValues);
return Optional.of(target);
} else {
@@ -874,14 +883,14 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
return allNodesInResult;
}
private Collection<Relationship> extractMatchingRelationships(Collection<Relationship> relationshipsFromResult,
private Collection<Relationship> extractMatchingRelationships(@Nullable Collection<Relationship> relationshipsFromResult,
RelationshipDescription relationshipDescription, String typeOfRelationship,
Predicate<Relationship> relationshipPredicate) {
Predicate<Relationship> onlyWithMatchingType = r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic();
return relationshipsFromResult.stream()
return (relationshipsFromResult != null) ? relationshipsFromResult.stream()
.filter(onlyWithMatchingType.and(relationshipPredicate))
.collect(Collectors.toList());
.collect(Collectors.toList()) : List.of();
}
private Collection<Relationship> extractRelationships(MapAccessor allValues) {
@@ -898,6 +907,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
return allRelationshipsInResult;
}
@SuppressWarnings("deprecation")
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
if (property.isInternalIdProperty()) {
if (Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(property.getType())) {
@@ -974,7 +984,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
}
}
private boolean isInCreation(@Nullable String internalId) {
private boolean isInCreation(String internalId) {
if (internalId == null) {
return false;
}
@@ -1037,7 +1047,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
read.lock();
return previousRecords.contains(internalId) || internalCurrentRecord.get(internalId);
return previousRecords.contains(internalId) || Optional.ofNullable(internalCurrentRecord.get(internalId)).orElse(Boolean.FALSE);
} finally {
read.unlock();
@@ -1074,20 +1084,25 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
internalCurrentRecord.clear();
}
private void mappedWithQueryResult(String internalId, MapAccessor queryResult) {
try {
write.lock();
mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet())
.add(queryResult.asMap());
} finally {
write.unlock();
private void mappedWithQueryResult(@Nullable String internalId, MapAccessor queryResult) {
if (internalId != null) {
try {
write.lock();
mappedQueryResults.computeIfAbsent(internalId, id -> ConcurrentHashMap.newKeySet())
.add(queryResult.asMap());
} finally {
write.unlock();
}
}
}
private Set<Map<String, Object>> getQueryResultsFor(String internalId) {
private Set<Map<String, Object>> getQueryResultsFor(@Nullable String internalId) {
if (internalId == null) {
return Set.of();
}
try {
read.lock();
return mappedQueryResults.get(internalId);
return Objects.requireNonNullElseGet(mappedQueryResults.get(internalId), Set::of);
} finally {
read.unlock();
}

View File

@@ -15,12 +15,12 @@
*/
package org.springframework.data.neo4j.core.mapping;
import java.util.Objects;
import java.util.function.Function;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogAccessor;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -53,7 +53,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
Assert.notNull(entityMetaData, "Entity meta data must not be null");
IdDescription idDescription = entityMetaData.getIdDescription();
IdDescription idDescription = Objects.requireNonNull(entityMetaData.getIdDescription(), () -> "Cannot determine id description for entity %s".formatted(entityMetaData.getType()));
Class<?> valueType = entityMetaData.getRequiredIdProperty().getType();
if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) {
@@ -84,7 +84,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
private final Class<?> valueType;
private @Nullable final Function<Object, Object> valueLookup;
private final Function<Object, Object> valueLookup;
private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class<?> valueType,
Function<Object, Object> valueLookup) {
@@ -104,7 +104,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
if (idDescription.isInternallyGeneratedId()) {
boolean isNew;
if (value != null && valueType.isPrimitive() && Number.class.isInstance(value)) {
if (value != null && valueType.isPrimitive() && value instanceof Number) {
isNew = ((Number) value).longValue() < 0;
} else {
isNew = value == null;
@@ -118,7 +118,7 @@ final class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
return value == null;
}
if (Number.class.isInstance(value)) {
if (value instanceof Number) {
return ((Number) value).longValue() == 0;
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
@@ -32,6 +33,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.log.LogAccessor;
import org.springframework.data.annotation.Persistent;
@@ -49,8 +52,6 @@ import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -85,6 +86,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
private final Lazy<Boolean> isRelationshipPropertiesEntity;
@Nullable
private NodeDescription<?> parentNodeDescription;
private List<NodeDescription<?>> childNodeDescriptionsInHierarchy;
@@ -255,14 +257,14 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
private void verifyDynamicAssociations() {
Set<Class> targetEntities = new HashSet<>();
AssociationHandlerSupport.of(this).doWithAssociations((Association<Neo4jPersistentProperty> association) -> {
Set<Class<?>> targetEntities = new HashSet<>();
AssociationHandlerSupport.of(this).doWithAssociations((Association<@NonNull Neo4jPersistentProperty> association) -> {
Neo4jPersistentProperty inverse = association.getInverse();
if (inverse.isDynamicAssociation()) {
Relationship relationship = inverse.findAnnotation(Relationship.class);
Assert.state(relationship == null || relationship.type().isEmpty(),
() -> "Dynamic relationships cannot be used with a fixed type; omit @Relationship or use @Relationship(direction = "
+ relationship.direction().name() + ") without a type in " + this.getUnderlyingClass() + " on field "
+ Optional.ofNullable(relationship).map(Relationship::direction).orElse(Relationship.Direction.OUTGOING).name() + ") without a type in " + this.getUnderlyingClass() + " on field "
+ inverse.getFieldName());
Assert.state(!targetEntities.contains(inverse.getAssociationTargetType()),
@@ -329,7 +331,6 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
* @param type the type of the underlying class
* @return computed primary label
*/
@Nullable
static String computePrimaryLabel(Class<?> type) {
Node nodeAnnotation = AnnotatedElementUtils.findMergedAnnotation(type, Node.class);
@@ -365,7 +366,6 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
*
* @return computed additional labels of the concrete class
*/
@NonNull
private List<String> computeOwnAdditionalLabels() {
List<String> result = new ArrayList<>();
@@ -398,7 +398,6 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
return Collections.unmodifiableList(result);
}
@NonNull
private List<String> computeParentLabels() {
List<String> parentLabels = new ArrayList<>();
Neo4jPersistentEntity<?> parentNodeDescriptionCalculated = (Neo4jPersistentEntity<?>) parentNodeDescription;
@@ -430,9 +429,10 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
@Override
public boolean hasVectorProperty() {
return Optional.ofNullable(getVectorProperty()).map(v -> true).orElse(false);
return getVectorProperty() != null;
}
@Nullable
public Neo4jPersistentProperty getVectorProperty() {
return this.vectorProperty.getNullable();
}
@@ -511,13 +511,11 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
return Collections.unmodifiableCollection(relationships);
}
@NonNull
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter) {
return getRelationshipsInHierarchy(propertyFilter, PropertyFilter.RelaxedPropertyPath.withRootType(this.getUnderlyingClass()));
}
@NonNull
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyFilter, PropertyFilter.RelaxedPropertyPath path) {
Collection<RelationshipDescription> relationships = new HashSet<>(getRelationships());
@@ -599,7 +597,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
}
@Override
public void setParentNodeDescription(NodeDescription<?> parent) {
public void setParentNodeDescription(@Nullable NodeDescription<?> parent) {
this.parentNodeDescription = parent;
}
@@ -634,9 +632,14 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
visitedNodes.add(targetNode);
// we don't care about the other content of relationship properties and jump straight into the `TargetNode`
String relationshipPropertiesPrefix = relationship.hasRelationshipProperties()
? "." + ((Neo4jPersistentEntity<?>) relationship.getRelationshipPropertiesEntity()).getPersistentProperty(TargetNode.class).getFieldName()
: "";
String relationshipPropertiesPrefix;
if (!relationship.hasRelationshipProperties()) {
relationshipPropertiesPrefix = "";
} else {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationship.getRequiredRelationshipPropertiesEntity();
var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType()));
relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName();
}
PropertyFilter.RelaxedPropertyPath nextPath = relaxedPropertyPath.append(relationship.getFieldName() + relationshipPropertiesPrefix);
if (calculatePossibleCircles(targetNode, visitedNodes, includeField, nextPath)) {
return true;
@@ -661,8 +664,14 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
// but don't (!) add them to the visitedNodes yet.
// Otherwise, the same "parallel" defined target nodes will report a false circle.
branchedVisitedNodes.add(targetNode);
String relationshipPropertiesPrefix = relationship.hasRelationshipProperties() ? "." + ((Neo4jPersistentEntity<?>) relationship.getRelationshipPropertiesEntity())
.getPersistentProperty(TargetNode.class).getFieldName() : "";
String relationshipPropertiesPrefix;
if (!relationship.hasRelationshipProperties()) {
relationshipPropertiesPrefix = "";
} else {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationship.getRequiredRelationshipPropertiesEntity();
var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType()));
relationshipPropertiesPrefix = "." + targetNodeProperty.getFieldName();
}
if (calculatePossibleCircles(targetNode, branchedVisitedNodes, includeField, path.append(relationship.getFieldName() + relationshipPropertiesPrefix))) {
return true;
}

View File

@@ -18,8 +18,11 @@ package org.springframework.data.neo4j.core.mapping;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.mapping.Association;
@@ -37,8 +40,6 @@ import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -63,7 +64,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
private final Lazy<Neo4jPersistentPropertyConverter<?>> customConversion;
private final @Nullable PersistentPropertyCharacteristics optionalCharacteristics;
@Nullable
private final PersistentPropertyCharacteristics optionalCharacteristics;
/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
@@ -110,7 +112,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
}
@Override
protected Association<Neo4jPersistentProperty> createAssociation() {
protected Association<@NonNull Neo4jPersistentProperty> createAssociation() {
Neo4jPersistentEntity<?> obverseOwner;
@@ -120,31 +122,29 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
if (this.hasActualTypeAnnotation(RelationshipProperties.class)) {
TypeInformation<?> typeInformation = getRelationshipPropertiesTargetType(getActualType());
obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).get();
relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())).get();
obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow();
relationshipPropertiesClass = this.mappingContext.addPersistentEntity(TypeInformation.of(getActualType())).orElseThrow();
} else {
Class<?> associationTargetType = this.getAssociationTargetType();
Class<?> associationTargetType = Objects.requireNonNull(this.getAssociationTargetType());
obverseOwner = this.mappingContext.addPersistentEntity(TypeInformation.of(associationTargetType)).orElse(null);
Assert.notNull(obverseOwner, "Obverse owner could not be added");
if (dynamicAssociation) {
TypeInformation<?> mapValueType = this.getTypeInformation().getMapValueType();
TypeInformation<?> mapValueType = Objects.requireNonNull(this.getTypeInformation().getMapValueType());
TypeInformation<?> componentType = mapValueType.getComponentType();
if (componentType != null) {
TypeInformation<?> actualType = mapValueType.getActualType();
boolean relationshipPropertiesCollection =
this.mappingContext.getPersistentEntity(mapValueType.getActualType().getType())
.isRelationshipPropertiesEntity();
if (actualType != null && this.mappingContext.getRequiredPersistentEntity(actualType.getType())
.isRelationshipPropertiesEntity()) {
TypeInformation<?> typeInformation = getRelationshipPropertiesTargetType(actualType.getType());
obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).orElseThrow();
relationshipPropertiesClass = this.mappingContext
.addPersistentEntity(componentType).orElseThrow();
boolean relationshipPropertiesScalar =
mapValueType.getType().isAnnotationPresent(RelationshipProperties.class);
if (relationshipPropertiesCollection) {
TypeInformation<?> typeInformation = getRelationshipPropertiesTargetType(mapValueType.getActualType().getType());
obverseOwner = this.mappingContext.addPersistentEntity(typeInformation).get();
relationshipPropertiesClass = this.mappingContext
.addPersistentEntity(mapValueType.getComponentType()).get();
} else if (relationshipPropertiesScalar) {
relationshipPropertiesClass = this.mappingContext.addPersistentEntity(mapValueType.getComponentType()).get();
} else if (mapValueType.getType().isAnnotationPresent(RelationshipProperties.class)) {
relationshipPropertiesClass = this.mappingContext.addPersistentEntity(componentType).orElseThrow();
}
}
}
}
@@ -164,6 +164,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
// Try to determine if there is a relationship definition that expresses logically the same relationship
// on the other end.
// At this point, obverseOwner can't be null
@SuppressWarnings("NullAway")
Optional<RelationshipDescription> obverseRelationshipDescription = obverseOwner.getRelationships().stream()
.filter(rel -> rel.getType().equals(type)
&& rel.getTarget().equals(this.getOwner())
@@ -180,7 +182,6 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
return relationshipDescription;
}
@NonNull
private TypeInformation<?> getRelationshipPropertiesTargetType(Class<?> relationshipPropertiesType) {
Field targetNodeField = ReflectionUtils.findField(relationshipPropertiesType,
@@ -190,8 +191,8 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
throw new MappingException("Missing @TargetNode declaration in " + relationshipPropertiesType);
}
TypeInformation<?> relationshipPropertiesTypeInformation = TypeInformation.of(relationshipPropertiesType);
Class<?> type = relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName()).getType();
if (Object.class == type && this.getField().getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) {
Class<?> type = Objects.requireNonNull(relationshipPropertiesTypeInformation.getProperty(targetNodeField.getName())).getType();
if (Object.class == type && this.getRequiredField().getGenericType() instanceof ParameterizedType pt && pt.getActualTypeArguments().length == 1) {
return TypeInformation.of(ResolvableType.forType(pt.getActualTypeArguments()[0]));
}
return TypeInformation.of(type);
@@ -231,6 +232,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
}
@Override
@Nullable
public Neo4jPersistentPropertyConverter<?> getOptionalConverter() {
return isEntity() ? null : customConversion.getOptional()
.map(Neo4jPersistentPropertyConverter.class::cast)
@@ -253,8 +255,7 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp
.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class);
String targetName = this.getName();
if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty()
&& propertyAnnotation.name().trim().length() != 0) {
if (propertyAnnotation != null && !propertyAnnotation.name().trim().isEmpty()) {
targetName = propertyAnnotation.name().trim();
}

View File

@@ -17,16 +17,17 @@ package org.springframework.data.neo4j.core.mapping;
import java.util.Objects;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.Association;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
* @author Gerrit Meier
* @since 6.0
*/
final class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty> implements RelationshipDescription {
final class DefaultRelationshipDescription extends Association<@NonNull Neo4jPersistentProperty> implements RelationshipDescription {
private final String type;
@@ -40,8 +41,10 @@ final class DefaultRelationshipDescription extends Association<Neo4jPersistentPr
private final Relationship.Direction direction;
@Nullable
private final NodeDescription<?> relationshipPropertiesClass;
@Nullable
private RelationshipDescription relationshipObverse;
private final boolean cascadeUpdates;
@@ -97,6 +100,7 @@ final class DefaultRelationshipDescription extends Association<Neo4jPersistentPr
}
@Override
@Nullable
public NodeDescription<?> getRelationshipPropertiesEntity() {
return relationshipPropertiesClass;
}
@@ -107,11 +111,12 @@ final class DefaultRelationshipDescription extends Association<Neo4jPersistentPr
}
@Override
public void setRelationshipObverse(RelationshipDescription relationshipObverse) {
public void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse) {
this.relationshipObverse = relationshipObverse;
}
@Override
@Nullable
public RelationshipDescription getRelationshipObverse() {
return relationshipObverse;
}

View File

@@ -21,6 +21,8 @@ import java.util.function.Function;
import org.apache.commons.logging.LogFactory;
import org.apiguardian.api.API;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
@@ -35,7 +37,6 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -73,8 +74,7 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
Neo4jPersistentEntity<?> sourceEntity = context.getRequiredPersistentEntity(entityInstance.getClass());
PersistentPropertyAccessor<Object> sourceAccessor = sourceEntity.getPropertyAccessor(entityInstance);
Neo4jPersistentEntity<?> targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)).orElse(null);
Assert.notNull(targetEntity, "Target entity could not be created for a DTO");
Neo4jPersistentEntity<?> targetEntity = context.addPersistentEntity(TypeInformation.of(targetType)).orElseThrow(() -> new IllegalStateException("Target entity could not be created for a DTO"));
InstanceCreatorMetadata<?> creator = targetEntity.getInstanceCreatorMetadata();
Object dto = context.getInstantiatorFor(targetEntity)
@@ -98,7 +98,6 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
return dto;
}
@Nullable
Object getPropertyValueDirectlyFor(PersistentProperty<?> targetProperty, PersistentEntity<?, ?> sourceEntity,
PersistentPropertyAccessor<?> sourceAccessor) {
@@ -111,18 +110,15 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
}
Object result = sourceAccessor.getProperty(sourceProperty);
if (targetProperty.isEntity() && !targetProperty.getTypeInformation().isAssignableFrom(sourceProperty.getTypeInformation())) {
if (result != null && targetProperty.isEntity() && !targetProperty.getTypeInformation().isAssignableFrom(sourceProperty.getTypeInformation())) {
return new DtoInstantiatingConverter(targetProperty.getType(), this.context).convertDirectly(result);
}
return result;
}
@Override
public Object convert(@Nullable EntityInstanceWithSource entityInstanceAndSource) {
if (entityInstanceAndSource == null) {
return null;
}
@Nullable
public Object convert(EntityInstanceWithSource entityInstanceAndSource) {
Object entityInstance = entityInstanceAndSource.getEntityInstance();
if (targetType.isInterface() || targetType.isInstance(entityInstance)) {
@@ -135,7 +131,7 @@ public final class DtoInstantiatingConverter implements Converter<EntityInstance
Neo4jPersistentEntity<?> targetEntity = context.addPersistentEntity(TypeInformation.of(targetType))
.orElseThrow(() -> new MappingException(
"Could not add a persistent entity for the projection target type '" + targetType.getName() + "'"));
InstanceCreatorMetadata<? extends PersistentProperty<?>> creator = targetEntity.getInstanceCreatorMetadata();
InstanceCreatorMetadata<@NonNull ? extends PersistentProperty<?>> creator = targetEntity.getInstanceCreatorMetadata();
Object dto = context.getInstantiatorFor(targetEntity)
.createInstance(targetEntity,

View File

@@ -17,9 +17,11 @@ package org.springframework.data.neo4j.core.mapping;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.InstanceCreatorMetadata;
@@ -33,7 +35,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -65,6 +66,7 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
}
@Override
@Nullable
public T convert(Object dtoInstance) {
if (dtoInstance == null) {
@@ -72,18 +74,19 @@ public final class EntityFromDtoInstantiatingConverter<T> implements Converter<O
}
PersistentEntity<?, ?> sourceEntity = context.addPersistentEntity(TypeInformation.of(dtoInstance.getClass()))
.get();
.orElseThrow();
PersistentPropertyAccessor<Object> sourceAccessor = sourceEntity.getPropertyAccessor(dtoInstance);
PersistentEntity<?, ?> targetEntity = context.getPersistentEntity(targetEntityType);
InstanceCreatorMetadata<?> creator = targetEntity.getInstanceCreatorMetadata();
PersistentEntity<?, ?> targetEntity = Objects.requireNonNull(context.getPersistentEntity(targetEntityType));
InstanceCreatorMetadata<?> creator = Objects.requireNonNull(targetEntity.getInstanceCreatorMetadata());
@SuppressWarnings({ "rawtypes", "unchecked" })
T entity = (T) context.getInstantiatorFor(targetEntity)
.createInstance(targetEntity, new ParameterValueProvider() {
@Override
@Nullable
public Object getParameterValue(Parameter parameter) {
PersistentProperty<?> targetProperty = targetEntity.getPersistentProperty(parameter.getName());
PersistentProperty<?> targetProperty = targetEntity.getPersistentProperty(Objects.requireNonNull(parameter.getName(), "Parameter names are not available"));
if (targetProperty == null) {
throw new MappingException("Cannot map constructor parameter " + parameter.getName()
+ " to a property of class " + targetEntityType);

View File

@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.core.mapping;
import java.util.Optional;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.Node;
@@ -25,7 +26,6 @@ import org.neo4j.cypherdsl.core.SymbolicName;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -40,17 +40,20 @@ public final class IdDescription {
/**
* The class representing a generator for new ids or null for assigned ids.
*/
private @Nullable final Class<? extends IdGenerator<?>> idGeneratorClass;
@Nullable
private final Class<? extends IdGenerator<?>> idGeneratorClass;
/**
* A reference to an ID generator.
*/
private @Nullable final String idGeneratorRef;
@Nullable
private final String idGeneratorRef;
/**
* The property that stores the id if applicable.
*/
private @Nullable final String graphPropertyName;
@Nullable
private final String graphPropertyName;
private final boolean isDeprecated;
private final Lazy<Expression> idExpression;
@@ -70,8 +73,8 @@ public final class IdDescription {
}
public static IdDescription forExternallyGeneratedIds(SymbolicName symbolicName,
@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef, String graphPropertyName) {
Class<? extends IdGenerator<?>> idGeneratorClass,
String idGeneratorRef, String graphPropertyName) {
Assert.notNull(graphPropertyName, "Graph property name is required");
try {
@@ -87,8 +90,9 @@ public final class IdDescription {
}
}
@SuppressWarnings("deprecation")
private IdDescription(SymbolicName symbolicName, @Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) {
@Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) {
this.idGeneratorClass = idGeneratorClass;
this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef;
@@ -98,7 +102,6 @@ public final class IdDescription {
this.idExpression = Lazy.of(() -> {
final Node rootNode = Cypher.anyNode(symbolicName);
if (this.isInternallyGeneratedId()) {
//noinspection deprecation
return isDeprecated ? rootNode.internalId() : rootNode.elementId();
} else {
return this.getOptionalGraphPropertyName()
@@ -118,14 +121,14 @@ public final class IdDescription {
* @param nodeName use this name as the symbolic name of the node in the query
* @return An expression that represents the right identifier type.
*/
@SuppressWarnings("deprecation")
public Expression asIdExpression(String nodeName) {
final Node rootNode = Cypher.anyNode(nodeName);
if (this.isInternallyGeneratedId()) {
//noinspection deprecation
return isDeprecated ? rootNode.internalId() : rootNode.elementId();
} else {
return this.getOptionalGraphPropertyName()
.map(propertyName -> Cypher.property(nodeName, propertyName)).get();
.map(propertyName -> Cypher.property(nodeName, propertyName)).orElseThrow();
}
}

View File

@@ -20,14 +20,13 @@ import static org.apiguardian.api.API.Status.INTERNAL;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.Entity;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* This class is <strong>not</strong> part of any public API and will be changed without further notice as needed. It's
@@ -61,7 +60,7 @@ public final class IdentitySupport {
* @return An internal id
*/
@Nullable
public static String getElementId(@NonNull MapAccessor row) {
public static String getElementId(MapAccessor row) {
if (row instanceof Entity entity) {
return getElementId(entity);
}
@@ -77,9 +76,10 @@ public final class IdentitySupport {
return value.asString();
}
@Nullable
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public static Long getInternalId(@NonNull MapAccessor row) {
@Nullable
public static Long getInternalId(MapAccessor row) {
if (row instanceof Entity entity) {
return entity.id();
}
@@ -93,7 +93,7 @@ public final class IdentitySupport {
}
@Nullable
public static String getPrefixedElementId(@NonNull MapAccessor queryResult, @Nullable String seed) {
public static String getPrefixedElementId(MapAccessor queryResult, @Nullable String seed) {
if (queryResult instanceof Node) {
return "N" + getElementId(queryResult);
} else if (queryResult instanceof Relationship) {

View File

@@ -27,13 +27,13 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
import org.neo4j.driver.types.Type;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
@@ -174,10 +174,10 @@ public final class MappingSupport {
Object relationshipPropertiesValue = entityHolder.getRelationshipProperties();
Neo4jPersistentEntity<?> persistentEntity =
neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass());
Objects.requireNonNull(neo4jMappingContext.getPersistentEntity(relationshipPropertiesValue.getClass()));
PersistentPropertyAccessor<Object> relationshipPropertiesAccessor = persistentEntity.getPropertyAccessor(relationshipPropertiesValue);
relationshipPropertiesAccessor.setProperty(persistentEntity.getPersistentProperty(TargetNode.class), newRelationshipObject);
relationshipPropertiesAccessor.setProperty(Objects.requireNonNull(persistentEntity.getPersistentProperty(TargetNode.class)), newRelationshipObject);
newRelationshipObject = relationshipPropertiesAccessor.getBean();
// If we recreate or manipulate the object including it's accessor, we must update it in the holder as well.

View File

@@ -31,12 +31,14 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.beans.BeanUtils;
@@ -67,7 +69,6 @@ import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.PostLoad;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
@@ -109,11 +110,12 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
private final Neo4jConversionService conversionService;
private final Map<Neo4jPersistentEntity, Set<MethodHolder>> postLoadMethods = new ConcurrentHashMap<>();
private final Map<Neo4jPersistentEntity<?>, Set<MethodHolder>> postLoadMethods = new ConcurrentHashMap<>();
private EventSupport eventSupport;
private @Nullable AutowireCapableBeanFactory beanFactory;
@Nullable
private AutowireCapableBeanFactory beanFactory;
private boolean strict = false;
@@ -127,7 +129,6 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
private Neo4jConversions neo4jConversions;
@Nullable
private TypeSystem typeSystem;
@Nullable
@@ -139,25 +140,25 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
private Builder(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem, @Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
this.neo4jConversions = neo4jConversions;
this.typeSystem = typeSystem;
this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault);
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
}
@SuppressWarnings("HiddenField")
public Builder withNeo4jConversions(@Nullable Neo4jConversions neo4jConversions) {
public Builder withNeo4jConversions(Neo4jConversions neo4jConversions) {
this.neo4jConversions = neo4jConversions;
return this;
}
@SuppressWarnings("HiddenField")
public Builder withPersistentPropertyCharacteristicsProvider(@Nullable PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
public Builder withPersistentPropertyCharacteristicsProvider(PersistentPropertyCharacteristicsProvider persistentPropertyCharacteristicsProvider) {
this.persistentPropertyCharacteristicsProvider = persistentPropertyCharacteristicsProvider;
return this;
}
@SuppressWarnings("HiddenField")
public Builder withTypeSystem(@Nullable TypeSystem typeSystem) {
this.typeSystem = typeSystem;
public Builder withTypeSystem(TypeSystem typeSystem) {
this.typeSystem = Objects.requireNonNullElseGet(typeSystem, TypeSystem::getDefault);
return this;
}
@@ -180,20 +181,6 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
this(new Builder(neo4jConversions, null, null));
}
/**
* This API is primarily used from inside the CDI extension to configure the type system. This is necessary as
* we don't get notified of the context via {@link #setApplicationContext(ApplicationContext applicationContext)}.
*
* @param neo4jConversions The conversions to be used
* @param typeSystem The current drivers type system. If this is null, we use the default one without accessing the driver.
* @deprecated Use {@link Neo4jMappingContext#builder()}
*/
@API(status = API.Status.INTERNAL, since = "6.0")
@Deprecated(since = "6.3.7", forRemoval = true)
public Neo4jMappingContext(Neo4jConversions neo4jConversions, @Nullable TypeSystem typeSystem) {
this(new Builder(neo4jConversions, typeSystem, null));
}
private Neo4jMappingContext(Builder builder) {
this.conversionService = new DefaultNeo4jConversionService(builder.neo4jConversions);
@@ -254,9 +241,9 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
if (!newEntity.describesInterface()) {
if (this.nodeDescriptionStore.containsKey(primaryLabel)) {
Neo4jPersistentEntity existingEntity = (Neo4jPersistentEntity) this.nodeDescriptionStore.get(
Neo4jPersistentEntity<?> existingEntity = (Neo4jPersistentEntity<?>) this.nodeDescriptionStore.get(
primaryLabel);
if (!existingEntity.getTypeInformation().getRawTypeInformation()
if (existingEntity != null && !existingEntity.getTypeInformation().getRawTypeInformation()
.equals(typeInformation.getRawTypeInformation())) {
String message = String.format(Locale.ENGLISH,
"The schema already contains a node description under the primary label %s", primaryLabel);
@@ -318,7 +305,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
return newEntity;
}
private static boolean isValidParentNode(@Nullable Class<?> parentClass) {
private static boolean isValidParentNode(Class<?> parentClass) {
if (parentClass == null || parentClass.equals(Object.class)) {
return false;
}
@@ -355,6 +342,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
}
@Override
@Nullable
public NodeDescription<?> getNodeDescription(Class<?> underlyingClass) {
return doGetPersistentEntity(underlyingClass);
}
@@ -418,7 +406,12 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
if (this.beanFactory == null) {
idGenerator = BeanUtils.instantiateClass(t);
} else {
idGenerator = this.beanFactory.getBeanProvider(t).getIfUnique(() -> this.beanFactory.createBean(t));
idGenerator = this.beanFactory.getBeanProvider(t).getIfUnique(() -> {
// The beanFactory can't actually be reassigned, so doing a whole double lock check is a bit overkill
@SuppressWarnings("NullAway")
var result = this.beanFactory.createBean(t);
return result;
});
}
return idGenerator;
}
@@ -510,10 +503,10 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
} else {
converterClass = customConverter.getClass();
}
Map<String, Type> typeVariableMap = GenericTypeResolver.getTypeVariableMap(converterClass)
Map<String, Type> typeVariableMap = (converterClass != null) ? GenericTypeResolver.getTypeVariableMap(converterClass)
.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue));
.collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)) : Map.of();
Type propertyType = null;
if (typeVariableMap.containsKey("T")) {
propertyType = typeVariableMap.get("T");

View File

@@ -19,6 +19,7 @@ import java.util.Optional;
import java.util.Set;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.MutablePersistentEntity;
@@ -76,8 +77,21 @@ public interface Neo4jPersistentEntity<T>
return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType());
}
/**
* @return true if this entity spots a vector property
*/
boolean hasVectorProperty();
/**
* Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise {@literal null}.
* @return an optional vector property on this entity
*/
@Nullable
Neo4jPersistentProperty getVectorProperty();
/**
* Will return the single supported vector property if {@link #hasVectorProperty()} returns {@literal true}, otherwise it will throw an {@link IllegalStateException}.
* @return the vector property on this entity.
*/
Neo4jPersistentProperty getRequiredVectorProperty();
}

View File

@@ -18,12 +18,12 @@ package org.springframework.data.neo4j.core.mapping;
import java.util.Optional;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Vector;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
import org.springframework.data.neo4j.core.schema.CompositeProperty;
import org.springframework.data.neo4j.core.schema.DynamicLabels;
import org.springframework.lang.Nullable;
/**
* A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related
@@ -45,7 +45,8 @@ public interface Neo4jPersistentProperty extends PersistentProperty<Neo4jPersist
*/
default boolean isDynamicAssociation() {
return isRelationship() && isMap() && (getComponentType() == String.class || getComponentType().isEnum());
Class<?> componentType = getComponentType();
return isRelationship() && isMap() && (componentType == String.class || (componentType != null && componentType.isEnum()));
}
/**

View File

@@ -20,14 +20,16 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apiguardian.api.API;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.lang.Nullable;
/**
* Working on nested relationships happens in a certain algorithmic context. This context enables a tight cohesion
@@ -42,6 +44,7 @@ import org.springframework.lang.Nullable;
@API(status = API.Status.INTERNAL, since = "6.0")
public final class NestedRelationshipContext {
private final Neo4jPersistentProperty inverse;
@Nullable
private final Object value;
private final RelationshipDescription relationship;
@@ -82,10 +85,9 @@ public final class NestedRelationshipContext {
public Object identifyAndExtractRelationshipTargetNode(Object relatedValue) {
Object valueToBeSaved = relatedValue;
if (relatedValue instanceof Map.Entry) {
Map.Entry<?, ?> relatedValueMapEntry = (Map.Entry<?, ?>) relatedValue;
if (relatedValue instanceof Map.Entry<?, ?> relatedValueMapEntry) {
if (this.hasRelationshipWithProperties()) {
Object mapValue = ((Map.Entry<?, ?>) relatedValue).getValue();
Object mapValue = relatedValueMapEntry.getValue();
// it can be either a scalar entity holder or a list of it
mapValue = mapValue instanceof List ? ((List<?>) mapValue).get(0) : mapValue;
valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) mapValue).getRelatedEntity();
@@ -100,7 +102,8 @@ public final class NestedRelationshipContext {
return valueToBeSaved;
}
public @Nullable PersistentPropertyAccessor<?> getRelationshipPropertiesPropertyAccessor(@Nullable Object relatedValue) {
@Nullable
public PersistentPropertyAccessor<?> getRelationshipPropertiesPropertyAccessor(Object relatedValue) {
if (!this.hasRelationshipWithProperties() || relatedValue == null) {
return null;
@@ -115,7 +118,7 @@ public final class NestedRelationshipContext {
}
}
public static NestedRelationshipContext of(Association<Neo4jPersistentProperty> handler,
public static NestedRelationshipContext of(Association<@NonNull Neo4jPersistentProperty> handler,
PersistentPropertyAccessor<?> propertyAccessor, Neo4jPersistentEntity<?> neo4jPersistentEntity) {
Neo4jPersistentProperty inverse = handler.getInverse();
@@ -130,7 +133,7 @@ public final class NestedRelationshipContext {
neo4jPersistentEntity.getName() + " does not define a relationship for " + inverse.getFieldName()));
if (relationship.hasRelationshipProperties() && value != null) {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationship.getRelationshipPropertiesEntity();
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationship.getRequiredRelationshipPropertiesEntity();
// If this is dynamic relationship (Map<Object, Object>), extract the keys as relationship names
// and the map values as values.
@@ -187,7 +190,8 @@ public final class NestedRelationshipContext {
private static Object getTargetNode(Neo4jPersistentEntity<?> relationshipPropertiesEntity, Object object) {
PersistentPropertyAccessor<Object> propertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(object);
return propertyAccessor.getProperty(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class));
var targetNodeProperty = Objects.requireNonNull(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class), () -> "Could not get target node property on %s".formatted(relationshipPropertiesEntity.getType()));
return Objects.requireNonNull(propertyAccessor.getProperty(targetNodeProperty));
}
}

View File

@@ -24,9 +24,9 @@ import java.util.Set;
import java.util.concurrent.locks.StampedLock;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
@@ -101,7 +101,7 @@ public final class NestedRelationshipProcessingStateMachine {
* @param valuesToStore Check whether all the values in the collection have been processed
* @return The state of things processed
*/
public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, Collection<?> valuesToStore) {
if (fromId == null) {
return ProcessState.PROCESSED_BOTH;
}
@@ -139,7 +139,7 @@ public final class NestedRelationshipProcessingStateMachine {
private record RelationshipIdUpdateContext(Statement cypher, Object fromId, Object toId,
NestedRelationshipContext relationshipContext,
Object relatedValueToStore, Neo4jPersistentProperty idProperty) {
Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) {
}
/**
@@ -147,7 +147,7 @@ public final class NestedRelationshipProcessingStateMachine {
*/
@FunctionalInterface
public interface RelationshipIdSupplier {
Optional<Object> getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId);
Optional<Object> getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId);
}
/**
@@ -155,7 +155,7 @@ public final class NestedRelationshipProcessingStateMachine {
*/
@FunctionalInterface
public interface ReactiveRelationshipIdSupplier {
Mono<Object> getId(Statement statement, Neo4jPersistentProperty idProperty, Object fromId, Object toId);
Mono<Object> getId(Statement statement, @Nullable Neo4jPersistentProperty idProperty, Object fromId, Object toId);
}
/**
@@ -163,8 +163,8 @@ public final class NestedRelationshipProcessingStateMachine {
*
* @param relationshipDescription To be marked as processed
*/
public void markRelationshipAsProcessed(Object fromId, @Nullable RelationshipDescription relationshipDescription) {
if (relationshipDescription == null) {
public void markRelationshipAsProcessed(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) {
if (fromId == null || relationshipDescription == null) {
return;
}
@@ -218,16 +218,20 @@ public final class NestedRelationshipProcessingStateMachine {
if (!processed && mappingContext.hasPersistentEntityFor(typeOfValue)) {
Neo4jPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeOfValue);
Neo4jPersistentProperty idProperty = entity.getIdProperty();
Object id = idProperty == null ? null : entity.getPropertyAccessor(valueToCheck).getProperty(idProperty);
Object id;
Optional<Object> alreadyProcessedObject = Optional.empty();
if (idProperty != null) {
// After the lookup by system.identityHashCode failed for a processed object alias,
// we must traverse or iterate over all value with the matching type and compare the domain ids
// to figure out if the logical object has already been processed through a different object instance.
// The type check is needed to avoid relationship ids <> node id conflicts.
id = entity.getPropertyAccessor(valueToCheck).getProperty(idProperty);
alreadyProcessedObject = processedObjectsAlias.values().stream()
.filter(typeOfValue::isInstance)
.filter(processedObject -> id != null && id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty)))
.findAny();
}
// After the lookup by system.identityHashCode failed for a processed object alias,
// we must traverse or iterate over all value with the matching type and compare the domain ids
// to figure out if the logical object has already been processed through a different object instance.
// The type check is needed to avoid relationship ids <> node id conflicts.
Optional<Object> alreadyProcessedObject = id == null ? Optional.empty() : processedObjectsAlias.values().stream()
.filter(typeOfValue::isInstance)
.filter(processedObject -> id.equals(entity.getPropertyAccessor(processedObject).getProperty(idProperty)))
.findAny();
if (alreadyProcessedObject.isPresent()) { // Skip the show the next time around.
processed = true;
Object internalId = getObjectId(alreadyProcessedObject.get());
@@ -249,16 +253,17 @@ public final class NestedRelationshipProcessingStateMachine {
* @param relationshipDescription the relationship that should be looked for in the registry.
* @return processed yes (true) / no (false)
*/
public boolean hasProcessedRelationship(Object fromId, @Nullable RelationshipDescription relationshipDescription) {
if (relationshipDescription != null) {
final long stamp = lock.readLock();
try {
return processedRelationshipDescriptions.contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription));
} finally {
lock.unlock(stamp);
}
public boolean hasProcessedRelationship(@Nullable Object fromId, @Nullable RelationshipDescription relationshipDescription) {
if (fromId == null || relationshipDescription == null) {
return false;
}
final long stamp = lock.readLock();
try {
return processedRelationshipDescriptions.contains(new RelationshipDescriptionWithSourceId(fromId, relationshipDescription));
} finally {
lock.unlock(stamp);
}
return false;
}
public void storeProcessRelationshipEntity(MappingSupport.RelationshipPropertiesWithEntityHolder id, Object source, Object target, RelationshipDescription type) {
@@ -284,7 +289,7 @@ public final class NestedRelationshipProcessingStateMachine {
}
public void requireIdUpdate(Neo4jPersistentEntity<?> sourceEntity, RelationshipDescription relationshipDescription, boolean canUseElementId,
Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, Neo4jPersistentProperty idProperty) {
Object fromId, Object toId, NestedRelationshipContext relationshipContext, Object relatedValueToStore, @Nullable Neo4jPersistentProperty idProperty) {
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.prepareSaveOfRelationshipWithProperties(
sourceEntity, relationshipDescription, false,
@@ -297,24 +302,6 @@ public final class NestedRelationshipProcessingStateMachine {
}
}
public Collection<RelationshipIdUpdateContext> getRequiresIdUpdate() {
final long stamp = lock.readLock();
try {
return Set.copyOf(requiresIdUpdate);
} finally {
lock.unlock(stamp);
}
}
public void markAsUpdated(RelationshipIdUpdateContext context) {
final long stamp = lock.writeLock();
try {
requiresIdUpdate.remove(context);
} finally {
lock.unlock(stamp);
}
}
public void updateRelationshipIds(RelationshipIdSupplier idSupplier) {
final long stamp = lock.writeLock();
try {
@@ -322,10 +309,12 @@ public final class NestedRelationshipProcessingStateMachine {
while (it.hasNext()) {
var requiredIdUpdate = it.next();
idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId()).ifPresent(anId -> {
requiredIdUpdate.relationshipContext()
.getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore())
.setProperty(requiredIdUpdate.idProperty(), anId);
it.remove();
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext()
.getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore());
if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) {
relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), anId);
it.remove();
}
});
}
} finally {
@@ -333,17 +322,19 @@ public final class NestedRelationshipProcessingStateMachine {
}
}
public Mono<Void> updateRelationshipIds(ReactiveRelationshipIdSupplier idSupplier) {
public Mono<Void> updateRelationshipIdsReactive(ReactiveRelationshipIdSupplier idSupplier) {
return Flux.defer(() -> {
final long stamp = lock.writeLock();
return Flux.fromIterable(requiresIdUpdate)
.flatMap(requiredIdUpdate -> Mono.just(requiredIdUpdate).zipWith(idSupplier.getId(requiredIdUpdate.cypher(), requiredIdUpdate.idProperty(), requiredIdUpdate.fromId(), requiredIdUpdate.toId())))
.doOnNext(t -> {
var requiredIdUpdate = t.getT1();
requiredIdUpdate.relationshipContext()
.getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore())
.setProperty(requiredIdUpdate.idProperty(), t.getT2());
requiresIdUpdate.remove(requiredIdUpdate);
PersistentPropertyAccessor<?> relationshipPropertiesPropertyAccessor = requiredIdUpdate.relationshipContext()
.getRelationshipPropertiesPropertyAccessor(requiredIdUpdate.relatedValueToStore());
if (relationshipPropertiesPropertyAccessor != null && requiredIdUpdate.idProperty() != null) {
relationshipPropertiesPropertyAccessor.setProperty(requiredIdUpdate.idProperty(), t.getT2());
requiresIdUpdate.remove(requiredIdUpdate);
}
}).doOnTerminate(() -> lock.unlock(stamp));
}).then();
}
@@ -387,14 +378,13 @@ public final class NestedRelationshipProcessingStateMachine {
}
@Nullable
private Object getProcessedObjectIds(@Nullable Object entity) {
private Object getProcessedObjectIds(Object entity) {
if (entity == null) {
return null;
}
return processedObjectsIds.get(System.identityHashCode(entity));
}
@NonNull
private Object extractRelatedValueFromRelationshipProperties(Object valueToStore) {
Object value;
if (valueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
@@ -424,8 +414,9 @@ public final class NestedRelationshipProcessingStateMachine {
return processedObjectsAlias.containsKey(System.identityHashCode(entity));
}
private boolean hasProcessedAllOf(@Nullable Collection<?> entities) {
private boolean hasProcessedAllOf(Collection<?> entities) {
// there can be null elements in the unified collection of values to store.
//noinspection ConstantValue
if (entities == null) {
return false;
}

View File

@@ -15,11 +15,13 @@
*/
package org.springframework.data.neo4j.core.mapping;
import java.io.Serial;
import java.util.Formattable;
import java.util.Formatter;
import java.util.Locale;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.types.MapAccessor;
import org.springframework.data.mapping.MappingException;
@@ -36,9 +38,15 @@ import org.springframework.data.mapping.MappingException;
@API(status = API.Status.INTERNAL, since = "6.0.2")
public final class NoRootNodeMappingException extends MappingException implements Formattable {
private MapAccessor mapAccessor;
private Neo4jPersistentEntity<?> entity;
@Serial
private static final long serialVersionUID = 5742846435191601546L;
@Nullable
private final transient MapAccessor mapAccessor;
@Nullable
private final transient Neo4jPersistentEntity<?> entity;
@SuppressWarnings("NullableProblems")
public NoRootNodeMappingException(MapAccessor mapAccessor, Neo4jPersistentEntity<?> entity) {
super(String.format("Could not find mappable nodes or relationships inside %s for %s", mapAccessor, entity));
this.mapAccessor = mapAccessor;
@@ -47,9 +55,13 @@ public final class NoRootNodeMappingException extends MappingException implement
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
String className = entity.getUnderlyingClass().getSimpleName();
formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", mapAccessor,
className.substring(0, 1).toLowerCase(
Locale.ROOT), String.join(":", entity.getStaticLabels()));
if (mapAccessor != null && entity != null) {
String className = entity.getUnderlyingClass().getSimpleName();
formatter.format("Could not find mappable nodes or relationships inside %s for %s:%s", mapAccessor,
className.substring(0, 1).toLowerCase(
Locale.ROOT), String.join(":", entity.getStaticLabels()));
} else {
formatter.format("Could not find mappable nodes or relationships inside a record");
}
}
}

View File

@@ -18,12 +18,13 @@ package org.springframework.data.neo4j.core.mapping;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Expression;
import org.springframework.lang.Nullable;
/**
* Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and
@@ -143,14 +144,15 @@ public interface NodeDescription<T> {
*/
default Expression getIdExpression() {
if (this.getIdDescription().getOptionalGraphPropertyName()
var idDescription = Objects.requireNonNull(this.getIdDescription(), "No id description available, cannot compute a Cypher expression for retrieving or storing the id");
if (idDescription.getOptionalGraphPropertyName()
.flatMap(this::getGraphProperty)
.filter(GraphPropertyDescription::isComposite)
.isPresent()) {
throw new IllegalStateException("A composite id property cannot be used as ID expression.");
}
return this.getIdDescription().asIdExpression();
return idDescription.asIdExpression();
}
/**

View File

@@ -23,12 +23,13 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.lang.Nullable;
/**
* This class is more or less just a wrapper around the node description lookup map. It ensures that there is no cyclic
@@ -102,7 +103,7 @@ final class NodeDescriptionStore {
return nodeDescriptionAndLabels.apply(entityDescription, labels);
}
private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription<?> entityDescription, @Nullable List<String> labels) {
private NodeDescriptionAndLabels computeConcreteNodeDescription(NodeDescription<?> entityDescription, List<String> labels) {
boolean isConcreteClassThatFulfillsEverything = !Modifier.isAbstract(entityDescription.getUnderlyingClass().getModifiers()) && entityDescription.getStaticLabels().containsAll(labels);
@@ -148,14 +149,19 @@ final class NodeDescriptionStore {
}
unmatchedLabelsCache.put(nd, unmatchedLabelsCount);
if (mostMatchingNodeDescription == null || unmatchedLabelsCount < unmatchedLabelsCache.get(mostMatchingNodeDescription)) {
if (mostMatchingNodeDescription == null || unmatchedLabelsCount < Objects.requireNonNullElse(unmatchedLabelsCache.get(mostMatchingNodeDescription), Integer.MAX_VALUE)) {
mostMatchingNodeDescription = nd;
mostMatchingStaticLabels = matchingLabels;
}
}
Set<String> surplusLabels = new HashSet<>(labels);
mostMatchingStaticLabels.forEach(surplusLabels::remove);
if (mostMatchingStaticLabels != null) {
mostMatchingStaticLabels.forEach(surplusLabels::remove);
}
if (mostMatchingNodeDescription == null) {
throw new IllegalStateException("Could not compute a concrete node description for entity %s and labels %s".formatted(entityDescription, labels));
}
return new NodeDescriptionAndLabels(mostMatchingNodeDescription, surplusLabels);
}

View File

@@ -15,10 +15,10 @@
*/
package org.springframework.data.neo4j.core.mapping;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
import org.springframework.lang.Nullable;
/**
* All property converters will be wrapped by this class. It adds the information if a converter needs to be applied to
@@ -60,7 +60,8 @@ final class NullSafeNeo4jPersistentPropertyConverter<T> implements Neo4jPersiste
return delegate.write(source);
}
@Override @Nullable
@Override
@Nullable
public T read(@Nullable Value source) {
return source == null || source.isNull() ? null : delegate.read(source);
}

View File

@@ -18,7 +18,7 @@ package org.springframework.data.neo4j.core.mapping;
import static org.apiguardian.api.API.Status.STABLE;
import org.apiguardian.api.API;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
/**
* The characteristics of a {@link Neo4jPersistentProperty} can diverge from what is by default derived from the annotated

View File

@@ -22,8 +22,8 @@ import java.util.Set;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
@@ -48,7 +48,7 @@ public abstract class PropertyFilter {
public abstract boolean isNotFiltering();
static String toDotPath(RelaxedPropertyPath propertyPath, @Nullable String lastSegment) {
static String toDotPath(RelaxedPropertyPath propertyPath, String lastSegment) {
if (lastSegment == null) {
return propertyPath.toDotPath();
@@ -167,7 +167,7 @@ public abstract class PropertyFilter {
return dotPath;
}
public String toDotPath(@Nullable String lastSegment) {
public String toDotPath(String lastSegment) {
if (lastSegment == null) {
return this.toDotPath();

View File

@@ -23,10 +23,10 @@ import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.lang.Nullable;
/**
* A strategy for traversing all properties (including association) once, without going in circles with cyclic mappings.

View File

@@ -15,12 +15,12 @@
*/
package org.springframework.data.neo4j.core.mapping;
import java.util.Objects;
import java.util.Optional;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction is
@@ -92,6 +92,11 @@ public interface RelationshipDescription {
@Nullable
NodeDescription<?> getRelationshipPropertiesEntity();
default NodeDescription<?> getRequiredRelationshipPropertiesEntity() {
return Objects.requireNonNull(getRelationshipPropertiesEntity(), () -> "Relationship entity %s does not point to an entity holding the relationships' properties".formatted(this.getType()));
}
/**
* Tells if this relationship is a relationship with additional properties. In such cases
* {@code getRelationshipPropertiesClass} will return the type of the properties holding class.
@@ -102,7 +107,7 @@ public interface RelationshipDescription {
default boolean hasInternalIdProperty() {
return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity().getIdDescription())
return hasRelationshipProperties() && Optional.ofNullable(getRelationshipPropertiesEntity()).map(NodeDescription::getIdDescription)
.filter(IdDescription::isInternallyGeneratedId).isPresent();
}
@@ -114,7 +119,6 @@ public interface RelationshipDescription {
return Relationship.Direction.INCOMING.equals(this.getDirection());
}
@NonNull
default String generateRelatedNodesCollectionName(NodeDescription<?> mostAbstractNodeDescription) {
return this.getSource().getMostAbstractParentLabel(mostAbstractNodeDescription) + "_" + this.getType() + "_" + this.getTarget().getPrimaryLabel() + "_" + this.isOutgoing();
@@ -125,11 +129,12 @@ public interface RelationshipDescription {
*
* @param relationshipObverse logically same relationship definition in the target entity
*/
void setRelationshipObverse(RelationshipDescription relationshipObverse);
void setRelationshipObverse(@Nullable RelationshipDescription relationshipObverse);
/**
* @return logically same relationship definition in the target entity
*/
@Nullable
RelationshipDescription getRelationshipObverse();
/**

View File

@@ -22,11 +22,11 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.lang.Nullable;
/**
* Contains the descriptions of all nodes, their properties and relationships known to SDN.
@@ -93,7 +93,13 @@ public interface Schema {
throw new UnknownEntityException(targetClass);
}
Neo4jEntityConverter entityConverter = getEntityConverter();
return (typeSystem, record) -> entityConverter.read(targetClass, record);
return (typeSystem, record) -> {
try {
return entityConverter.read(targetClass, record);
} catch (IllegalStateException ex) {
return null;
}
};
}
/**

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.neo4j.core.mapping;
import java.io.Serial;
import org.apiguardian.api.API;
import org.springframework.dao.InvalidDataAccessApiUsageException;
@@ -28,6 +30,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
@API(status = API.Status.STABLE, since = "6.0")
public final class UnknownEntityException extends InvalidDataAccessApiUsageException {
@Serial
private static final long serialVersionUID = -1769937352513022599L;
private final Class<?> targetClass;
public UnknownEntityException(Class<?> targetClass) {

View File

@@ -23,7 +23,6 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.lang.Nullable;
/**
* Utility class that orchestrates {@link EntityCallbacks}. Not to be used outside the framework.
@@ -74,8 +73,7 @@ public final class EventSupport {
this.entityCallbacks = entityCallbacks;
}
@Nullable
public <T> T maybeCallBeforeBind(@Nullable T object) {
public <T> T maybeCallBeforeBind(T object) {
if (object == null) {
return object;
@@ -90,8 +88,7 @@ public final class EventSupport {
* @param <T> Expected type
* @return The instance to which the callback was applied to
*/
@Nullable
public <T> T maybeCallAfterConvert(@Nullable T object, Neo4jPersistentEntity<T> entity, MapAccessor source) {
public <T> T maybeCallAfterConvert(T object, Neo4jPersistentEntity<T> entity, MapAccessor source) {
if (object == null) {
return object;

View File

@@ -15,7 +15,4 @@
* <!-- end::intent[] -->
* @author Michael J. Simons
*/
@NonNullApi
package org.springframework.data.neo4j.core.mapping.callback;
import org.springframework.lang.NonNullApi;

View File

@@ -2,11 +2,11 @@
* <!-- tag::intent[] -->
The main mapping framework. This package orchestrates the reading and writing of entities and all tasks related to it.
The only public API of this package is the subpackage {@literal callback}, containing the event support.
The core package itself has to be considered an internal api and we don't give any guarantees of API stability.
The core package itself has to be considered an internal api, and we don't give any guarantees of API stability.
* <!-- end::intent[] -->
* @author Michael J. Simons
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core.mapping;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -5,7 +5,7 @@
the imperative and reactive variants of the client and the template.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -25,14 +25,18 @@ import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.GenericTypeResolver;
@@ -45,7 +49,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyToMapC
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.schema.CompositeProperty.Phase;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -62,7 +65,7 @@ import org.springframework.util.StringUtils;
* @since 6.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Target({ElementType.FIELD})
@Inherited
@ConvertWith(converterFactory = CompositePropertyConverterFactory.class)
@API(status = API.Status.STABLE, since = "6.0")
@@ -207,7 +210,7 @@ final class CompositePropertyConverter<K, P> implements Neo4jPersistentPropertyC
}
@Override
public Value write(P property) {
public Value write(@Nullable P property) {
Map<K, Value> source = delegate.decompose(property, neo4jConversionService);
Map<String, Object> temp = new HashMap<>();
@@ -216,7 +219,12 @@ final class CompositePropertyConverter<K, P> implements Neo4jPersistentPropertyC
}
@Override
public P read(Value source) {
@Nullable
public P read(@Nullable Value source) {
if (source == null || TypeSystem.getDefault().NULL().isTypeOf(source)) {
return null;
}
Map<K, Value> temp = new HashMap<>();
source.keys().forEach(k -> {
@@ -230,6 +238,7 @@ final class CompositePropertyConverter<K, P> implements Neo4jPersistentPropertyC
/**
* Internally used via reflection.
*
* @return The type of the underlying delegate.
*/
@SuppressWarnings("unused")
@@ -249,7 +258,7 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty
private final BeanFactory beanFactory;
private final Neo4jConversionService conversionServiceDelegate;
CompositePropertyConverterFactory(@Nullable BeanFactory beanFactory, @Nullable Neo4jConversionService conversionServiceDelegate) {
CompositePropertyConverterFactory(BeanFactory beanFactory, Neo4jConversionService conversionServiceDelegate) {
this.beanFactory = beanFactory;
this.conversionServiceDelegate = conversionServiceDelegate;
}
@@ -306,15 +315,16 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty
}
if (persistentProperty.getActualType() != type) {
var typeName = Optional.ofNullable(type).map(Type::getTypeName).orElse("n/a");
throw new IllegalArgumentException(
"The property type `" + typeVariableMap.get(PROPERTY_TYPE_KEY).getTypeName() + "` created by `"
"The property type `" + typeName + "` created by `"
+ delegateClass.getName() + "` " + generateLocation(persistentProperty)
+ " doesn't match the actual property type");
}
componentType = (Class<?>) typeVariableMap.get(KEY_TYPE_KEY);
}
boolean isEnum = componentType.isEnum();
boolean isEnum = componentType != null && componentType.isEnum();
if (!(componentType == String.class || isEnum)) {
throw new IllegalArgumentException("@" + CompositeProperty.class.getSimpleName()
+ " can only be used on Map properties with a key type of String or enum. Was " + generateLocation(
@@ -343,7 +353,7 @@ final class CompositePropertyConverterFactory implements Neo4jPersistentProperty
String prefixWithDelimiter = persistentProperty.computePrefixWithDelimiter();
return new CompositePropertyConverter(
delegate, prefixWithDelimiter, conversionServiceDelegate, componentType, keyWriter, keyReader);
delegate, prefixWithDelimiter, conversionServiceDelegate, Objects.requireNonNull(componentType), keyWriter, keyReader);
}
private static String generateLocation(Neo4jPersistentProperty persistentProperty) {

View File

@@ -4,7 +4,7 @@
*
* @author Michael J. Simons
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core.schema;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -15,8 +15,10 @@
*/
package org.springframework.data.neo4j.core.support;
import java.util.Objects;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.ObjectProvider;
@@ -24,7 +26,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.lang.Nullable;
/**
* Don't use outside SDN code. You have been warned.
@@ -35,18 +36,20 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
private final Supplier<Neo4jBookmarkManager> defaultBookmarkManagerSupplier;
private ObjectProvider<Neo4jBookmarkManager> neo4jBookmarkManagers = new ObjectProvider<Neo4jBookmarkManager>() {
private ObjectProvider<Neo4jBookmarkManager> neo4jBookmarkManagers = new ObjectProvider<>() {
@Override
public Neo4jBookmarkManager getObject(Object... args) throws BeansException {
throw new BeanCreationException("This provider can't create new beans");
}
@Override
@Nullable
public Neo4jBookmarkManager getIfAvailable() throws BeansException {
return null;
}
@Override
@Nullable
public Neo4jBookmarkManager getIfUnique() throws BeansException {
return null;
}
@@ -60,6 +63,7 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
@Nullable
private volatile Neo4jBookmarkManager bookmarkManager;
@Nullable
private ApplicationEventPublisher applicationEventPublisher;
public BookmarkManagerReference(Supplier<Neo4jBookmarkManager> defaultBookmarkManagerSupplier, @Nullable Neo4jBookmarkManager bookmarkManager) {
@@ -73,7 +77,8 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
this.neo4jBookmarkManagers = applicationContext.getBeanProvider(Neo4jBookmarkManager.class);
this.applicationEventPublisher = applicationContext;
if (this.bookmarkManager != null) {
this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher);
Objects.requireNonNull(this.bookmarkManager)
.setApplicationEventPublisher(this.applicationEventPublisher);
}
}
@@ -84,6 +89,7 @@ public final class BookmarkManagerReference implements ApplicationContextAware {
result = this.bookmarkManager;
if (result == null) {
this.bookmarkManager = neo4jBookmarkManagers.getIfAvailable(this.defaultBookmarkManagerSupplier);
//noinspection DataFlowIssue
this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher);
result = this.bookmarkManager;
}

View File

@@ -23,8 +23,10 @@ import java.lang.annotation.Target;
import java.util.Date;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.data.neo4j.core.convert.ConvertWith;
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
@@ -47,12 +49,13 @@ public @interface DateLong {
final class DateLongConverter implements Neo4jPersistentPropertyConverter<Date> {
@Override
public Value write(Date source) {
return Values.value(source.getTime());
public Value write(@Nullable Date source) {
return source == null ? Values.NULL : Values.value(source.getTime());
}
@Override
public Date read(Value source) {
return new Date(source.asLong());
@Nullable
public Date read(@Nullable Value source) {
return source == null || TypeSystem.getDefault().NULL().isTypeOf(source) ? null : new Date(source.asLong());
}
}

View File

@@ -26,8 +26,10 @@ import java.util.Date;
import java.util.TimeZone;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.neo4j.core.convert.ConvertWith;
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
@@ -100,14 +102,15 @@ final class DateStringConverter implements Neo4jPersistentPropertyConverter<Date
}
@Override
public Value write(Date source) {
return Values.value(getFormat().format(source));
public Value write(@Nullable Date source) {
return source == null ? Values.NULL : Values.value(getFormat().format(source));
}
@Override
public Date read(Value source) {
@Nullable
public Date read(@Nullable Value source) {
try {
return getFormat().parse(source.asString());
return source == null || TypeSystem.getDefault().NULL().isTypeOf(source) ? null : getFormat().parse(source.asString());
} catch (ParseException e) {
throw new RuntimeException(e);
}

View File

@@ -15,10 +15,10 @@
*/
package org.springframework.data.neo4j.core.support;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Driver;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.lang.Nullable;
/**
* Representation of a user agent containing sensible information to identify queries generated by or executed via Spring Data Neo4j.

View File

@@ -4,7 +4,7 @@
indicating that some transaction may be retried and additional converters and id generators.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core.support;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -24,9 +24,9 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
/**
* Default bookmark manager.

View File

@@ -20,9 +20,9 @@ import java.util.Set;
import java.util.function.Supplier;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
/**
* Responsible for storing, updating and retrieving the bookmarks of Neo4j's transaction.
@@ -58,7 +58,7 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No
* bookmarks on each call.
* @return A bookmark manager
*/
static Neo4jBookmarkManager create(@Nullable Supplier<Set<Bookmark>> bookmarksSupplier) {
static Neo4jBookmarkManager create(Supplier<Set<Bookmark>> bookmarksSupplier) {
return new DefaultBookmarkManager(bookmarksSupplier);
}
@@ -72,7 +72,7 @@ public sealed interface Neo4jBookmarkManager permits AbstractBookmarkManager, No
* bookmarks on each call.
* @return A reactive bookmark manager
*/
static Neo4jBookmarkManager createReactive(@Nullable Supplier<Set<Bookmark>> bookmarksSupplier) {
static Neo4jBookmarkManager createReactive(Supplier<Set<Bookmark>> bookmarksSupplier) {
return new ReactiveDefaultBookmarkManager(bookmarksSupplier);
}

View File

@@ -15,10 +15,12 @@
*/
package org.springframework.data.neo4j.core.transaction;
import java.io.Serial;
import java.util.Collections;
import java.util.Set;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationEvent;
@@ -33,7 +35,10 @@ import org.springframework.context.ApplicationEvent;
@API(status = API.Status.STABLE, since = "6.1.1")
public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent {
private final Set<Bookmark> bookmarks;
@Serial
private static final long serialVersionUID = 2143476552056698819L;
@Nullable
private transient final Set<Bookmark> bookmarks;
Neo4jBookmarksUpdatedEvent(Set<Bookmark> bookmarks) {
super(bookmarks);
@@ -45,6 +50,6 @@ public final class Neo4jBookmarksUpdatedEvent extends ApplicationEvent {
*/
public Set<Bookmark> getBookmarks() {
return Collections.unmodifiableSet(this.bookmarks);
return this.bookmarks == null ? Set.of() : Collections.unmodifiableSet(this.bookmarks);
}
}

View File

@@ -17,13 +17,13 @@ package org.springframework.data.neo4j.core.transaction;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.support.RetryExceptionPredicate;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.util.Assert;

View File

@@ -15,9 +15,12 @@
*/
package org.springframework.data.neo4j.core.transaction;
import java.io.Serial;
import java.util.Collection;
import java.util.Objects;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
@@ -33,7 +36,6 @@ import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
@@ -43,7 +45,6 @@ import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This
@@ -55,6 +56,9 @@ import org.springframework.util.StringUtils;
@API(status = API.Status.STABLE, since = "6.0")
public final class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements ApplicationContextAware {
@Serial
private static final long serialVersionUID = 7971369288503005574L;
/**
* Start building a new transaction manager for the given driver instance.
* @param driver A fixed driver instance.
@@ -114,7 +118,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
return this;
}
public Builder withBookmarkManager(@Nullable Neo4jBookmarkManager bookmarkManager) {
public Builder withBookmarkManager(Neo4jBookmarkManager bookmarkManager) {
this.bookmarkManager = bookmarkManager;
return this;
}
@@ -127,19 +131,19 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
/**
* The underlying driver, which is also the synchronisation object.
*/
private final Driver driver;
private transient final Driver driver;
/**
* Database name provider.
*/
private final DatabaseSelectionProvider databaseSelectionProvider;
private transient final DatabaseSelectionProvider databaseSelectionProvider;
/**
* Provider for user impersonation.
*/
private final UserSelectionProvider userSelectionProvider;
private transient final UserSelectionProvider userSelectionProvider;
private final BookmarkManagerReference bookmarkManager;
private transient final BookmarkManagerReference bookmarkManager;
/**
* This will create a transaction manager for the default database.
@@ -193,20 +197,6 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
this.bookmarkManager.setApplicationContext(applicationContext);
}
/**
* @param driver The driver that has been used as a synchronization object.
* @param targetDatabase The target database
* @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing
* Spring transaction
* @see #retrieveTransaction(Driver, DatabaseSelection, UserSelection)
* @deprecated since 6.2, use #retrieveTransaction(Driver, DatabaseSelection, UserSelection)
*/
@Deprecated
public static @Nullable Transaction retrieveTransaction(final Driver driver, @Nullable final String targetDatabase) {
return retrieveTransaction(driver, StringUtils.hasText(targetDatabase) ? DatabaseSelection.byName(targetDatabase) : DatabaseSelection.undecided(), UserSelection.connectedUser());
}
/**
* This method provides a native Neo4j transaction to be used from within a {@link org.springframework.data.neo4j.core.Neo4jClient}.
* In most cases this the native transaction will be controlled from the Neo4j specific {@link org.springframework.transaction.PlatformTransactionManager}.
@@ -219,7 +209,8 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
* @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing
* Spring transaction
*/
public static @Nullable Transaction retrieveTransaction(
@Nullable
public static Transaction retrieveTransaction(
final Driver driver,
final DatabaseSelection targetDatabase,
final UserSelection asUser
@@ -257,7 +248,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
.registerSynchronization(new Neo4jSessionSynchronization(connectionHolder, driver));
TransactionSynchronizationManager.bindResource(driver, connectionHolder);
return connectionHolder.getTransaction(targetDatabase, asUser);
return Objects.requireNonNull(connectionHolder.getTransaction(targetDatabase, asUser));
}
private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) {
@@ -285,7 +276,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
@Override
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return extractNeo4jTransaction(transaction).hasResourceHolder();
return extractNeo4jTransaction(transaction).getResourceHolder() != null;
}
@Override
@@ -331,7 +322,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(Objects.requireNonNull(transaction));
transactionObject.setResourceHolder((Neo4jTransactionHolder) suspendedResources);
TransactionSynchronizationManager.bindResource(driver, suspendedResources);
@@ -347,7 +338,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
this.bookmarkManager.resolve().updateBookmarks(transactionHolder.getBookmarks(), newBookmarks);
} catch (Neo4jException ex) {
if (ex instanceof RetryableException) {
throw new TransactionSystemException(ex.getMessage(), ex);
throw new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex);
}
throw ex;
}
@@ -383,7 +374,8 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
// If it is null, there's no existing session / transaction.
@Nullable private Neo4jTransactionHolder resourceHolder;
@Nullable
private Neo4jTransactionHolder resourceHolder;
Neo4jTransactionObject(@Nullable Neo4jTransactionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
@@ -393,23 +385,19 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
* Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's no existing
* transaction.
*
* @param resourceHolder A newly created resource holder with a fresh drivers session,
* @param resourceHolder A newly created resource holder with a fresh drivers' session,
*/
void setResourceHolder(@Nullable Neo4jTransactionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* @return {@literal true} if a {@link Neo4jTransactionHolder} is set.
*/
boolean hasResourceHolder() {
return resourceHolder != null;
@Nullable Neo4jTransactionHolder getResourceHolder() {
return resourceHolder;
}
Neo4jTransactionHolder getRequiredResourceHolder() {
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
return resourceHolder;
return Objects.requireNonNull(resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
}
void setRollbackOnly() {
@@ -419,7 +407,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
@Override
public boolean isRollbackOnly() {
return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly();
return this.resourceHolder != null && this.resourceHolder.isRollbackOnly();
}
@Override

View File

@@ -20,6 +20,7 @@ import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.SessionConfig;
@@ -39,6 +40,7 @@ import org.springframework.util.ReflectionUtils;
*/
public final class Neo4jTransactionUtils {
@Nullable
private static final Method WITH_IMPERSONATED_USER
= ReflectionUtils.findMethod(SessionConfig.Builder.class, "withImpersonatedUser", String.class);
@@ -46,7 +48,7 @@ public final class Neo4jTransactionUtils {
return WITH_IMPERSONATED_USER != null;
}
@SuppressWarnings("UnusedReturnValue")
@SuppressWarnings({"UnusedReturnValue", "NullAway"})
public static SessionConfig.Builder withImpersonatedUser(SessionConfig.Builder builder, String user) {
if (driverSupportsImpersonation()) {

View File

@@ -22,9 +22,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
/**
* Default bookmark manager.

View File

@@ -20,12 +20,12 @@ import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.reactivestreams.ReactiveSession;
import org.neo4j.driver.reactivestreams.ReactiveTransaction;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
/**

View File

@@ -15,10 +15,14 @@
*/
package org.springframework.data.neo4j.core.transaction;
import java.io.Serial;
import java.util.Objects;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.Driver;
import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.exceptions.RetryableException;
@@ -32,7 +36,6 @@ import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.lang.Nullable;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
@@ -52,6 +55,9 @@ import org.springframework.util.Assert;
@API(status = API.Status.STABLE, since = "6.0")
public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTransactionManager implements ApplicationContextAware {
@Serial
private static final long serialVersionUID = 204661696798919944L;
/**
* Start building a new transaction manager for the given driver instance.
* @param driver A fixed driver instance.
@@ -124,19 +130,19 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
/**
* The underlying driver, which is also the synchronisation object.
*/
private final Driver driver;
private transient final Driver driver;
/**
* Database name provider.
*/
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
private transient final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
/**
* Provider for user impersonation.
*/
private final ReactiveUserSelectionProvider userSelectionProvider;
private transient final ReactiveUserSelectionProvider userSelectionProvider;
private final BookmarkManagerReference bookmarkManager;
private transient final BookmarkManagerReference bookmarkManager;
/**
* This will create a transaction manager for the default database.
@@ -272,7 +278,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
@Override
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return extractNeo4jTransaction(transaction).hasResourceHolder();
return extractNeo4jTransaction(transaction).getResourceHolder() != null;
}
@Override
@@ -328,7 +334,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
.getRequiredResourceHolder();
return holder.commit()
.doOnNext(bookmark -> bookmarkManager.resolve().updateBookmarks(holder.getBookmarks(), bookmark))
.onErrorMap(e -> e instanceof RetryableException, e -> new TransactionSystemException(e.getMessage(), e))
.onErrorMap(e -> e instanceof RetryableException, ex -> new TransactionSystemException(Objects.requireNonNullElse(ex.getMessage(), "Caught a retryable exception"), ex))
.then();
}
@@ -350,10 +356,10 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
}
@Override
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, Object transaction,
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction,
Object suspendedResources) throws TransactionException {
return Mono.just(extractNeo4jTransaction(transaction))
return Mono.just(extractNeo4jTransaction(Objects.requireNonNull(transaction)))
.doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources))
.then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources)));
}
@@ -379,7 +385,8 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
// If it is null, there's no existing session / transaction.
@Nullable private ReactiveNeo4jTransactionHolder resourceHolder;
@Nullable
private ReactiveNeo4jTransactionHolder resourceHolder;
ReactiveNeo4jTransactionObject(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
@@ -389,33 +396,24 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
* Usually called in {@link #doBegin(TransactionSynchronizationManager, Object, TransactionDefinition)} which is
* called when there's no existing transaction.
*
* @param resourceHolder A newly created resource holder with a fresh drivers session,
* @param resourceHolder A newly created resource holder with a fresh drivers' session,
*/
void setResourceHolder(@Nullable ReactiveNeo4jTransactionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* @return {@literal true} if a {@link Neo4jTransactionHolder} is set.
*/
boolean hasResourceHolder() {
return resourceHolder != null;
}
ReactiveNeo4jTransactionHolder getRequiredResourceHolder() {
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
return resourceHolder;
return Objects.requireNonNull(this.resourceHolder, RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
}
void setRollbackOnly() {
getRequiredResourceHolder().setRollbackOnly();
@Nullable ReactiveNeo4jTransactionHolder getResourceHolder() {
return resourceHolder;
}
@Override
public boolean isRollbackOnly() {
return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly();
return this.resourceHolder != null && this.resourceHolder.isRollbackOnly();
}
@Override

View File

@@ -4,7 +4,7 @@
both the imperative and reactive `TransactionManager` as `Neo4jTransactionManager` and `ReactiveNeo4jTransactionManager`.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.core.transaction;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.neo4j.repository;
import java.io.Serial;
import org.apiguardian.api.API;
import org.springframework.dao.EmptyResultDataAccessException;
@@ -26,8 +28,10 @@ import org.springframework.dao.EmptyResultDataAccessException;
* @since 6.0
*/
@API(status = API.Status.STABLE, since = "6.0")
public class NoResultException extends EmptyResultDataAccessException {
public final class NoResultException extends EmptyResultDataAccessException {
@Serial
private static final long serialVersionUID = -1508370436250180391L;
private final String query;
public NoResultException(int expectedNumberOfResults, String query) {

View File

@@ -132,7 +132,7 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("neo4jOperations",
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
builder.addPropertyReference("neo4jMappingContext",
builder.addPropertyReference("mappingContext",
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
}

View File

@@ -130,7 +130,7 @@ public final class ReactiveNeo4jRepositoryConfigurationExtension extends Reposit
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("neo4jOperations",
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
builder.addPropertyReference("neo4jMappingContext",
builder.addPropertyReference("mappingContext",
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
}
}

View File

@@ -4,7 +4,7 @@
and reactive Spring Data Neo4j repositories.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.repository.config;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -3,7 +3,7 @@
This package provides the Neo4j imperative and reactive repository API.
* <!-- end::intent[] -->
*/
@NonNullApi
@NullMarked
package org.springframework.data.neo4j.repository;
import org.springframework.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;

View File

@@ -24,6 +24,7 @@ import java.util.function.LongSupplier;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.convert.converter.Converter;
@@ -48,7 +49,6 @@ import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -94,13 +94,14 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
if (Iterable.class.isAssignableFrom(returnType)) {
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
return GeoResult.class.equals(from.getComponentType().getType());
return from.getComponentType() != null && GeoResult.class.equals(from.getComponentType().getType());
}
return GeoPage.class.isAssignableFrom(returnType);
}
@Override
@Nullable
public final Object execute(Object[] parameters) {
boolean incrementLimit = queryMethod.incrementLimit();
@@ -122,8 +123,10 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext);
// Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case
preparingConverter = source -> converter.convert(
(EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source));
preparingConverter = source -> {
var unwrapped = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source);
return (unwrapped == null) ? null : converter.convert(unwrapped);
};
}
if (queryMethod.isPageQuery()) {
@@ -180,11 +183,11 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor
}
}
protected abstract <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType,
protected abstract <T> PreparedQuery<T> prepareQuery(Class<T> returnedType,
Collection<PropertyFilter.ProjectedPath> includedProperties, Neo4jParameterAccessor parameterAccessor,
@Nullable Neo4jQueryType queryType,
@Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction,
@Nullable UnaryOperator<Integer> limitModifier);
UnaryOperator<Integer> limitModifier);
protected Optional<PreparedQuery<Long>> getCountQuery(Neo4jParameterAccessor parameterAccessor) {
return Optional.empty();

View File

@@ -20,6 +20,7 @@ import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.core.convert.converter.Converter;
@@ -37,7 +38,6 @@ import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
@@ -84,13 +84,15 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R
if (Flux.class.isAssignableFrom(returnType)) {
TypeInformation<?> from = TypeInformation.fromReturnTypeOf(repositoryMethod);
return GeoResult.class.equals(from.getComponentType().getType());
TypeInformation<?> componentType = from.getComponentType();
return componentType != null && GeoResult.class.equals(componentType.getType());
}
return false;
}
@Override
@Nullable
public final Object execute(Object[] parameters) {
boolean incrementLimit = queryMethod.incrementLimit();
@@ -111,8 +113,10 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R
DtoInstantiatingConverter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), mappingContext);
// Neo4jQuerySupport ensure we will get an EntityInstanceWithSource in the projecting case
preparingConverter = source -> converter.convert(
(EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source));
preparingConverter = source -> {
var intermediate = (EntityInstanceWithSource) OptionalUnwrappingConverter.INSTANCE.convert(source);
return (intermediate == null) ? null : converter.convert(intermediate);
};
}
if (queryMethod.isScrollQuery()) {
@@ -125,6 +129,6 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R
protected abstract <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType,
Collection<PropertyFilter.ProjectedPath> includedProperties, Neo4jParameterAccessor parameterAccessor,
@Nullable Neo4jQueryType queryType, @Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction,
@Nullable UnaryOperator<Integer> limitModifier);
@Nullable Neo4jQueryType queryType, Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction,
UnaryOperator<Integer> limitModifier);
}

View File

@@ -20,21 +20,21 @@ import static org.neo4j.cypherdsl.core.Cypher.property;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.SortItem;
import org.neo4j.cypherdsl.core.StatementBuilder;
import org.neo4j.cypherdsl.core.SymbolicName;
import org.neo4j.driver.Value;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition.Direction;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
@@ -161,7 +161,7 @@ public final class CypherAdapterUtils {
expression = entity.getIdExpression();
var comparatorFunction = getComparatorFunction(scrollPosition.scrollsForward() ? Sort.Direction.ASC : Sort.Direction.DESC, scrollDirection);
allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(comparatorFunction.apply(expression, parameter));
} else {
} else if (propertyAndDirection.containsKey(k)) {
var p = propertyAndDirection.get(k);
expression = p.property.isIdProperty() ? entity.getIdExpression() : root.property(k);
@@ -188,22 +188,14 @@ public final class CypherAdapterUtils {
* @param sort The sort object to convert
* @return An of sort items. It will be empty when sort is unsorted.
*/
public static Collection<SortItem> toSortItems(NodeDescription<?> nodeDescription, Sort sort) {
public static Collection<SortItem> toSortItems(@Nullable NodeDescription<?> nodeDescription, Sort sort) {
if (nodeDescription == null) {
return List.of();
}
return sort.stream().map(sortAdapterFor(nodeDescription)).collect(Collectors.toList());
}
public static StatementBuilder.BuildableStatement addPagingParameter(NodeDescription<?> nodeDescription,
Pageable pageable, StatementBuilder.OngoingReadingAndReturn returning) {
Sort sort = pageable.getSort();
long skip = pageable.getOffset();
int pageSize = pageable.getPageSize();
return returning.orderBy(toSortItems(nodeDescription, sort)).skip(skip).limit(pageSize);
}
private CypherAdapterUtils() {}
}

View File

@@ -32,6 +32,7 @@ import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
@@ -64,8 +65,6 @@ import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* A Cypher-DSL based implementation of the {@link AbstractQueryCreator} that eventually creates Cypher queries as
@@ -94,11 +93,13 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
private final Pageable pagingParameter;
@Nullable
private final ScrollPosition scrollPosition;
/**
* Stores the number of max results, if the {@link PartTree tree} is limiting.
*/
@Nullable
private final Number maxResults;
/**
@@ -188,7 +189,6 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
return new QueryFragmentsAndParameters(nodeDescription, queryFragments, convertedParameters, theSort);
}
@NonNull
private QueryFragments createQueryFragments(@Nullable Condition condition, Sort sort) {
QueryFragments queryFragments = new QueryFragments();
@@ -234,7 +234,9 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
// Enforce sorting by something that is hopefully stable comparable (looking at Neo4j's id() with tears in my eyes).
theSort = theSort.and(Sort.by(entity.getRequiredIdProperty().getName()).ascending());
queryFragments.setLimit(limitModifier.apply(maxResults.intValue()));
if (maxResults != null) {
queryFragments.setLimit(limitModifier.apply(maxResults.intValue()));
}
if (!keysetScrollPosition.isInitial()) {
conditionFragment = conditionFragment.and(CypherAdapterUtils.combineKeysetIntoCondition(entity, keysetScrollPosition, theSort, mappingContext.getConversionService()));
}
@@ -244,7 +246,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
if (!offsetScrollPosition.isInitial()) {
queryFragments.setSkip(offsetScrollPosition.getOffset() + 1);
}
queryFragments.setLimit(limitModifier.apply(pagingParameter.isUnpaged() ? maxResults.intValue() : pagingParameter.getPageSize()));
queryFragments.setLimit(limitModifier.apply((pagingParameter.isUnpaged() && maxResults != null) ? maxResults.intValue() : pagingParameter.getPageSize()));
}
var finalSortItems = new ArrayList<>(this.sortItems);
@@ -580,7 +583,8 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryFragmentsAndPar
final Object value;
final @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride;
@Nullable
final Neo4jPersistentPropertyConverter<?> conversionOverride;
Parameter(String nameOrIndex, Object value, @Nullable Neo4jPersistentPropertyConverter<?> conversionOverride) {
this.nameOrIndex = nameOrIndex;

View File

@@ -23,6 +23,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn;
import org.neo4j.driver.types.MapAccessor;
@@ -64,8 +65,8 @@ final class CypherdslBasedQuery extends AbstractNeo4jQuery {
@Override
protected <T> PreparedQuery<T> prepareQuery(Class<T> returnedType,
Collection<PropertyFilter.ProjectedPath> includedProperties,
Neo4jParameterAccessor parameterAccessor, Neo4jQueryType queryType,
Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction,
Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType,
@Nullable Supplier<BiFunction<TypeSystem, MapAccessor, ?>> mappingFunction,
UnaryOperator<Integer> limitModifier) {
Object[] parameters = parameterAccessor.getValues();

View File

@@ -15,7 +15,14 @@
*/
package org.springframework.data.neo4j.repository.query;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.stream.Stream;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.neo4j.cypherdsl.core.Condition;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.KeysetScrollPosition;
@@ -30,13 +37,6 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.stream.Stream;
/**
* Immutable implementation of a {@link FetchableFluentQuery}. All
@@ -124,6 +124,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
}
@Override
@Nullable
public R oneValue() {
return findOperation.find(example.getProbeType())
@@ -134,6 +135,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
}
@Override
@Nullable
public R firstValue() {
List<R> all = all();
@@ -166,7 +168,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
@Override
public Window<R> scroll(ScrollPosition scrollPosition) {
Class<S> domainType = this.example.getProbeType();
Neo4jPersistentEntity<?> entity = mappingContext.getPersistentEntity(domainType);
Neo4jPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainType);
var skip = scrollPosition.isInitial()
? 0
@@ -174,7 +176,7 @@ final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> im
: 0;
Condition condition = scrollPosition instanceof KeysetScrollPosition keysetScrollPosition
? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService())
? CypherAdapterUtils.combineKeysetIntoCondition(mappingContext.getRequiredPersistentEntity(example.getProbeType()), keysetScrollPosition, sort, mappingContext.getConversionService())
: null;
List<R> rawResult = findOperation.find(domainType)

Some files were not shown because too many files have changed in this diff Show More