Drop the notion of the NodeManager.

This removes the NodeManager and related infrastructure and introduces some configuration support.
See more about the reasoning in ADR-004.

Co-authored-by: Gerrit Meier <meistermeier@gmail.com>
This commit is contained in:
Michael Simons
2019-05-21 18:54:30 +02:00
parent 6c0c5e509e
commit 808eb87507
42 changed files with 406 additions and 1908 deletions

View File

@@ -49,14 +49,7 @@ Each property of a class that is not identified as a simple type by `org.springf
NOTE: Context in this sections refers especially to dirty tracking and dealing with state of entities.
It is unlikely that we can do completely without context.
While it's easy enough to update on entity as is with the <<schema>>, it's unlikely that we can deal this way with collections of relationships.
Spring Data JDBC's approach is as follow
____
If the aggregate root is not new, all referenced entities get deleted, the aggregate root gets updated, and all referenced entities get inserted again.
____
That sounds good in theory, but the Graph itself being more optimized for reads than ongoing large chunks of writes probably won't like that.
We decided against a context for tracking changes, much like Spring Data JDBC did.
=== Other principles
@@ -182,7 +175,8 @@ It is only meant to be a basic for discussions.
note "Implementation of Spring Data Commons SPI" as SDC_note
package "org.springframework.data.neo4j" {
package "core" {
interface Neo4jTemplate
interface Neo4jClient
interface ReactiveNeo4jClient
package "schema" {
package "internal" {
note "Schema description" as schemaDescription
@@ -194,8 +188,8 @@ package "core" {
interface Neo4jPersistentEntity
interface Neo4jPersistentProperty
}
package "session" {
interface Neo4jSessionFactory
package "transaction" {
class Neo4jTransactionManager
}
package "convert" {
note "conversion support" as conversionNote
@@ -234,7 +228,6 @@ core-[hidden]--->repository
|`Neo4jTemplate` and related classes.
|core.schema
|Annotations for marking classes as nodes to be saved as well as internal schema description.
|context
|Infrastructure for dirty tracking etc.
|core.mapping
|Spring mapping information.
@@ -291,10 +284,10 @@ The other execution paths are only drafts right now and marked with a `*`.
From our previous experience and handling in other Spring Data stores this would branch off in two (technical three) directions:
. `StringBasedNeo4jQuery` for custom Cypher queries that are provided with the `@Query` annotation.
. `*` `StringBasedNeo4jQuery` for named queries that are outsourced in property files.
. `*` `PartTreeNeo4jQuery` for derived finder methods.
. `StringBasedNeo4jQuery` for named queries that are outsourced in property files.
. `PartTreeNeo4jQuery` for derived finder methods.
All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jOperations` and `QueryMethodEvaluationContextProvider` (not used yet) provided.
All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jClient` and `QueryMethodEvaluationContextProvider` (not used yet) provided.
This is a wrapper around the `java.lang.reflect.Method` passed into the `resolveQuery` method of the `Neo4jQueryLookupStrategy` to provide additional metadata.
==== `StringBasedNeo4jQuery` execution
@@ -314,30 +307,7 @@ We considered several approaches of dirty tracking in SDN-RX:
. Shallow copy of objects to get compared on save.
_A full copy of the objects will occupy twice the memory._
Picking the third option for now (_using some kind of event / listener_) would allow us to track single field / relationship changes per object.
An entity will get tracked after it gets initially saved into or loaded from the database.
[NOTE]
.Architecture evaluation fragment (not current anymore but should get archived)
====
As the technical solution we decided to use proxies to listen for the changes.
It will then be required for the user to work with the returned (proxy) object after calling `save` to set additional fields etc.
The event tracking and event triggering through the proxies should communicate via an API to have the option of using another tracking approach in the future.
Moved back to evaluation: It is not possible to simply intercept arbitrary property changes in Java.
We would need some kind of byte code enhancement/maven plugin to achieve this.
====
The information about object changes will be based on an object comparison.
We will store the saved or loaded object state and compare it with the actual version that should get saved.
To keep the eventing idea up this will create an one-shot event containing multiple changed properties.
This will also be needed if we implement the support for property changes through the byte code enhancement,
because a change / domain interaction could affect more than one property at a time.
The events will then get used to construct the matching cypher for the next save interaction.
==== Side-effect on "flush mode"
We can also track repository interactions like `save` and defer them depending on the flush mode.
This would make it possible to remove the need for calling `save` explicitly but be more JPA-ish.
We have settled with option 1 (See ADR-004), analogue to Spring Data JDBC.
[[starter]]
== Spring Boot Starter

22
etc/adr/adr-004.adoc Normal file
View File

@@ -0,0 +1,22 @@
== ADR 4: Drop the notion of the `NodeManager`
=== Status
accepted
=== Context
We introduced the `NodeManager` as a pendan to Hibernates `EntityManager` and with it, a concept of a persistence context, tracking changes.
This setup is required for updating only changed properties and also having implicit saves.
=== Decision
The previous versions of SDN and OGM all copied the concept of having a tracking of entities.
We decided against it this time to remove complexity.
We will update all properties each time a node is save, relying on the database to do this in an efficient way.
Relationships will be updated via smart queries.
=== Consequences
The biggest impact will probably more network traffic with models having a huge number of properties on a single domain object.

View File

@@ -16,22 +16,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
package org.springframework.data.neo4j.config;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException;
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.Neo4jClient;
/**
* Throw when a query doesn't return a unique result.
* Base class for imperative SDN-RX configuration using JavaConfig.
* This can be included in all scenarios in which Spring Boot is not an option.
*
* @author Michael J. Simons
* @soundtrack Deichkind - Niveau weshalb warum
* @author Gerrit Meier
* @since 1.0
*/
@Configuration
@API(status = API.Status.STABLE, since = "1.0")
public class NonUniqueResultException extends IllegalResultSizeException {
public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
public NonUniqueResultException(String query, long actualNumberOfResults) {
super(1L, actualNumberOfResults, query);
/**
* The driver used here should be the driver resulting from {@link #driver()}, which is the default.
*
* @param driver The driver to connect with.
* @return A imperative Neo4j client.
*/
@Bean
public Neo4jClient neo4jClient(Driver driver) {
return Neo4jClient.create(driver);
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.config;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apiguardian.api.API;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out about
* which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver
* contains both imperative and reactive components.
*
* @author Michael J. Simons
* @author Gerrit Meier
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
public abstract class Neo4jConfigurationSupport {
/**
* The driver to be used for interacting with Neo4j.
*
* @return
*/
public abstract Driver driver();
/**
* Creates a {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} equipped with entity classes
* scanned from the mapping base package.
*
* @return
* @see #getMappingBasePackages()
*/
@Bean
public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException {
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
return mappingContext;
}
/**
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
*
* @param driver The driver to synchronize against
* @return A platform transaction manager
*/
@Bean
public PlatformTransactionManager transactionManager(Driver driver) {
return new Neo4jTransactionManager(driver);
}
/**
* Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the
* configuration class' (the concrete class, not this one here) by default. So if you have a
* {@code com.acme.AppConfig} extending {@link Neo4jConfigurationSupport} the base package will be considered
* {@code com.acme} unless the method is overridden to implement alternate behavior.
*
* @return the base packages to scan for mapped {@link Node} classes
* or an empty collection to not enable scanning for entities.
*/
protected final Collection<String> getMappingBasePackages() {
Package mappingBasePackage = getClass().getPackage();
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
}
/**
* Scans the mapping base package for classes annotated with {@link Node}.
* By default, it scans for entities in all packages returned by {@link #getMappingBasePackages()}.
*
* @return
* @throws ClassNotFoundException
* @see #getMappingBasePackages()
*/
protected final Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
for (String basePackage : getMappingBasePackages()) {
initialEntitySet.addAll(scanForEntities(basePackage));
}
return initialEntitySet;
}
/**
* Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}.
*
* @param basePackage must not be {@literal null}.
* @return
* @throws ClassNotFoundException
*/
protected final Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
if (!StringUtils.hasText(basePackage)) {
return Collections.emptySet();
}
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
ClassPathScanningCandidateComponentProvider componentProvider =
new ClassPathScanningCandidateComponentProvider(false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class));
ClassLoader classLoader = Neo4jConfigurationSupport.class.getClassLoader();
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader));
}
return initialEntitySet;
}
}

View File

@@ -1,136 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
import static java.util.stream.Collectors.*;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apiguardian.api.API;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.exceptions.NoSuchRecordException;
import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec;
import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec;
import org.springframework.data.neo4j.core.context.DefaultPersistenceContext;
import org.springframework.data.neo4j.core.context.PersistenceContext;
import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.Schema;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
* @author Gerrit Meier
* @since 1.0
*/
@API(status = API.Status.INTERNAL, since = "1.0")
class DefaultNodeManager implements NodeManager {
private final Schema schema;
private final Neo4jClient neo4jClient;
private final Transaction transaction;
private final PersistenceContext persistenceContext;
DefaultNodeManager(Schema schema, Neo4jClient neo4jClient, @Nullable Transaction transaction) {
this.schema = schema;
this.neo4jClient = neo4jClient;
this.transaction = transaction;
this.persistenceContext = new DefaultPersistenceContext();
}
@Override
@Nullable
public Transaction getTransaction() {
return transaction;
}
@Override
public <T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery) {
Class<T> resultType = preparedQuery.getResultType();
MappingSpec<Optional<T>, Collection<T>, T> mappingSpec = neo4jClient.newQuery(preparedQuery.getCypherQuery())
.bindAll(preparedQuery.getParameters())
.fetchAs(resultType);
RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec = preparedQuery.getOptionalMappingFunction()
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
.orElse(mappingSpec);
return new DefaultExecutableQuery(preparedQuery, schema.getNodeDescription(resultType), fetchSpec);
}
@Override
public <T> T save(T entityWithUnknownState) {
// TODO if already registered, here or in the context?
this.persistenceContext
.register(entityWithUnknownState, schema.getRequiredNodeDescription(entityWithUnknownState.getClass()));
throw new UnsupportedOperationException("Not there yet.");
}
@Override
public void delete(Object managedEntity) {
this.persistenceContext.deregister(managedEntity);
throw new UnsupportedOperationException("Not there yet.");
}
@RequiredArgsConstructor
class DefaultExecutableQuery<T> implements ExecutableQuery<T> {
private final PreparedQuery<T> preparedQuery;
private final Optional<NodeDescription<?>> optionalNodeDescription;
private final RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec;
@Override
public List<T> getResults() {
return fetchSpec.all().stream().map(this::register).collect(toList());
}
@Override
public Optional<T> getSingleResult() {
try {
return fetchSpec.one().map(this::register);
} catch (NoSuchRecordException e) {
return Optional.empty();
}
}
@Override
public T getRequiredSingleResult() {
return fetchSpec.one().map(this::register)
.orElseThrow(() -> new NoResultException(1L, preparedQuery.getCypherQuery()));
}
private T register(T entity) {
this.optionalNodeDescription.ifPresent(
nodeDescription -> DefaultNodeManager.this.persistenceContext.register(entity, nodeDescription));
return entity;
}
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
import org.neo4j.driver.Transaction;
import org.springframework.lang.Nullable;
/**
* Entry point for creating queries that return managed Nodes. The node manager is not supposed to be kept around
* for longer than necessary. Try to keep your transactions short to avoid memory pressure due to keeping track of
* managed nodes.
*
* @author Michael J. Simons
*/
@API(status = API.Status.STABLE, since = "1.0")
public interface NodeManager {
/**
* Clears all managed entities and flushes any open state to the underlying storage.
* TODO reflect if we really, really want to have this.
* If an user flushes the node manager, we will end up in a state where we do not have any information about
* the nodes / entities currently processed in the ongoing transaction.
*/
default void flush() {
}
@Nullable
Transaction getTransaction();
<T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery);
/**
* Saves an entity. When the entity is not yet managed in this instance of the NodeManager, and will be registered as
* a managed instance. In either way, the state of the entity will be written to the underlying store afterwards.
* It is recommended to use the returned, managed instance of the object. This is especially true when dealing with
* immutable entities, where SDN RX has to return new instances to fill in generated keys and the like.
*
* @param entityWithUnknownState An entity that is either managed or unmanaged
* @return A managed object
*/
<T> T save(T entityWithUnknownState);
/**
* Delete an object from the persistence context and the underlying store.
*
* @param managedEntity Object to be removed
*/
void delete(Object managedEntity);
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apiguardian.api.API;
import org.neo4j.driver.Driver;
import org.springframework.data.neo4j.core.schema.Schema;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.lang.Nullable;
/**
* Creates ready to use instances of {@link NodeManager}.
*
* @author Michael J. Simons
*/
@API(status = API.Status.STABLE, since = "1.0")
@Slf4j
public final class NodeManagerFactory {
private final AtomicBoolean initialized = new AtomicBoolean(false);
/**
* Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils.
*/
private final Driver driver;
/**
* The initial set of classes that will be registered with the schema {@link #initialize()} to build the schema for node managers
* belonging to this factory.
*/
private final Set<Class<?>> initialPersistentClasses;
@Nullable
private Schema schema;
/**
* Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup,
* i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use
* the same {@link Driver driver instance} for both the node and the transaction manager.
* <p>
* Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns.
*
* @param driver The driver used to obtain statement runners from when creating instances of node managers.
* @param initialPersistentClasses The set of classes that should be initially scanned
*/
public NodeManagerFactory(Driver driver, Class<?>... initialPersistentClasses) {
this(driver, Arrays.asList(initialPersistentClasses));
}
/**
* @see NodeManagerFactory#NodeManagerFactory(Driver, Class...)
*/
public NodeManagerFactory(Driver driver, Collection<Class<?>> initialPersistentClasses) {
this.driver = driver;
this.initialPersistentClasses = new HashSet<>(initialPersistentClasses);
}
/**
* Creates a new node manager. The returned manager is supposed to have a short lifetime. When used in a Spring setup,
* this method should not called directly by client code. Instead the client code should use an injected instance of
* {@link NodeManager}, which will participate in Springs application based transaction or in JTA transactions.
*
* @return A new node manager
*/
public NodeManager createNodeManager() {
if (!initialized.get()) {
throw new IllegalStateException(
"This factory has not been correctly initialized. Please provider a schema and register your persistent classes.");
}
// The call here to our Spring transaction shim has to be rethought in case we move this out of a Spring scope.
// I dropped all the methods to configure that in an effort to make the setup more simple. ^mjs
return new DefaultNodeManager(schema, Neo4jClient.create(driver),
Neo4jTransactionUtils.retrieveTransaction(driver, null).orElse(null));
}
/**
* Provides the schema for this node manager factory. The schemas has to be set before a node manager is retrieved from this factory.
*
* @param schema
*/
public void setSchema(@Nullable Schema schema) {
this.schema = schema;
}
/**
* This initializes this factory and is usually called by Springs infrastructure and only useful as standalone call
* when the node manager factory is used without Spring.
*/
public void initialize() {
if (this.initialized.compareAndSet(false, true)) {
Objects.requireNonNull(schema, "A schema is required. Did you provide one with #setSchema() beforehand?");
log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size());
this.schema.register(this.initialPersistentClasses);
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
/**
* Shared base class for exceptions during persistence operations of a {@link NodeManager}.
*
* @author Michael J. Simons
* @soundtrack Deichkind - Niveau weshalb warum
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
public abstract class PersistenceException extends RuntimeException {
protected PersistenceException(String message) {
super(message);
}
protected PersistenceException(String message, Throwable cause) {
super(message, cause);
}
abstract static class IllegalResultSizeException extends PersistenceException {
private final long expectedNumberOfResults;
private final long actualNumberOfResults;
private final String query;
IllegalResultSizeException(long expectedNumberOfResults, long actualNumberOfResults, String query) {
super(String.format("Expected %d results, got %d", expectedNumberOfResults, actualNumberOfResults));
this.query = query;
this.expectedNumberOfResults = expectedNumberOfResults;
this.actualNumberOfResults = actualNumberOfResults;
}
public long getExpectedNumberOfResults() {
return expectedNumberOfResults;
}
public long getActualNumberOfResults() {
return actualNumberOfResults;
}
public String getQuery() {
return query;
}
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.data.neo4j.core.context.tracking.EntityChangeEvent;
import org.springframework.data.neo4j.core.context.tracking.EntityComparisonStrategy;
import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* TODO explain what the persistence context should do other than being the plural of EntityTrackingStrategy :D
*
* @author Michael J. Simons
* @author Gerrit Meier
*/
@Slf4j
public class DefaultPersistenceContext implements PersistenceContext {
private final EntityTrackingStrategy entityTrackingStrategy;
private final Set<Integer> registeredObjectIds = new HashSet<>();
public DefaultPersistenceContext() {
this.entityTrackingStrategy = getEntityTrackingStrategy();
}
@Override
public void register(Object entity, NodeDescription<?> description) {
Objects.requireNonNull(entity, "Cannot track null-entity!");
int identityOfEntity = getIdentityOf(entity);
if (registeredObjectIds.contains(identityOfEntity)) {
log.debug("Object " + entity + " was already registered");
return;
}
registeredObjectIds.add(identityOfEntity);
entityTrackingStrategy.track(entity, description);
}
@Override
public void deregister(Object managedEntity) {
int identityOfEntity = getIdentityOf(managedEntity);
if (!registeredObjectIds.contains(identityOfEntity)) {
log.info("Cannot deregister " + managedEntity + " because it were never registered");
return;
}
entityTrackingStrategy.untrack(managedEntity);
}
@Override
public Collection<EntityChanges> getEntityChanges(Object... objects) {
List<EntityChanges> entityChanges = new ArrayList<>();
for (Object object : objects) {
Collection<EntityChangeEvent> changeEvents = entityTrackingStrategy.getAggregatedEntityChangeEvents(object);
entityChanges.add(new EntityChanges(object, changeEvents));
}
return entityChanges;
}
EntityTrackingStrategy getEntityTrackingStrategy() {
// todo choose the right / fitting implementation
return new EntityComparisonStrategy();
}
private int getIdentityOf(Object entity) {
return entityTrackingStrategy.getObjectIdentifier(entity);
}
@Getter
static class EntityChanges {
private final Object entity;
private final Collection<EntityChangeEvent> changeEvents;
EntityChanges(Object entity, Collection<EntityChangeEvent> changeEvents) {
this.entity = entity;
this.changeEvents = changeEvents;
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context;
import java.util.Collection;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* Represents the state of varies instances being tracked. Those include:
* <ul>
* <li>All nodes</li>
* <li>All relationships</li>
* </ul>
* including properties of them.
*
* @author Michael J. Simons
* @author Gerrit Meier
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public interface PersistenceContext {
/**
* Registers an entity within the context and starts tracking the entities state.
*
* @param entity The entity to register
* @param description The schema description of the entities class
*/
void register(Object entity, NodeDescription<?> description);
/**
* Removes an entity from this context and stops tracking the entitys state.
*
* @param managedEntity The entity to remove
*/
void deregister(Object managedEntity);
/**
* Calculates the deltas for each registered object and returns them grouped by the object identifying function
* defined in
* {@link org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy#getObjectIdentifier(Object)} or
* one of its implementations.
*
* @param objects to calculate and return changes for
* @return All change events that got registered since registration.
*/
Collection<DefaultPersistenceContext.EntityChanges> getEntityChanges(Object... objects);
}

View File

@@ -1,9 +0,0 @@
/**
* Contains all necessary infrastructure for the context of a session, i.e. dirty tracking and related.
*
* @author Michael J. Simons
*/
@NonNullApi
package org.springframework.data.neo4j.core.context;
import org.springframework.lang.NonNullApi;

View File

@@ -1,39 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context.tracking;
import lombok.Getter;
import org.apiguardian.api.API;
/**
* @author Gerrit Meier
* @author Michael J. Simons
*/
@API(status = API.Status.INTERNAL, since = "1.0")
@Getter
public final class EntityChangeEvent {
private final String propertyField;
private final Object value;
EntityChangeEvent(String propertyField, Object value) {
this.propertyField = propertyField;
this.value = value;
}
}

View File

@@ -1,170 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context.tracking;
import static org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.springframework.data.neo4j.core.schema.GraphPropertyDescription;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* Dirty tracking strategy based on property comparison. The strategy can compare two states of the same instance on
* attribute level but uses hashes for collection-like fields. Only fields that are considered to be node properties
* will get compared.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
public class EntityComparisonStrategy implements EntityTrackingStrategy {
private final Map<Integer, EntityState> statesOfEntities = new HashMap<>();
@Override
public void track(Object entity, NodeDescription nodeDescription) {
statesOfEntities.put(getObjectIdentifier(entity),
new EntityState(nodeDescription, entity, getDefaultObjectIdentifier()));
}
@Override
public void untrack(Object entity) {
statesOfEntities.remove(getObjectIdentifier(entity));
}
@Override
public Collection<EntityChangeEvent> getAggregatedEntityChangeEvents(Object entity) {
int objectIdentifier = getObjectIdentifier(entity);
return statesOfEntities.get(objectIdentifier).aggregateChanges(entity);
}
/**
* Compares two entities of the same instance and creates a delta {@link EntityChangeEvent}.
*/
private static class EntityState {
private final Map<String, Object> oldState;
private final List<Field> objectFields;
private final Object identifier;
private final Function<Object, Integer> objectIdentifyingFuction;
EntityState(NodeDescription nodeDescription, Object entity, Function<Object, Integer> objectIdentifyingFuction) {
Collection<GraphPropertyDescription> properties = nodeDescription.getGraphProperties();
this.objectFields = getFieldsFromProperties(entity, properties);
this.objectIdentifyingFuction = objectIdentifyingFuction;
this.identifier = this.objectIdentifyingFuction.apply(entity);
this.oldState = retrieveStateFrom(entity);
}
Set<EntityChangeEvent> aggregateChanges(Object newObject) {
if (!sameObject(objectIdentifyingFuction.apply(newObject))) {
throw new IllegalArgumentException("The objects to compare are not the same.");
}
Set<EntityChangeEvent> changes = new HashSet<>();
for (Field field : objectFields) {
Object newValue = getFieldValueOrHashCode(field, newObject);
Object oldValue = oldState.get(field.getName());
// check oldValue null first to avoid NPE in `equals` calls
if (oldValue == null && newValue == null) {
continue;
}
if (oldValue == null || !oldValue.equals(newValue)) {
changes.add(new EntityChangeEvent(field.getName(), newValue));
}
}
return changes;
}
private List<Field> getFieldsFromProperties(Object entity, Collection<GraphPropertyDescription> properties) {
List<Field> fields = new ArrayList<>();
Class<?> entityClass = entity.getClass();
Map<String, Field> classFields = retrieveAllFields(entityClass);
for (GraphPropertyDescription property : properties) {
fields.add(classFields.get(property.getFieldName()));
}
return fields;
}
private Map<String, Field> retrieveAllFields(Class<?> entityClass) {
Map<String, Field> classFields = new HashMap<>();
Class<?> classToScan = entityClass;
do {
for (Field field : classToScan.getDeclaredFields()) {
classFields.put(field.getName(), field);
}
classToScan = classToScan.getSuperclass();
} while (classToScan != null);
return classFields;
}
private boolean sameObject(Integer objectIdentifier) {
return this.identifier.equals(objectIdentifier);
}
private Map<String, Object> retrieveStateFrom(Object entity) {
Map<String, Object> state = new HashMap<>();
for (Field field : objectFields) {
state.put(field.getName(), getFieldValueOrHashCode(field, entity));
}
return Collections.unmodifiableMap(state);
}
private Object getFieldValueOrHashCode(Field field, Object entity) {
try {
field.setAccessible(true);
Object value = field.get(entity);
field.setAccessible(false);
if (value == null) {
return null;
} else if (!Collection.class.isAssignableFrom(value.getClass())) {
return value;
} else {
return value.hashCode();
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Cannot determine field value", e);
}
}
}
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context.tracking;
import java.util.Collection;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* A tracking strategy is used to determine if an entity has changed it state over time in a transaction. It is also
* responsible to generate and return a complete list of all these changes on an entity's attribute level.
*
* @author Gerrit Meier
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public interface EntityTrackingStrategy {
static Function<Object, Integer> getDefaultObjectIdentifier() {
return System::identityHashCode;
}
/**
* Register an entity in the entity tracking strategy. {@link NodeDescription} is needed to determine the fields that
* are considered mapping relevant.
*
* @param entity the object that should get tracked.
* @param nodeDescription the "rich" entity's class description.
*/
void track(Object entity, NodeDescription nodeDescription);
/**
* Remove an entity from tracking. This method should get called after an entity was deleted to have a clean change
* history state.
*
* @param entity to get removed from entity tracking
*/
void untrack(Object entity);
/**
* Aggregates all changes that were registered for this entity since the start of tracking.
*
* @param entity for which a summarized collection of {@EntityChangeEvent}s should get created.
* @return all events that occurred since tracking started.
*/
Collection<EntityChangeEvent> getAggregatedEntityChangeEvents(Object entity);
/**
* Returns an object identifier for a given entity.
*
* @param entity the entity to get an unique identifier for.
* @return hash or something similar that represents the entity.
*/
default int getObjectIdentifier(Object entity) {
return getDefaultObjectIdentifier().apply(entity);
}
}

View File

@@ -138,13 +138,6 @@ public class Neo4jMappingContext
return new DefaultNeo4jPersistentProperty(property, neo4jPersistentProperties, simpleTypeHolder);
}
@Override
public synchronized void register(Set<? extends Class<?>> entityClasses) {
this.setInitialEntitySet(entityClasses);
this.initialize();
}
@Override
public Optional<NodeDescription<?>> getNodeDescription(String primaryLabel) {
return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel));

View File

@@ -40,11 +40,16 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchA
public interface Schema {
/**
* Registers and scans the given set of classes to be available as Neo4j domain entities.
* Registers the given set of classes to be available as Neo4j domain entities.
*
* @param entityClasses The additional set of classes to register with this schema
* @param initialEntitySet The set of classes to register with this schema
*/
void register(Set<? extends Class<?>> entityClasses);
void setInitialEntitySet(Set<? extends Class<?>> initialEntitySet);
/**
* Triggers the scanning of the registered, initial entity set.
*/
void initialize();
/**
* Retrieves a nodes description by its primary label.

View File

@@ -23,18 +23,12 @@ import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apiguardian.api.API;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.neo4j.driver.TransactionConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.lang.Nullable;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.InvalidIsolationLevelException;
@@ -57,32 +51,14 @@ import org.springframework.util.Assert;
*/
@API(status = API.Status.STABLE, since = "1.0")
@Slf4j
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware {
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager {
private final Driver driver;
@Nullable
private NodeManagerFactory nodeManagerFactory;
public Neo4jTransactionManager(Driver driver) {
this.driver = driver;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
try {
this.setNodeManagerFactory(beanFactory.getBean(NodeManagerFactory.class));
} catch (NoSuchBeanDefinitionException ex) {
log.warn("Found no instance of {}", NodeManagerFactory.class);
}
}
public void setNodeManagerFactory(@Nullable NodeManagerFactory nodeManagerFactory) {
// TODO Check if the NodeManager uses the same datasource aka driver as we do
this.nodeManagerFactory = nodeManagerFactory;
}
@Override
protected Object doGetTransaction() throws TransactionException {
@@ -118,13 +94,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
connectionHolder.setSynchronizedWithTransaction(true);
transactionObject.setResourceHolder(connectionHolder);
TransactionSynchronizationManager.bindResource(this.driver, connectionHolder);
if (this.nodeManagerFactory != null) {
NodeManagerHolder nodeManagerHolder = new NodeManagerHolder(this.nodeManagerFactory.createNodeManager());
nodeManagerHolder.setSynchronizedWithTransaction(true);
transactionObject.setNodeManagerHolder(nodeManagerHolder);
TransactionSynchronizationManager.bindResource(this.nodeManagerFactory, nodeManagerHolder);
}
} catch (Exception ex) {
ex.printStackTrace();
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()));
@@ -173,14 +142,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
transactionObject.getRequiredResourceHolder().close();
transactionObject.getNodeManagerHolder()
.map(NodeManagerHolder::getNodeManager)
.ifPresent(nodeManager -> nodeManager.flush());
TransactionSynchronizationManager.unbindResource(driver);
if (this.nodeManagerFactory != null) {
TransactionSynchronizationManager.unbindResource(this.nodeManagerFactory);
}
}
private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) {
@@ -227,9 +190,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
@Nullable
private Neo4jConnectionHolder resourceHolder;
@Nullable
private NodeManagerHolder nodeManagerHolder;
Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
@@ -244,10 +204,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
this.resourceHolder = resourceHolder;
}
void setNodeManagerHolder(@Nullable NodeManagerHolder nodeManagerHolder) {
this.nodeManagerHolder = nodeManagerHolder;
}
/**
* @return {@literal true} if a {@link Neo4jConnectionHolder} is set.
*/
@@ -261,10 +217,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
return resourceHolder;
}
Optional<NodeManagerHolder> getNodeManagerHolder() {
return Optional.ofNullable(nodeManagerHolder);
}
void setRollbackOnly() {
getRequiredResourceHolder().setRollbackOnly();

View File

@@ -29,8 +29,6 @@ import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionParametersTemplate;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.reactive.RxTransaction;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -48,12 +46,10 @@ public final class Neo4jTransactionUtils {
* @return Session parameters to configure the default session used
*/
public static Consumer<SessionParametersTemplate> defaultSessionParameters(@Nullable String databaseName) {
return t -> {
t
.withDefaultAccessMode(AccessMode.WRITE)
.withBookmarks(Collections.EMPTY_LIST)
.withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME));
};
return t -> t
.withDefaultAccessMode(AccessMode.WRITE)
.withBookmarks(Collections.EMPTY_LIST)
.withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME));
}
/**
@@ -105,33 +101,6 @@ public final class Neo4jTransactionUtils {
return Mono.empty();
}
public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return nodeManagerFactory.createNodeManager();
}
// Try existing transaction
NodeManagerHolder nodeManagerHolder = (NodeManagerHolder) TransactionSynchronizationManager
.getResource(nodeManagerFactory);
if (nodeManagerHolder != null) {
return nodeManagerHolder.getNodeManager();
}
// Manually create a new synchronization
nodeManagerHolder = new NodeManagerHolder(nodeManagerFactory.createNodeManager());
nodeManagerHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.registerSynchronization(
new NodeManagerSynchronization(nodeManagerHolder, nodeManagerFactory));
TransactionSynchronizationManager.bindResource(nodeManagerFactory, nodeManagerHolder);
return nodeManagerHolder.getNodeManager();
}
private Neo4jTransactionUtils() {
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.transaction;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.util.Assert;
/**
* Dedicated holder for storing a NodeManager inside a transaction.
* <p>
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Michael J. Simons
*/
class NodeManagerHolder extends ResourceHolderSupport {
@Nullable
private final NodeManager nodeManager;
NodeManagerHolder(@Nullable NodeManager nodeManager) {
this.nodeManager = nodeManager;
}
public NodeManager getNodeManager() {
Assert.state(this.nodeManager != null, "No NodeManager available");
return this.nodeManager;
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.transaction;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.transaction.support.ResourceHolderSynchronization;
/**
* @author Michael J. Simons
*/
public class NodeManagerSynchronization
extends ResourceHolderSynchronization<NodeManagerHolder, Object> {
private final NodeManagerHolder localNodeManagerHolder;
NodeManagerSynchronization(NodeManagerHolder nodeManagerHolder, NodeManagerFactory nodeManagerFactory) {
super(nodeManagerHolder, nodeManagerFactory);
this.localNodeManagerHolder = nodeManagerHolder;
}
@Override
protected void flushResource(NodeManagerHolder resourceHolder) {
resourceHolder.getNodeManager().flush();
}
}

View File

@@ -16,10 +16,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
package org.springframework.data.neo4j.repository;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* Throw when a query doesn't return a required result.
@@ -29,9 +29,16 @@ import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSiz
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
public class NoResultException extends IllegalResultSizeException {
public class NoResultException extends EmptyResultDataAccessException {
NoResultException(long expectedNumberOfResults, String query) {
super(expectedNumberOfResults, 0L, query);
private final String query;
public NoResultException(int expectedNumberOfResults, String query) {
super(expectedNumberOfResults);
this.query = query;
}
public String getQuery() {
return query;
}
}

View File

@@ -31,9 +31,7 @@ import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()},
@@ -78,15 +76,14 @@ public @interface EnableNeo4jRepositories {
Class<?> repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class;
/**
* Configures the name of the {@link NodeManager} bean to be used with the repositories detected.
* Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected.
*/
String nodeManagerFactoryRef() default DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME;
String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME;
/**
* Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {@code transactionManager}.
* Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected.
*/
String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME;
String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME;
/**
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from

View File

@@ -24,12 +24,9 @@ import java.util.Collections;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.data.neo4j.repository.support.NodeManagerFactoryBean;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
@@ -49,20 +46,12 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
/**
* See {@link AbstractBeanDefinition#INFER_METHOD}.
*/
static final String GENERATE_BEAN_NAME = "(generated)";
static final String DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME = "nodeManagerFactory";
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient";
/**
* Holds the name of the shared NodeManagerBean created from the factory with the configured name.
* See {@link AbstractBeanDefinition#INFER_METHOD}.
*/
private String generatedNodeManagerBeanName;
/**
* Holds the name of the shared Neo4j mapping context..
*/
private String generatedMappingContextBeanName;
static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext";
/*
* (non-Javadoc)
@@ -99,61 +88,9 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
@Override
public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
builder.addPropertyValue("transactionManager",
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName);
builder.addPropertyReference("neo4jMappingContext", this.generatedMappingContextBeanName);
}
@Override
public void registerBeansForRoot(BeanDefinitionRegistry registry,
RepositoryConfigurationSource config) {
// Mapping context
AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(Neo4jMappingContext.class)
.getBeanDefinition();
this.generatedMappingContextBeanName = registerWithSourceAndGeneratedBeanName(
neo4jMappingContextBeanDefinition, registry, config);
// Augmented node manager factory (creating injectable, shared instances of NodeManager that are aware of the mapping context).
String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef")
.orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME);
AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(NodeManagerFactoryBean.class)
.addConstructorArgReference(nameOfNodeManagerFactory)
.addConstructorArgReference(generatedMappingContextBeanName)
.getBeanDefinition();
this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName(
sharedSessionCreatorBeanDefinition, registry, config);
}
/**
* Uses a generated bean name if {@code configuredBeanName} is equal to {@link #GENERATE_BEAN_NAME}, otherwise uses
* the configured bean name to register the new bean. Does not check if there's already a bean under the configured
* name but throws a {@link org.springframework.beans.factory.BeanDefinitionStoreException}. Checks whether a bean is
* already registered under {@code configuredBeanName} in the given {@link BeanDefinitionRegistry} and uses a
* generated name for registering the bean instead. If not, the suggested bean name is used.
*
* @param bean must not be {@literal null}.
* @param registry must not be {@literal null}.
* @param configuredBeanName must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @return the bean name used for registering the given {@link AbstractBeanDefinition}
* @throws org.springframework.beans.factory.BeanDefinitionStoreException if the BeanDefinition is invalid or if there
* is already a BeanDefinition for the specified bean name * (and we are not allowed to override it)
*/
private static String registerWithGeneratedNameOrUseConfigured(AbstractBeanDefinition bean,
BeanDefinitionRegistry registry, String configuredBeanName, Object source) {
String registeredBeanName = configuredBeanName;
if (GENERATE_BEAN_NAME.equals(configuredBeanName)) {
registeredBeanName = registerWithSourceAndGeneratedBeanName(bean, registry, source);
} else {
bean.setSource(source);
registry.registerBeanDefinition(configuredBeanName, bean);
}
return registeredBeanName;
builder.addPropertyReference("neo4jClient",
source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME));
builder.addPropertyReference("neo4jMappingContext",
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
}
}

View File

@@ -25,9 +25,9 @@ import org.springframework.data.domain.Range;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.PreparedQuery;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.repository.support.PreparedQuery;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.util.Assert;
@@ -41,19 +41,19 @@ import org.springframework.util.Assert;
*/
abstract class AbstractNeo4jQuery implements RepositoryQuery {
protected final NodeManager nodeManager;
protected final Neo4jClient neo4jClient;
protected final Neo4jMappingContext mappingContext;
protected final Neo4jQueryMethod queryMethod;
protected final Class<?> domainType;
AbstractNeo4jQuery(NodeManager nodeManager,
AbstractNeo4jQuery(Neo4jClient neo4jClient,
Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) {
Assert.notNull(nodeManager, "The node manager is required.");
Assert.notNull(neo4jClient, "The Neo4j client is required.");
Assert.notNull(mappingContext, "The mapping context is required.");
Assert.notNull(queryMethod, "Query method must not be null!");
this.nodeManager = nodeManager;
this.neo4jClient = neo4jClient;
this.mappingContext = mappingContext;
this.queryMethod = queryMethod;
this.domainType = queryMethod.getReturnedObjectType();
@@ -66,7 +66,7 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery {
@Override
public final Object execute(Object[] parameters) {
return new Neo4jQueryExecution.DefaultQueryExecution(nodeManager)
return new Neo4jQueryExecution.DefaultQueryExecution(neo4jClient)
.execute(prepareQuery(parameters), queryMethod.isCollectionQuery());
}

View File

@@ -20,9 +20,9 @@ package org.springframework.data.neo4j.repository.query;
import lombok.RequiredArgsConstructor;
import org.springframework.data.neo4j.core.ExecutableQuery;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.PreparedQuery;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.repository.support.ExecutableQuery;
import org.springframework.data.neo4j.repository.support.PreparedQuery;
/**
* Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
@@ -40,12 +40,12 @@ interface Neo4jQueryExecution {
@RequiredArgsConstructor
class DefaultQueryExecution implements Neo4jQueryExecution {
private final NodeManager nodeManager;
private final Neo4jClient neo4jClient;
@Override
public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) {
public Object execute(PreparedQuery description, boolean asCollectionQuery) {
ExecutableQuery executableQuery = nodeManager.toExecutableQuery(preparedQuery);
ExecutableQuery executableQuery = ExecutableQuery.create(description, neo4jClient);
if (asCollectionQuery) {
return executableQuery.getResults();
} else {

View File

@@ -23,7 +23,7 @@ import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
@@ -43,7 +43,7 @@ import org.springframework.data.repository.query.RepositoryQuery;
@RequiredArgsConstructor
public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
private final NodeManager nodeManager;
private final Neo4jClient neo4jClient;
private final Neo4jMappingContext mappingContext;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
@@ -58,12 +58,12 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
namedQueries.getQuery(namedQueryName));
} else if (queryMethod.hasQueryAnnotation()) {
return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod);
return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod);
} else {
return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod);
return new PartTreeNeo4jQuery(neo4jClient, mappingContext, queryMethod);
}
}
}

View File

@@ -35,10 +35,10 @@ import java.util.Map;
import java.util.Set;
import org.neo4j.driver.types.Point;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.PreparedQuery;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters;
import org.springframework.data.neo4j.repository.support.PreparedQuery;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -75,11 +75,11 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
private final PartTree tree;
PartTreeNeo4jQuery(
NodeManager nodeManager,
Neo4jClient neo4jClient,
Neo4jMappingContext mappingContext,
Neo4jQueryMethod queryMethod
) {
super(nodeManager, mappingContext, queryMethod);
super(neo4jClient, mappingContext, queryMethod);
this.tree = new PartTree(queryMethod.getName(), domainType);
@@ -97,7 +97,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
mappingContext, domainType, tree, formalParameters, actualParameters
);
String cypherQuery = queryCreator.createQuery();
Map<String, Object> boundedParameters = formalParameters
.getBindableParameters().stream()

View File

@@ -23,9 +23,9 @@ import java.util.Map;
import java.util.Optional;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.PreparedQuery;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.repository.support.PreparedQuery;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
@@ -92,13 +92,13 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
* Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation
* is expected to have a value.
*
* @param nodeManager
* @param neo4jClient
* @param mappingContext
* @param evaluationContextProvider
* @param queryMethod
* @return A new instance of a String based Neo4j query.
*/
static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext,
static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext,
QueryMethodEvaluationContextProvider evaluationContextProvider,
Neo4jQueryMethod queryMethod) {
@@ -109,36 +109,36 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
.filter(StringUtils::hasText)
.orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not."));
return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete());
}
/**
* Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template.
*
* @param nodeManager
* @param neo4jClient
* @param mappingContext
* @param evaluationContextProvider
* @param queryMethod
* @param cypherTemplate The template to use.
* @return A new instance of a String based Neo4j query.
*/
static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext,
static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext,
QueryMethodEvaluationContextProvider evaluationContextProvider,
Neo4jQueryMethod queryMethod, String cypherTemplate) {
Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template.");
return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
cypherTemplate, false, false, false);
}
private StringBasedNeo4jQuery(NodeManager nodeManager,
private StringBasedNeo4jQuery(Neo4jClient neo4jClient,
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery,
boolean existsQuery, boolean deleteQuery) {
super(nodeManager, mappingContext, queryMethod);
super(neo4jClient, mappingContext, queryMethod);
this.countQuery = countQuery;
this.existsQuery = existsQuery;

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.support;
import static java.util.stream.Collectors.*;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.neo4j.driver.exceptions.NoSuchRecordException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.repository.NoResultException;
@RequiredArgsConstructor public
final class DefaultExecutableQuery<T> implements ExecutableQuery<T> {
private final PreparedQuery<T> preparedQuery;
private final Neo4jClient.RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec;
@Override
public List<T> getResults() {
return fetchSpec.all().stream().collect(toList());
}
@Override
public Optional<T> getSingleResult() {
try {
return fetchSpec.one();
} catch (NoSuchRecordException e) {
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
// So there has been an incorrect result size, but not to few results but to many.
throw new IncorrectResultSizeDataAccessException(1);
}
}
@Override
public T getRequiredSingleResult() {
return fetchSpec.one()
.orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
}
}

View File

@@ -16,7 +16,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
package org.springframework.data.neo4j.repository.support;
import java.util.Map;
import java.util.Optional;
@@ -27,9 +27,9 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.lang.Nullable;
/**
* Typed preparation of a query that is used to create an {@link org.springframework.data.neo4j.core.ExecutableQuery} of the same type.
* Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type.
* <p/>
* When no mapping function is provided, the node manager will assume a simple type to be returned. Otherwise make sure
* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected
* by the mapping function to work correctly.
*

View File

@@ -16,12 +16,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
package org.springframework.data.neo4j.repository.support;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apiguardian.api.API;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.repository.NoResultException;
/**
* Interface for controlling query execution.
@@ -31,9 +35,23 @@ import org.apiguardian.api.API;
* @soundtrack Deichkind - Niveau weshalb warum
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
@API(status = API.Status.INTERNAL, since = "1.0")
public interface ExecutableQuery<T> {
static <T> ExecutableQuery<T> create(PreparedQuery<T> description, Neo4jClient neo4jClient) {
Class<T> resultType = description.getResultType();
Neo4jClient.MappingSpec<Optional<T>, Collection<T>, T> mappingSpec = neo4jClient
.newQuery(description.getCypherQuery())
.bindAll(description.getParameters())
.fetchAs(resultType);
Neo4jClient.RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec = description.getOptionalMappingFunction()
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
.orElse(mappingSpec);
return new DefaultExecutableQuery<>(description, fetchSpec);
}
/**
* @return All results returned by this query.
*/
@@ -41,13 +59,12 @@ public interface ExecutableQuery<T> {
/**
* @return A single result
* @throws NonUniqueResultException
* @throws IncorrectResultSizeDataAccessException
*/
Optional<T> getSingleResult();
/**
* @return A single result
* @throws NonUniqueResultException
* @throws NoResultException
*/
T getRequiredSingleResult();

View File

@@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support;
import java.util.Optional;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy;
@@ -41,12 +41,12 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
*/
final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
private final NodeManager nodeManager;
private final Neo4jClient neo4jClient;
private final Neo4jMappingContext mappingContext;
Neo4jRepositoryFactory(NodeManager nodeManager, Neo4jMappingContext mappingContext) {
this.nodeManager = nodeManager;
Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext) {
this.neo4jClient = neo4jClient;
this.mappingContext = mappingContext;
}
@@ -57,7 +57,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Object getTargetRepository(RepositoryInformation metadata) {
return getTargetRepositoryViaReflection(metadata, nodeManager, mappingContext, metadata.getDomainType());
return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType());
}
@Override
@@ -73,6 +73,6 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, mappingContext, evaluationContextProvider));
return Optional.of(new Neo4jQueryLookupStrategy(neo4jClient, mappingContext, evaluationContextProvider));
}
}

View File

@@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support;
import java.io.Serializable;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
@@ -36,7 +36,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF
public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
private NodeManager nodeManager;
private Neo4jClient neo4jClient;
private Neo4jMappingContext neo4jMappingContext;
@@ -49,8 +49,8 @@ public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
super(repositoryInterface);
}
public void setNodeManager(NodeManager nodeManager) {
this.nodeManager = nodeManager;
public void setNeo4jClient(Neo4jClient neo4jClient) {
this.neo4jClient = neo4jClient;
}
public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
@@ -59,7 +59,7 @@ public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
return new Neo4jRepositoryFactory(nodeManager, neo4jMappingContext);
return new Neo4jRepositoryFactory(neo4jClient, neo4jMappingContext);
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.support;
import lombok.RequiredArgsConstructor;
import org.apiguardian.api.API;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
/**
* This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory
* that needs to be provided by the user and the mapping context provided by our infrastructure. Springs mapping context
* replaces the default noop scanner.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
@API(status = API.Status.INTERNAL, since = "1.0")
@RequiredArgsConstructor
public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean<NodeManager> {
private final NodeManagerFactory target;
private final Neo4jMappingContext neo4jMappingContext;
@Override
public NodeManager getObject() {
return SharedNodeManagerCreator.createSharedNodeManager(this.target);
}
@Override
public Class<?> getObjectType() {
return NodeManager.class;
}
@Override
public void afterPropertiesSet() {
target.setSchema(this.neo4jMappingContext);
target.initialize();
}
}

View File

@@ -16,7 +16,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core;
package org.springframework.data.neo4j.repository.support;
import java.util.Collections;
import java.util.HashMap;
@@ -30,12 +30,19 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.lang.Nullable;
/**
* Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type.
* <p/>
* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected
* by the mapping function to work correctly.
*
* @param <T> The type of the objects returned by this query.
* @see ExecutableQuery#create(PreparedQuery, org.springframework.data.neo4j.core.Neo4jClient)
* @author Michael J. Simons
* @soundtrack Deichkind - Arbeit nervt
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
@API(status = API.Status.INTERNAL, since = "1.0")
public interface PreparedQuery<T> {
static <CT> RequiredBuildStep<CT> queryFor(Class<CT> resultType) {

View File

@@ -1,127 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.support;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ReflectionUtils;
/**
* @author Michael J. Simons
*/
@Slf4j
final class SharedNodeManagerCreator {
private static final Set<String> TRANSACTION_REQUIRING_METHODS;
static {
Set<String> tmp = new HashSet<>();
tmp.add("save");
tmp.add("delete");
TRANSACTION_REQUIRING_METHODS = Collections.unmodifiableSet(tmp);
}
public static NodeManager createSharedNodeManager(NodeManagerFactory nodeManagerFactory) {
return (NodeManager) Proxy.newProxyInstance(SharedNodeManagerCreator.class.getClassLoader(),
new Class<?>[] { NodeManager.class }, new SharedNodeManagerInvocationHandler(nodeManagerFactory));
}
private static class SharedNodeManagerInvocationHandler implements InvocationHandler, Serializable {
private final NodeManagerFactory targetFactory;
SharedNodeManagerInvocationHandler(NodeManagerFactory targetFactory) {
this.targetFactory = targetFactory;
}
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
switch (methodName) {
case "equals":
// Only consider equal when proxies are identical.
return (proxy == args[0]);
case "hashCode":
// Use hashCode of proxy.
return hashCode();
case "toString":
// Deliver toString without touching a target Session.
return "Shared Session proxy for target factory [" + this.targetFactory + "]";
default:
Function<NodeManager, Object> methodCall =
targetSession -> ReflectionUtils.invokeMethod(method, targetSession, args);
return invokeInTransaction(methodName, methodCall);
}
}
private Object invokeInTransaction(String methodName, Function<NodeManager, Object> methodCall) {
// Determine current Session: either the transactional one
// managed by the factory or a temporary one for the given invocation.
NodeManager target = Neo4jTransactionUtils.retrieveTransactionalNodeManager(this.targetFactory);
if (TRANSACTION_REQUIRING_METHODS.contains(methodName)) {
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive()
&& target.getTransaction() != null)) {
throw new IllegalStateException("No NodeManager with actual transaction available "
+ "for current thread - cannot reliably process '" + methodName + "' call");
}
}
// Regular Session operations.
boolean isNewSession = false;
if (target == null) {
log.debug("Creating new Session for shared Session invocation");
target = this.targetFactory.createNodeManager();
isNewSession = true;
}
// Invoke method on current Session.
try {
return methodCall.apply(target);
} finally {
if (isNewSession) {
target.flush();
}
}
}
}
private SharedNodeManagerCreator() {
}
}

View File

@@ -19,6 +19,7 @@
package org.springframework.data.neo4j.repository.support;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
import static lombok.AccessLevel.*;
import static org.springframework.data.neo4j.core.cypher.Cypher.*;
import static org.springframework.data.neo4j.core.schema.NodeDescription.*;
@@ -45,8 +46,7 @@ import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.PreparedQuery;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.cypher.Condition;
import org.springframework.data.neo4j.core.cypher.Conditions;
import org.springframework.data.neo4j.core.cypher.Cypher;
@@ -81,7 +81,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
private static final Renderer renderer = CypherRenderer.create();
private final NodeManager nodeManager;
private final Neo4jClient neo4jClient;
private final Neo4jMappingContext mappingContext;
@@ -91,9 +91,9 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
private final BiFunction<TypeSystem, Record, ?> mappingFunction;
private Expression idExpression;
SimpleNeo4jRepository(NodeManager nodeManager, Neo4jMappingContext mappingContext, Class<T> nodeClass) {
SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class<T> nodeClass) {
this.nodeManager = nodeManager;
this.neo4jClient = neo4jClient;
this.mappingContext = mappingContext;
this.nodeClass = nodeClass;
@@ -124,7 +124,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.orderBy(toSortItems(nodeDescription, sort))
.build();
return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
return createExecutableQuery(statement).getResults();
}
@Override
@@ -137,7 +137,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
Statement statement = returningWithPaging.build();
List<T> allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
List<T> allResult = createExecutableQuery(statement).getResults();
LongSupplier totalCountSupplier = this::count;
return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier);
}
@@ -153,8 +153,8 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
Statement statement = returningWithPaging.build();
List<S> page = nodeManager
.toExecutableQuery(prepareQuery(example.getProbeType(), statement, predicate.parameters)).getResults();
List<S> page = createExecutableQuery(example.getProbeType(), statement, predicate.parameters)
.getResults();
LongSupplier totalCountSupplier = this::count;
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);
}
@@ -173,7 +173,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
@Override
@Transactional
public <S extends T> S save(S entity) {
return this.nodeManager.save(entity);
throw new UnsupportedOperationException("Not there yet.");
}
@Override
@@ -189,7 +189,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id))))
.returning(asterisk())
.build();
return nodeManager.toExecutableQuery(prepareQuery(statement)).getSingleResult();
return createExecutableQuery(statement).getSingleResult();
}
@Override
@@ -200,9 +200,9 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
@Override
public Iterable<T> findAll() {
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk())
.build();
return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty())
.returning(asterisk()).build();
return createExecutableQuery(statement).getResults();
}
@Override
@@ -213,7 +213,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.returning(asterisk())
.build();
return nodeManager.toExecutableQuery(prepareQuery(nodeClass, statement, singletonMap("ids", ids))).getResults();
return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults();
}
@Override
@@ -222,8 +222,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty())
.returning(Functions.count(asterisk())).build();
return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement, Collections.emptyMap()))
.getRequiredSingleResult();
return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getRequiredSingleResult();
}
@Override
@@ -263,8 +262,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.returning(asterisk())
.build();
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
return nodeManager.toExecutableQuery(preparedQuery).getSingleResult();
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult();
}
@Override
@@ -275,8 +273,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.returning(asterisk())
.build();
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
return nodeManager.toExecutableQuery(preparedQuery).getResults();
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults();
}
@Override
@@ -288,8 +285,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.returning(asterisk())
.orderBy(toSortItems(nodeDescription, sort)).build();
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
return nodeManager.toExecutableQuery(preparedQuery).getResults();
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults();
}
@Override
@@ -301,8 +297,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
.returning(Functions.count(asterisk()))
.build();
PreparedQuery<Long> preparedQuery = prepareQuery(Long.class, statement, predicate.parameters);
return nodeManager.toExecutableQuery(preparedQuery).getRequiredSingleResult();
return createExecutableQuery(Long.class, statement, predicate.parameters).getRequiredSingleResult();
}
@Override
@@ -419,11 +414,11 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
return predicate;
}
private PreparedQuery<T> prepareQuery(Statement statement) {
return prepareQuery(nodeClass, statement, Collections.emptyMap());
private ExecutableQuery<T> createExecutableQuery(Statement statement) {
return createExecutableQuery(nodeClass, statement, Collections.emptyMap());
}
private <T> PreparedQuery<T> prepareQuery(Class<T> resultType, Statement statement,
private <RS> ExecutableQuery<RS> createExecutableQuery(Class<RS> resultType, Statement statement,
Map<String, Object> parameters) {
BiFunction<TypeSystem, Record, ?> mappingFunctionToUse = this.mappingFunction;
@@ -431,10 +426,12 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null);
}
return PreparedQuery.queryFor(resultType)
PreparedQuery queryDescription = PreparedQuery.queryFor(resultType)
.withCypherQuery(renderer.render(statement))
.withParameters(parameters)
.usingMappingFunction(mappingFunctionToUse)
.build();
return ExecutableQuery.create(queryDescription, neo4jClient);
}
}

View File

@@ -1,140 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.HashSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.Schema;
/**
* @author Gerrit Meier
* @author Michael J. Simons
*/
class DefaultPersistenceContextTest {
private Schema schema;
private PersistenceContext context;
private EntityTrackingStrategy entityTrackingStrategy;
@BeforeEach
void setup() {
entityTrackingStrategy = mock(EntityTrackingStrategy.class);
when(entityTrackingStrategy.getObjectIdentifier(any()))
.thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0]));
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
mappingContext.setInitialEntitySet(new HashSet<Class<?>>(Arrays.asList(Something.class)));
mappingContext.initialize();
schema = mappingContext;
// override method to return a verifiable mock
context = new DefaultPersistenceContext() {
@Override
EntityTrackingStrategy getEntityTrackingStrategy() {
return entityTrackingStrategy;
}
};
}
@Test
void registerAddsEntityToTrackingStrategy() {
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
context.register(new Something(), nodeDescription);
verify(entityTrackingStrategy).track(any(), any());
}
@Test
void registerTheSameObjectMultipleTimesCallsTrackJustOnce() {
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
Something entity = new Something();
context.register(entity, nodeDescription);
context.register(entity, nodeDescription);
verify(entityTrackingStrategy).track(any(), any());
}
@Test
void registerTwoObjectOfTheSameTypeCallsTrackTwice() {
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
Something entity1 = new Something();
Something entity2 = new Something();
context.register(entity1, nodeDescription);
context.register(entity2, nodeDescription);
verify(entityTrackingStrategy, times(2)).track(any(), any());
}
@Test
void triggersDeltaCalculationOnDeltaCall() {
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
Something entity = new Something();
context.register(entity, nodeDescription);
context.getEntityChanges(entity);
verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity);
}
@Test
void deregisterRemovesEntityFromTracking() {
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
Something entity = new Something();
context.register(entity, nodeDescription);
context.deregister(entity);
verify(entityTrackingStrategy).untrack(entity);
}
@Test
void deregisterUnknownEntityDoesNotCallUntrack() {
Something entity = new Something();
context.deregister(entity);
verify(entityTrackingStrategy, never()).untrack(entity);
}
class Something {
@Id
private Long id;
String value;
}
}

View File

@@ -1,199 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.context.tracking;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.springframework.data.neo4j.core.schema.GraphPropertyDescription;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* @author Gerrit Meier
*/
class EntityComparisonStrategyTest {
private final GraphPropertyDescription valuePropertyDescription;
private final GraphPropertyDescription informationPropertyDescription;
private final GraphPropertyDescription parentPropertyDescription;
private final NodeDescription description;
EntityComparisonStrategyTest() {
this.valuePropertyDescription = mock(GraphPropertyDescription.class);
when(this.valuePropertyDescription.getFieldName()).thenReturn("value");
when(this.valuePropertyDescription.getPropertyName()).thenReturn("value");
this.informationPropertyDescription = mock(GraphPropertyDescription.class);
when(this.informationPropertyDescription.getFieldName()).thenReturn("information");
when(this.informationPropertyDescription.getPropertyName()).thenReturn("information");
this.parentPropertyDescription = mock(GraphPropertyDescription.class);
when(this.parentPropertyDescription.getFieldName()).thenReturn("parentValue");
when(this.parentPropertyDescription.getPropertyName()).thenReturn("parentValue");
this.description = mock(NodeDescription.class);
when(this.description.getPrimaryLabel()).thenReturn("Something");
when(this.description.getGraphProperties()).thenReturn(Arrays
.asList(this.valuePropertyDescription, this.informationPropertyDescription,
this.parentPropertyDescription));
when(this.description.getUnderlyingClass()).thenReturn(Something.class);
}
@Test
void trackSimplePropertyChange() {
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
Something something = new Something("oldValue");
strategy.track(something, description);
String fieldName = "value";
String newValue = "newValue";
something.value = newValue;
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
EntityChangeEvent changeEvent = changeEvents.iterator().next();
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent.getValue()).isEqualTo(newValue);
}
@Test
void trackCollectionPropertyChange() {
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
Something something = new Something("oldValue");
strategy.track(something, description);
String fieldName = "information";
something.information.add("additional entry");
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
EntityChangeEvent changeEvent = changeEvents.iterator().next();
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent.getValue()).isInstanceOf(Integer.class);
}
@Test
void trackCollectionPropertyReorderChange() {
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
Something something = new Something("blubb");
something.information.add("entry 1");
something.information.add("entry 2");
strategy.track(something, description);
something.information.sort(Comparator.reverseOrder());
String fieldName = "information";
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
EntityChangeEvent changeEvent = changeEvents.iterator().next();
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent.getValue()).isInstanceOf(Integer.class);
}
@Test
void trackParentClassPropertyChange() {
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
Something something = new Something("oldValue");
strategy.track(something, description);
String fieldName = "parentValue";
String newValue = "newValue";
something.parentValue = newValue;
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
EntityChangeEvent changeEvent = changeEvents.iterator().next();
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent.getValue()).isEqualTo(newValue);
}
@Test
void trackMultipleObjects() {
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
Something something1 = new Something("oldValue");
Something something2 = new Something("oldValue");
strategy.track(something1, description);
strategy.track(something2, description);
String fieldName = "value";
String newValue1 = "newValue1";
String newValue2 = "newValue2";
something1.value = newValue1;
something2.value = newValue2;
Collection<EntityChangeEvent> changeEvents1 = strategy.getAggregatedEntityChangeEvents(something1);
Collection<EntityChangeEvent> changeEvents2 = strategy.getAggregatedEntityChangeEvents(something2);
EntityChangeEvent changeEvent1 = changeEvents1.iterator().next();
assertThat(changeEvent1.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent1.getValue()).isEqualTo(newValue1);
EntityChangeEvent changeEvent2 = changeEvents2.iterator().next();
assertThat(changeEvent2.getPropertyField()).isEqualTo(fieldName);
assertThat(changeEvent2.getValue()).isEqualTo(newValue2);
}
class ParentClass {
String parentValue;
}
class Something extends ParentClass {
final List<String> information = new ArrayList<>();
String value;
Something(String value) {
this.value = value;
}
// create own equals and hashCode that should not get used in any technical parts of the dirty tracking
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Something something = (Something) o;
return value.equals(something.value) && information.equals(something.information);
}
@Override
public int hashCode() {
return Objects.hash(value, information);
}
}
}

View File

@@ -50,14 +50,12 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
@@ -830,25 +828,12 @@ class RepositoryIT {
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
static class Config {
static class Config extends AbstractNeo4jConfig {
@Bean
public static Driver driver() {
public Driver driver() {
return neo4jConnectionSupport.openConnection();
}
@Bean
public NodeManagerFactory nodeManagerFactory(Driver driver) {
return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class,
PersonWithWither.class, ThingWithAssignedId.class, KotlinPerson.class);
}
@Bean
public PlatformTransactionManager transactionManager(Driver driver) {
return new Neo4jTransactionManager(driver);
}
}
}

View File

@@ -35,7 +35,7 @@ import org.neo4j.driver.Values;
import org.neo4j.driver.types.Point;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -70,7 +70,7 @@ final class RepositoryQueryTest {
private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory();
@Mock
NodeManager nodeManager;
Neo4jClient neo4jClient;
@Mock
Neo4jMappingContext schema;
@@ -97,7 +97,7 @@ final class RepositoryQueryTest {
@Test
void shouldSelectPartTreeNeo4jQuery() {
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
RepositoryQuery query = lookupStrategy
@@ -109,7 +109,7 @@ final class RepositoryQueryTest {
@Test
void shouldSelectStringBasedNeo4jQuery() {
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
RepositoryQuery query = lookupStrategy
@@ -125,7 +125,7 @@ final class RepositoryQueryTest {
when(namedQueries.hasQuery(namedQueryName)).thenReturn(true);
when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n");
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
RepositoryQuery query = lookupStrategy
@@ -180,7 +180,7 @@ final class RepositoryQueryTest {
assertThatExceptionOfType(MappingException.class)
.isThrownBy(
() -> StringBasedNeo4jQuery.create(mock(NodeManager.class), mock(Neo4jMappingContext.class),
() -> StringBasedNeo4jQuery.create(mock(Neo4jClient.class), mock(Neo4jMappingContext.class),
QueryMethodEvaluationContextProvider.DEFAULT, method))
.withMessage("Expected @Query annotation to have a value, but it did not.");
}
@@ -191,7 +191,7 @@ final class RepositoryQueryTest {
Neo4jQueryMethod method = RepositoryQueryTest
.neo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, String.class);
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class),
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class),
mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT,
method);
@@ -210,7 +210,7 @@ final class RepositoryQueryTest {
.neo4jQueryMethod("findByDontDoThisInRealLiveNamed", org.neo4j.driver.types.Point.class, String.class,
String.class);
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class),
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class),
mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT,
method);

View File

@@ -1,96 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.IdDescription;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* @author Gerrit Meier
* @author Michael J. Simons
*/
class SimpleNeo4jRepositoryTest {
private NodeManager nodeManager;
private Neo4jMappingContext mappingContext;
private NodeDescription nodeDescription;
private SimpleNeo4jRepository<TestNode, Long> repository;
@BeforeEach
void setupMock() {
nodeManager = mock(NodeManager.class);
nodeDescription = mock(NodeDescription.class);
when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode");
when(nodeDescription.getIdDescription()).thenReturn(new IdDescription());
mappingContext = mock(Neo4jMappingContext.class);
when(mappingContext.getRequiredNodeDescription(TestNode.class)).thenReturn(nodeDescription);
repository = new SimpleNeo4jRepository(this.nodeManager, this.mappingContext, TestNode.class);
}
@Test
void saveNotImplemented() {
repository.save(null); // todo this should throw an exception upfront
verify(nodeManager).save(any());
}
@Test
void saveAllNotImplemented() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.saveAll(null));
}
@Test
void deleteByIdNotImplemented() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteById(null));
}
@Test
void deleteNotImplemented() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.delete(null));
}
@Test
void deleteAll1NotImplemented() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> repository.deleteAll(Collections.emptyList()));
}
@Node
class TestNode {
@Id
private Long id;
private String value;
}
}